From 8335cb82e8fd4b1e80103b510ac35a533a3b171a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E6=AD=A3=E6=B5=B7?= Date: Tue, 14 Apr 2026 16:20:33 +0800 Subject: [PATCH 01/10] feat: add deployment automation (#1) * feat: add deployment automation (CI/CD, systemd, Nginx) - Add backend CI/CD: Go build + deploy via SSH, auto-rollback on failure - Add frontend CI/CD: Cloudflare Pages deployment - Add deploy scripts: first-time setup (Nginx + SSL) and update deploy - Add systemd service, Nginx reverse proxy template, .env.example - Add deployment documentation with scaling guide - Remove unused upstream workflows (Docker, Electron, Gitee sync, etc.) Co-Authored-By: Claude Opus 4.6 * docs: improve .env.example with detailed comments Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .github/workflows/deploy-backend.yml | 99 ++++++++ .github/workflows/deploy-web.yml | 102 ++++++++ .github/workflows/docker-image-alpha.yml | 179 ------------- .github/workflows/docker-image-arm64.yml | 182 -------------- .github/workflows/electron-build.yml | 141 ----------- .github/workflows/pr-check.yml | 33 --- .github/workflows/release.yml | 156 ------------ .github/workflows/sync-to-gitee.yml | 91 ------- deploy/.env.example | 36 +++ deploy/README.md | 303 +++++++++++++++++++++++ deploy/deploy-all.sh | 176 +++++++++++++ deploy/deploy.sh | 190 ++++++++++++++ deploy/new-api.service | 24 ++ deploy/nginx-new-api.conf | 49 ++++ makefile | 36 ++- 15 files changed, 1005 insertions(+), 792 deletions(-) create mode 100644 .github/workflows/deploy-backend.yml create mode 100644 .github/workflows/deploy-web.yml delete mode 100644 .github/workflows/docker-image-alpha.yml delete mode 100644 .github/workflows/docker-image-arm64.yml delete mode 100644 .github/workflows/electron-build.yml delete mode 100644 .github/workflows/pr-check.yml delete mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/sync-to-gitee.yml create mode 100644 deploy/.env.example create mode 100644 deploy/README.md create mode 100755 deploy/deploy-all.sh create mode 100755 deploy/deploy.sh create mode 100644 deploy/new-api.service create mode 100644 deploy/nginx-new-api.conf diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml new file mode 100644 index 000000000000..df2cee3002bd --- /dev/null +++ b/.github/workflows/deploy-backend.yml @@ -0,0 +1,99 @@ +name: Backend CI/CD + +on: + push: + branches: [main] + paths: + - '**.go' + - 'go.mod' + - 'go.sum' + - 'VERSION' + - 'deploy/**' + - '.github/workflows/deploy-backend.yml' + workflow_dispatch: + +# 防止同时部署 +concurrency: + group: backend-${{ github.ref }} + cancel-in-progress: true + +env: + GO_VERSION: '1.23' + API_DOMAIN: 'api.4aicode.com' + BACKEND_PORT: '3002' + BACKEND_HOST: '127.0.0.1' + SSL_EMAIL: 'ceo@richcalls.xyz' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Run tests + run: go test ./... -v + continue-on-error: true + + - name: Slack 通知 + if: always() + uses: forecho/slack-deploy-action@v1 + with: + slack_bot_token: ${{ secrets.SLACK_BOT_TOKEN }} + slack_channel_id: ${{ secrets.SLACK_CHANNEL_ID }} + language: zh + service_name: 后端测试 + + build-and-deploy: + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: 编译 + run: | + VERSION=$(cat VERSION 2>/dev/null || echo "dev") + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=${VERSION}'" \ + -o new-api + + - name: 上传到服务器 + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + source: "new-api,deploy/" + target: "/tmp/new-api-deploy/" + strip_components: 0 + + - name: 执行部署 + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + script: | + cd /tmp/new-api-deploy + chmod +x deploy/deploy.sh + sudo ./deploy/deploy.sh "/tmp/new-api-deploy" "${{ env.API_DOMAIN }}" "${{ env.BACKEND_PORT }}" "${{ env.BACKEND_HOST }}" "${{ env.SSL_EMAIL }}" + + - name: Slack 通知 + if: always() + uses: forecho/slack-deploy-action@v1 + with: + slack_bot_token: ${{ secrets.SLACK_BOT_TOKEN }} + slack_channel_id: ${{ secrets.SLACK_CHANNEL_ID }} + language: zh + service_name: 后端部署 diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml new file mode 100644 index 000000000000..c41cfa2c0c2e --- /dev/null +++ b/.github/workflows/deploy-web.yml @@ -0,0 +1,102 @@ +name: Frontend CI/CD + +on: + push: + branches: [main] + paths: + - 'web/**' + - '.github/workflows/deploy-web.yml' + pull_request: + branches: [main] + paths: + - 'web/**' + +permissions: + contents: read + deployments: write + pull-requests: write + +env: + VITE_REACT_APP_SERVER: 'https://api.4aicode.com' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: web/package-lock.json + + - name: 安装依赖 + working-directory: ./web + run: npm ci + + - name: 类型检查 + working-directory: ./web + run: npm run type-check 2>/dev/null || true + continue-on-error: true + + - name: 构建 + working-directory: ./web + run: npm run build + + - name: 保存构建产物 + uses: actions/upload-artifact@v4 + with: + name: web-dist + path: web/dist + + - name: Slack 通知 + if: always() + uses: forecho/slack-deploy-action@v1 + with: + slack_bot_token: ${{ secrets.SLACK_BOT_TOKEN }} + slack_channel_id: ${{ secrets.SLACK_CHANNEL_ID }} + language: zh + service_name: 前端构建 + + deploy: + needs: build + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: web/package-lock.json + + - name: 安装依赖 + working-directory: ./web + run: npm ci + + - name: 构建 + working-directory: ./web + run: npm run build + + - name: 部署到 Cloudflare Pages + uses: cloudflare/pages-action@v1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: 4aicode + directory: web/dist + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + + - name: Slack 通知 + if: always() + uses: forecho/slack-deploy-action@v1 + with: + slack_bot_token: ${{ secrets.SLACK_BOT_TOKEN }} + slack_channel_id: ${{ secrets.SLACK_CHANNEL_ID }} + language: zh + service_name: 前端部署 diff --git a/.github/workflows/docker-image-alpha.yml b/.github/workflows/docker-image-alpha.yml deleted file mode 100644 index 116dd1452152..000000000000 --- a/.github/workflows/docker-image-alpha.yml +++ /dev/null @@ -1,179 +0,0 @@ -name: Publish Docker image (alpha) - -on: - push: - branches: - - alpha - workflow_dispatch: - inputs: - name: - description: "reason" - required: false - -jobs: - build_single_arch: - name: Build & push (${{ matrix.arch }}) [native] - strategy: - fail-fast: false - matrix: - include: - - arch: amd64 - platform: linux/amd64 - runner: ubuntu-latest - - arch: arm64 - platform: linux/arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} - permissions: - packages: write - contents: read - id-token: write - steps: - - name: Check out (shallow) - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 1 - - - name: Determine alpha version - id: version - run: | - VERSION="alpha-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)" - echo "$VERSION" > VERSION - echo "value=$VERSION" >> $GITHUB_OUTPUT - echo "VERSION=$VERSION" >> $GITHUB_ENV - echo "Publishing version: $VERSION for ${{ matrix.arch }}" - - - name: Normalize GHCR repository - run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - - - name: Log in to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Log in to GHCR - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (labels) - id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 - with: - images: | - calciumion/new-api - ghcr.io/${{ env.GHCR_REPOSITORY }} - - - name: Build & push single-arch (to both registries) - id: build - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 - with: - context: . - platforms: ${{ matrix.platform }} - push: true - tags: | - calciumion/new-api:alpha-${{ matrix.arch }} - calciumion/new-api:${{ steps.version.outputs.value }}-${{ matrix.arch }} - ghcr.io/${{ env.GHCR_REPOSITORY }}:alpha-${{ matrix.arch }} - ghcr.io/${{ env.GHCR_REPOSITORY }}:${{ steps.version.outputs.value }}-${{ matrix.arch }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - provenance: mode=max - sbom: true - - - name: Install cosign - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 - - - name: Sign image with cosign - run: | - cosign sign --yes calciumion/new-api@${{ steps.build.outputs.digest }} - cosign sign --yes ghcr.io/${{ env.GHCR_REPOSITORY }}@${{ steps.build.outputs.digest }} - - - name: Output digest - run: | - echo "### Docker Image Digest (${{ matrix.arch }})" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "calciumion/new-api:alpha-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY - echo "ghcr.io/${{ env.GHCR_REPOSITORY }}:alpha-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY - echo "${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - create_manifests: - name: Create multi-arch manifests (Docker Hub + GHCR) - needs: [build_single_arch] - runs-on: ubuntu-latest - permissions: - packages: write - contents: read - steps: - - name: Check out (shallow) - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 1 - - - name: Normalize GHCR repository - run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - - - name: Determine alpha version - id: version - run: | - VERSION="alpha-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)" - echo "value=$VERSION" >> $GITHUB_OUTPUT - echo "VERSION=$VERSION" >> $GITHUB_ENV - - - name: Log in to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Create & push manifest (Docker Hub - alpha) - run: | - docker buildx imagetools create \ - -t calciumion/new-api:alpha \ - calciumion/new-api:alpha-amd64 \ - calciumion/new-api:alpha-arm64 - - - name: Create & push manifest (Docker Hub - versioned alpha) - run: | - docker buildx imagetools create \ - -t calciumion/new-api:${VERSION} \ - calciumion/new-api:${VERSION}-amd64 \ - calciumion/new-api:${VERSION}-arm64 - - - name: Log in to GHCR - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Create & push manifest (GHCR - alpha) - run: | - docker buildx imagetools create \ - -t ghcr.io/${GHCR_REPOSITORY}:alpha \ - ghcr.io/${GHCR_REPOSITORY}:alpha-amd64 \ - ghcr.io/${GHCR_REPOSITORY}:alpha-arm64 - - - name: Create & push manifest (GHCR - versioned alpha) - run: | - docker buildx imagetools create \ - -t ghcr.io/${GHCR_REPOSITORY}:${VERSION} \ - ghcr.io/${GHCR_REPOSITORY}:${VERSION}-amd64 \ - ghcr.io/${GHCR_REPOSITORY}:${VERSION}-arm64 - - - name: Output manifest digest - run: | - echo "### Multi-arch Manifest Digests" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - docker buildx imagetools inspect calciumion/new-api:alpha >> $GITHUB_STEP_SUMMARY - echo "---" >> $GITHUB_STEP_SUMMARY - docker buildx imagetools inspect ghcr.io/${GHCR_REPOSITORY}:alpha >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/docker-image-arm64.yml b/.github/workflows/docker-image-arm64.yml deleted file mode 100644 index 83303ee30612..000000000000 --- a/.github/workflows/docker-image-arm64.yml +++ /dev/null @@ -1,182 +0,0 @@ -name: Publish Docker image (Multi Registries, native amd64+arm64) - -on: - push: - tags: - - '*' - - '!nightly*' - workflow_dispatch: - inputs: - tag: - description: 'Tag name to build (e.g., v0.10.8-alpha.3)' - required: true - type: string - -jobs: - build_single_arch: - name: Build & push (${{ matrix.arch }}) [native] - strategy: - fail-fast: false - matrix: - include: - - arch: amd64 - platform: linux/amd64 - runner: ubuntu-latest - - arch: arm64 - platform: linux/arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} - - permissions: - packages: write - contents: read - id-token: write - - steps: - - name: Check out - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: ${{ github.event_name == 'workflow_dispatch' && 0 || 1 }} - ref: ${{ github.event.inputs.tag || github.ref }} - - - name: Resolve tag & write VERSION - run: | - if [ -n "${{ github.event.inputs.tag }}" ]; then - TAG="${{ github.event.inputs.tag }}" - # Verify tag exists - if ! git rev-parse "refs/tags/$TAG" >/dev/null 2>&1; then - echo "Error: Tag '$TAG' does not exist in the repository" - exit 1 - fi - else - TAG=${GITHUB_REF#refs/tags/} - fi - echo "TAG=$TAG" >> $GITHUB_ENV - echo "$TAG" > VERSION - echo "Building tag: $TAG for ${{ matrix.arch }}" - - -# - name: Normalize GHCR repository -# run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - - - name: Log in to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - -# - name: Log in to GHCR -# uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 -# with: -# registry: ghcr.io -# username: ${{ github.actor }} -# password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (labels) - id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 - with: - images: | - calciumion/new-api -# ghcr.io/${{ env.GHCR_REPOSITORY }} - - - name: Build & push single-arch (to both registries) - id: build - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 - with: - context: . - platforms: ${{ matrix.platform }} - push: true - tags: | - calciumion/new-api:${{ env.TAG }}-${{ matrix.arch }} - calciumion/new-api:latest-${{ matrix.arch }} -# ghcr.io/${{ env.GHCR_REPOSITORY }}:${{ env.TAG }}-${{ matrix.arch }} -# ghcr.io/${{ env.GHCR_REPOSITORY }}:latest-${{ matrix.arch }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - provenance: mode=max - sbom: true - - - name: Install cosign - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 - - - name: Sign image with cosign - run: cosign sign --yes calciumion/new-api@${{ steps.build.outputs.digest }} - - - name: Output digest - run: | - echo "### Docker Image Digest (${{ matrix.arch }})" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "calciumion/new-api:${{ env.TAG }}-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY - echo "${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - create_manifests: - name: Create multi-arch manifests (Docker Hub) - needs: [build_single_arch] - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' - steps: - - name: Extract tag - run: | - if [ -n "${{ github.event.inputs.tag }}" ]; then - echo "TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV - else - echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - fi -# -# - name: Normalize GHCR repository -# run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - - - name: Log in to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Create & push manifest (Docker Hub - version) - run: | - docker buildx imagetools create \ - -t calciumion/new-api:${TAG} \ - calciumion/new-api:${TAG}-amd64 \ - calciumion/new-api:${TAG}-arm64 - - - name: Create & push manifest (Docker Hub - latest) - run: | - docker buildx imagetools create \ - -t calciumion/new-api:latest \ - calciumion/new-api:latest-amd64 \ - calciumion/new-api:latest-arm64 - - - name: Output manifest digest - run: | - echo "### Multi-arch Manifest" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - docker buildx imagetools inspect calciumion/new-api:${TAG} >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - # ---- GHCR ---- -# - name: Log in to GHCR -# uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 -# with: -# registry: ghcr.io -# username: ${{ github.actor }} -# password: ${{ secrets.GITHUB_TOKEN }} - -# - name: Create & push manifest (GHCR - version) -# run: | -# docker buildx imagetools create \ -# -t ghcr.io/${GHCR_REPOSITORY}:${TAG} \ -# ghcr.io/${GHCR_REPOSITORY}:${TAG}-amd64 \ -# ghcr.io/${GHCR_REPOSITORY}:${TAG}-arm64 -# -# - name: Create & push manifest (GHCR - latest) -# run: | -# docker buildx imagetools create \ -# -t ghcr.io/${GHCR_REPOSITORY}:latest \ -# ghcr.io/${GHCR_REPOSITORY}:latest-amd64 \ -# ghcr.io/${GHCR_REPOSITORY}:latest-arm64 diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml deleted file mode 100644 index 20113e00fe6b..000000000000 --- a/.github/workflows/electron-build.yml +++ /dev/null @@ -1,141 +0,0 @@ -name: Build Electron App - -on: - push: - tags: - - '*' # Triggers on version tags like v1.0.0 - - '!*-*' # Ignore pre-release tags like v1.0.0-beta - - '!*-alpha*' # Ignore alpha tags like v1.0.0-alpha - workflow_dispatch: # Allows manual triggering - -jobs: - build: - strategy: - matrix: - # os: [macos-latest, windows-latest] - os: [windows-latest] - - runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: '>=1.25.1' - - - name: Build frontend - env: - CI: "" - NODE_OPTIONS: "--max-old-space-size=4096" - run: | - cd web - bun install - DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build - cd .. - - # - name: Build Go binary (macos/Linux) - # if: runner.os != 'Windows' - # run: | - # go mod download - # go build -ldflags "-s -w -X 'new-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api - - - name: Build Go binary (Windows) - if: runner.os == 'Windows' - run: | - go mod download - go build -ldflags "-s -w -X 'new-api/common.Version=$(git describe --tags)'" -o new-api.exe - - - name: Update Electron version - run: | - cd electron - VERSION=$(git describe --tags) - VERSION=${VERSION#v} # Remove 'v' prefix if present - # Convert to valid semver: take first 3 components and convert rest to prerelease format - # e.g., 0.9.3-patch.1 -> 0.9.3-patch.1 - if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(.*)$ ]]; then - MAJOR=${BASH_REMATCH[1]} - MINOR=${BASH_REMATCH[2]} - PATCH=${BASH_REMATCH[3]} - REST=${BASH_REMATCH[4]} - - VERSION="$MAJOR.$MINOR.$PATCH" - - # If there's extra content, append it without adding -dev - if [[ -n "$REST" ]]; then - VERSION="$VERSION$REST" - fi - fi - npm version $VERSION --no-git-tag-version --allow-same-version - - - name: Install Electron dependencies - run: | - cd electron - npm install - - # - name: Build Electron app (macOS) - # if: runner.os == 'macOS' - # run: | - # cd electron - # npm run build:mac - # env: - # CSC_IDENTITY_AUTO_DISCOVERY: false # Skip code signing - - - name: Build Electron app (Windows) - if: runner.os == 'Windows' - run: | - cd electron - npm run build:win - - # - name: Upload artifacts (macOS) - # if: runner.os == 'macOS' - # uses: actions/upload-artifact@v4 - # with: - # name: macos-build - # path: | - # electron/dist/*.dmg - # electron/dist/*.zip - - - name: Upload artifacts (Windows) - if: runner.os == 'Windows' - uses: actions/upload-artifact@v4 - with: - name: windows-build - path: | - electron/dist/*.exe - - release: - needs: build - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') - permissions: - contents: write - - steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 - - - name: Upload to Release - uses: softprops/action-gh-release@v2 - with: - files: | - windows-build/* - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml deleted file mode 100644 index 2dcda35e676e..000000000000 --- a/.github/workflows/pr-check.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: PR Check - -permissions: - contents: read - issues: read - pull-requests: read - -on: - pull_request_target: - types: [opened, reopened] - -jobs: - pr-quality: - runs-on: ubuntu-latest - steps: - - uses: peakoss/anti-slop@v0.2.1 - with: - max-failures: 4 - require-description: true - - # require-linked-issue: false - blocked-terms: | - 🤖 Generated with Claude Code - - require-pr-template: true - strict-pr-template-sections: "✅ 提交前检查项 / Checklist" - - detect-spam-usernames: true - min-account-age: 30 - - failure-add-pr-labels: "pr-check-failed" - failure-pr-message: "感谢您的提交。由于该 PR 未遵循我们的贡献模板,且被识别为缺乏人工参与的纯 AI 生成内容 (AI Slop),我们将先予以关闭。我们更欢迎经过人工审核、验证并带有个人思考的贡献。如果您认为这其中存在误解,请回复告知。/ Thank you for your submission. This PR has been closed because it does not follow our contribution template and has been identified as purely AI-generated content (AI Slop) without meaningful human involvement. We prioritize contributions that are human-verified and reflect individual effort. If you believe this is a mistake, please let us know by replying to this comment." - close-pr: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 1a903322033d..000000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,156 +0,0 @@ -name: Release (Linux, macOS, Windows) -permissions: - contents: write - -on: - workflow_dispatch: - inputs: - name: - description: 'reason' - required: false - push: - tags: - - '*' - - '!*-alpha*' - -jobs: - linux: - name: Linux Release - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 0 - - name: Determine Version - run: | - VERSION=$(git describe --tags) - echo "VERSION=$VERSION" >> $GITHUB_ENV - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - with: - bun-version: latest - - name: Build Frontend - env: - CI: "" - run: | - cd web - bun install - DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build - cd .. - - name: Set up Go - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 - with: - go-version: '>=1.25.1' - - name: Build Backend (amd64) - run: | - go mod download - go build -ldflags "-s -w -X 'new-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-$VERSION - - name: Build Backend (arm64) - run: | - sudo apt-get update - DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gcc-aarch64-linux-gnu - CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'new-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-arm64-$VERSION - - name: Generate checksums - run: sha256sum new-api-* > checksums-linux.txt - - - name: Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 - if: startsWith(github.ref, 'refs/tags/') - with: - files: | - new-api-* - checksums-linux.txt - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - macos: - name: macOS Release - runs-on: macos-latest - steps: - - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 0 - - name: Determine Version - run: | - VERSION=$(git describe --tags) - echo "VERSION=$VERSION" >> $GITHUB_ENV - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - with: - bun-version: latest - - name: Build Frontend - env: - CI: "" - NODE_OPTIONS: "--max-old-space-size=4096" - run: | - cd web - bun install - DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build - cd .. - - name: Set up Go - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 - with: - go-version: '>=1.25.1' - - name: Build Backend - run: | - go mod download - go build -ldflags "-X 'new-api/common.Version=$VERSION'" -o new-api-macos-$VERSION - - name: Generate checksums - run: shasum -a 256 new-api-macos-* > checksums-macos.txt - - - name: Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 - if: startsWith(github.ref, 'refs/tags/') - with: - files: | - new-api-macos-* - checksums-macos.txt - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - windows: - name: Windows Release - runs-on: windows-latest - defaults: - run: - shell: bash - steps: - - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 0 - - name: Determine Version - run: | - VERSION=$(git describe --tags) - echo "VERSION=$VERSION" >> $GITHUB_ENV - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - with: - bun-version: latest - - name: Build Frontend - env: - CI: "" - run: | - cd web - bun install - DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build - cd .. - - name: Set up Go - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 - with: - go-version: '>=1.25.1' - - name: Build Backend - run: | - go mod download - go build -ldflags "-s -w -X 'new-api/common.Version=$VERSION'" -o new-api-$VERSION.exe - - name: Generate checksums - run: sha256sum new-api-*.exe > checksums-windows.txt - - - name: Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 - if: startsWith(github.ref, 'refs/tags/') - with: - files: | - new-api-*.exe - checksums-windows.txt - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sync-to-gitee.yml b/.github/workflows/sync-to-gitee.yml deleted file mode 100644 index 4f515a188dbe..000000000000 --- a/.github/workflows/sync-to-gitee.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Sync Release to Gitee - -permissions: - contents: read - -on: - workflow_dispatch: - inputs: - tag_name: - description: 'Release Tag to sync (e.g. v1.0.0)' - required: true - type: string - -# 配置你的 Gitee 仓库信息 -env: - GITEE_OWNER: 'QuantumNous' # 修改为你的 Gitee 用户名 - GITEE_REPO: 'new-api' # 修改为你的 Gitee 仓库名 - -jobs: - sync-to-gitee: - runs-on: sync - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Get Release Info - id: release_info - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG_NAME: ${{ github.event.inputs.tag_name }} - run: | - # 获取 release 信息 - RELEASE_INFO=$(gh release view "$TAG_NAME" --json name,body,tagName,targetCommitish) - - RELEASE_NAME=$(echo "$RELEASE_INFO" | jq -r '.name') - TARGET_COMMITISH=$(echo "$RELEASE_INFO" | jq -r '.targetCommitish') - - # 使用多行字符串输出 - { - echo "release_name=$RELEASE_NAME" - echo "target_commitish=$TARGET_COMMITISH" - echo "release_body<> $GITHUB_OUTPUT - - # 下载 release 的所有附件 - gh release download "$TAG_NAME" --dir ./release_assets || echo "No assets to download" - - # 列出下载的文件 - ls -la ./release_assets/ || echo "No assets directory" - - - name: Create Gitee Release - id: create_release - uses: nICEnnnnnnnLee/action-gitee-release@v2.0.0 - with: - gitee_action: create_release - gitee_owner: ${{ env.GITEE_OWNER }} - gitee_repo: ${{ env.GITEE_REPO }} - gitee_token: ${{ secrets.GITEE_TOKEN }} - gitee_tag_name: ${{ github.event.inputs.tag_name }} - gitee_release_name: ${{ steps.release_info.outputs.release_name }} - gitee_release_body: ${{ steps.release_info.outputs.release_body }} - gitee_target_commitish: ${{ steps.release_info.outputs.target_commitish }} - - - name: Upload Assets to Gitee - if: hashFiles('release_assets/*') != '' - uses: nICEnnnnnnnLee/action-gitee-release@v2.0.0 - with: - gitee_action: upload_asset - gitee_owner: ${{ env.GITEE_OWNER }} - gitee_repo: ${{ env.GITEE_REPO }} - gitee_token: ${{ secrets.GITEE_TOKEN }} - gitee_release_id: ${{ steps.create_release.outputs.release-id }} - gitee_upload_retry_times: 3 - gitee_files: | - release_assets/* - - - name: Cleanup - if: always() - run: | - rm -rf release_assets/ - - - name: Summary - if: success() - run: | - echo "✅ Successfully synced release ${{ github.event.inputs.tag_name }} to Gitee!" - echo "🔗 Gitee Release URL: https://gitee.com/${{ env.GITEE_OWNER }}/${{ env.GITEE_REPO }}/releases/tag/${{ github.event.inputs.tag_name }}" - diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 000000000000..dceea1e9fd4d --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,36 @@ +# New API 环境变量配置 +# 复制为 .env 并上传到服务器: +# scp .env hetzner:/opt/new-api/.env +# ssh hetzner 'chmod 600 /opt/new-api/.env' + +# ====== 数据库 ====== +# 格式: 用户名:密码@tcp(地址:端口)/数据库名 +SQL_DSN=new_api:password@tcp(127.0.0.1:3306)/new_api + +# ====== Redis ====== +# 无密码: redis://127.0.0.1:6379 +# 有密码: redis://default:password@127.0.0.1:6379 +# 多项目共用同一个 Redis 时,用不同的数据库编号(0-15)避免 key 冲突: +# redis://127.0.0.1:6379/2 +REDIS_CONN_STRING=redis://127.0.0.1:6379/2 + +# ====== 密钥 ====== +# 生成方式: openssl rand -hex 32 +# 所有节点(主节点 + 从节点)必须保持完全一致,否则用户会话会失效 +SESSION_SECRET= +CRYPTO_SECRET= + +# ====== 节点配置 ====== +# 主节点留空,从节点设为 slave +NODE_TYPE= +# 主从同步间隔(秒) +SYNC_FREQUENCY=60 + +# ====== 批量更新 ====== +# API 调用产生的配额消耗(token 用量、余额扣减)先攒在内存里, +# 每隔 BATCH_UPDATE_INTERVAL 秒批量写入数据库,减少数据库压力。建议保持开启。 +BATCH_UPDATE_ENABLED=true +BATCH_UPDATE_INTERVAL=5 + +# ====== 时区 ====== +TZ=Asia/Shanghai diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 000000000000..f38b8a7b198a --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,303 @@ +# New API 部署文档 + +## 目录 + +- [架构概览](#架构概览) +- [自动部署(日常使用)](#自动部署日常使用) +- [首次部署](#首次部署) +- [扩展:添加从节点](#扩展添加从节点) +- [运维命令](#运维命令) +- [环境变量说明](#环境变量说明) +- [故障排查](#故障排查) + +--- + +## 架构概览 + +``` + 浏览器 + │ │ + │ └── 页面/静态资源 ──→ Cloudflare Pages (前端) + │ + └── /api ──→ Nginx (HTTPS) ──→ new-api :3002 (systemd) + │ + ┌───────┴────────┐ + ▼ ▼ + MySQL 8.0 Redis 7.0 + (同一台机器) +``` + +**当前部署环境**:hetzner 服务器(Ubuntu 24.04, 4C/8G),MySQL 和 Redis 已有,New API 部署在同一台机器上。 + +**后续扩展**:加新服务器只跑 New API 从节点,MySQL/Redis 继续用现有服务器,通过 Nginx upstream 负载均衡。 + +--- + +## 自动部署(日常使用) + +部署完成后,日常只需要 push 代码,不需要登录服务器: + +| 操作 | 触发方式 | 自动执行 | +|------|---------|---------| +| 改后端代码 | push `.go`/`go.mod`/`go.sum`/`VERSION` 到 main | CI 编译 → 上传 → 重启服务,失败自动回滚 | +| 改前端代码 | push `web/` 目录到 main | Cloudflare Pages 自动构建部署 | +| 改部署配置 | push `deploy/` 目录到 main | CI 重新部署 | +| 手动触发 | GitHub Actions 页面点 "Run workflow" | 同上 | + +--- + +## 首次部署 + +### 第一步:准备 .env + +参考 `deploy/.env.example` 创建 `.env` 文件,填入实际的数据库和 Redis 连接信息。 + +生成密钥: + +```bash +openssl rand -hex 32 # SESSION_SECRET +openssl rand -hex 32 # CRYPTO_SECRET +``` + +上传到服务器: + +```bash +scp .env hetzner:/opt/new-api/.env +ssh hetzner 'chmod 600 /opt/new-api/.env' +``` + +### 第二步:首次部署 + +```bash +make deploy +``` + +deploy.sh 会自动检测到是首次部署(Nginx 配置不存在),执行完整流程: + +1. 创建 `/opt/new-api/` 目录和 `new-api` 系统用户 +2. 安装二进制文件 +3. 安装 systemd 服务 +4. 安装 Nginx 并配置反向代理 +5. 申请 Let's Encrypt SSL 证书 +6. 启动服务并验证 + +### 第三步:配置 DNS + +添加 A 记录: + +``` +api.4aicode.com → +``` + +### 第四步:配置 GitHub Secrets + +在 `richcalls/new-api` 仓库的 Settings → Secrets and variables → Actions 添加: + +| Secret | 值 | 必需 | +|--------|-----|------| +| `DEPLOY_HOST` | hetzner 服务器公网 IP | ✅ | +| `DEPLOY_USER` | `root` | ✅ | +| `DEPLOY_SSH_KEY` | SSH 私钥(`cat ~/.ssh/id_ed25519`) | ✅ | +| `CLOUDFLARE_API_TOKEN` | Cloudflare API Token | ✅ 前端部署 | +| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare Account ID | ✅ 前端部署 | +| `SLACK_BOT_TOKEN` | Slack Bot Token | 可选 | +| `SLACK_CHANNEL_ID` | Slack Channel ID | 可选 | + +配好后,每次 push 到 main 自动部署。 + +--- + +## 扩展:添加从节点 + +当主节点扛不住时,可以加从节点分担读请求。New API 原生支持主从模式。 + +### 架构变化 + +``` + Nginx (主节点) + │ + ┌───────┼───────┐ + ▼ ▼ ▼ + 主节点 从节点1 从节点2 + :3002 :3002 :3002 + │ │ │ + └───────┼───────┘ + ▼ + MySQL + Redis + (主节点上) +``` + +### 操作步骤 + +#### 1. 准备从节点 .env + +从主节点复制 `.env`,修改以下内容: + +```bash +# 指向主节点的 MySQL 和 Redis +SQL_DSN=new_api:<密码>@tcp(<主节点IP>:3306)/new_api +REDIS_CONN_STRING=redis://default:<密码>@<主节点IP>:6379 + +# SESSION_SECRET 和 CRYPTO_SECRET 必须和主节点完全一致(直接复制) + +# 设为 slave +NODE_TYPE=slave +``` + +上传到从节点: + +```bash +ssh new-api-slave1 'mkdir -p /opt/new-api' +scp .env new-api-slave1:/opt/new-api/.env +ssh new-api-slave1 'chmod 600 /opt/new-api/.env' +``` + +#### 2. 部署从节点 + +```bash +make deploy DEPLOY_HOST=new-api-slave1 +``` + +#### 3. 主节点数据库授权 + +在主节点 MySQL 中为从节点 IP 授权: + +```sql +CREATE USER IF NOT EXISTS 'new_api'@'<从节点IP>' IDENTIFIED BY '<密码>'; +GRANT ALL PRIVILEGES ON new_api.* TO 'new_api'@'<从节点IP>'; +FLUSH PRIVILEGES; +``` + +同时确保主节点的 Redis 允许从节点连接(`bind 0.0.0.0` + `requirepass`)。 + +#### 4. 主节点 Nginx 添加从节点 + +编辑主节点 `/etc/nginx/sites-available/new-api`,把 `proxy_pass` 改为 upstream 模式: + +```nginx +# 在 server 块外面添加 +upstream new_api_backend { + server 127.0.0.1:3002; # 主节点 + server <从节点IP>:3002; # 从节点1 +} + +# server 块里改为 +location / { + proxy_pass http://new_api_backend; + # ... 其他 proxy_set_header 保持不变 +} +``` + +重载: + +```bash +sudo nginx -t && sudo systemctl reload nginx +``` + +#### 5. 从节点 CI 自动部署 + +复制 `deploy-backend.yml`,新建 `deploy-backend-slave1.yml`,使用不同的 Secrets(`DEPLOY_HOST_SLAVE1` 等),去掉域名和 SSL 参数: + +```yaml +script: | + cd /tmp/new-api-deploy + chmod +x deploy/deploy.sh + sudo ./deploy/deploy.sh "/tmp/new-api-deploy" +``` + +--- + +## 运维命令 + +```bash +# 查看服务状态 +make status + +# 实时日志 +make logs + +# 重启服务 +make restart + +# 手动部署 +make deploy + +# 同步上游 new-api 代码 +make sync-upstream +``` + +--- + +## 环境变量说明 + +配置文件:`/opt/new-api/.env`,参考 `deploy/.env.example`。 + +| 变量 | 说明 | 示例 | +|------|------|------| +| `SQL_DSN` | MySQL 连接字符串 | `new_api:pwd@tcp(127.0.0.1:3306)/new_api` | +| `REDIS_CONN_STRING` | Redis 连接字符串 | `redis://default:pwd@127.0.0.1:6379` | +| `SESSION_SECRET` | 会话密钥,**所有节点必须一致** | `openssl rand -hex 32` | +| `CRYPTO_SECRET` | 加密密钥,**所有节点必须一致** | `openssl rand -hex 32` | +| `NODE_TYPE` | 主节点留空,从节点设 `slave` | `slave` | +| `SYNC_FREQUENCY` | 主从同步间隔(秒) | `60` | +| `BATCH_UPDATE_ENABLED` | 批量更新开关 | `true` | +| `BATCH_UPDATE_INTERVAL` | 批量更新间隔(秒) | `5` | +| `TZ` | 时区 | `Asia/Shanghai` | + +--- + +## 故障排查 + +### 服务启动失败 + +```bash +sudo journalctl -u new-api -n 50 --no-pager + +# 常见原因: +# 1. .env 没上传或配置错误 +# 2. MySQL/Redis 没启动或连接不上 +# 3. 端口被占用:sudo ss -tlnp | grep :3002 +``` + +### 部署失败自动回滚 + +deploy.sh 部署时会备份旧二进制为 `new-api.backup`,新版本启动失败时自动回滚。 + +手动回滚: + +```bash +cd /opt/new-api +sudo systemctl stop new-api +sudo cp new-api.backup new-api +sudo systemctl start new-api +``` + +### SSL 证书 + +```bash +sudo certbot certificates # 查看状态 +sudo certbot renew # 手动续期(一般自动) +``` + +### Nginx + +```bash +sudo nginx -t # 测试配置 +sudo systemctl reload nginx # 重载 +# 配置损坏时恢复备份: +ls /etc/nginx/sites-available/new-api.backup.* +``` + +--- + +## 文件说明 + +| 文件 | 用途 | +|------|------| +| `deploy/.env.example` | 环境变量模板 | +| `deploy/deploy.sh` | 主部署脚本(自动判断首次/更新,失败自动回滚) | +| `deploy/deploy-all.sh` | 首次部署调用(安装 Nginx、申请 SSL 证书) | +| `deploy/new-api.service` | systemd 服务配置 | +| `deploy/nginx-new-api.conf` | Nginx 反向代理模板 | +| `.github/workflows/deploy-backend.yml` | 后端 CI/CD | +| `.github/workflows/deploy-web.yml` | 前端 CI/CD(Cloudflare Pages) | diff --git a/deploy/deploy-all.sh b/deploy/deploy-all.sh new file mode 100755 index 000000000000..a36c1dbd950b --- /dev/null +++ b/deploy/deploy-all.sh @@ -0,0 +1,176 @@ +#!/bin/bash +# New API 完整部署脚本(首次部署:Nginx + SSL) +# Usage: sudo ./deploy-all.sh [domain] [backend_port] [backend_host] [ssl_email] + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +DOMAIN="${1:-}" +BACKEND_PORT="${2:-3000}" +BACKEND_HOST="${3:-127.0.0.1}" +SERVICE_NAME="new-api" +NGINX_CONF_SOURCE="deploy/nginx-new-api.conf" +NGINX_CONF_TARGET="/etc/nginx/sites-available/new-api" +NGINX_ENABLED_SITES="/etc/nginx/sites-enabled" +EMAIL="${4:-admin@${DOMAIN}}" + +echo -e "${BLUE}=== New API Nginx 反向代理部署 ===${NC}" +echo "" +echo "配置参数:" +echo " 域名: ${DOMAIN:-未配置}" +echo " 后端地址: ${BACKEND_HOST}:${BACKEND_PORT}" +echo " 邮箱: ${EMAIL}" +echo "" + +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}Error: 请使用 root 权限运行 (use sudo)${NC}" + exit 1 +fi + +# 如果没有提供域名,只启动 Go 服务 +if [ -z "$DOMAIN" ]; then + echo -e "${YELLOW}警告: 未配置域名,跳过 Nginx 和 SSL 配置${NC}" + systemctl restart $SERVICE_NAME || true + systemctl enable $SERVICE_NAME + echo -e "${GREEN}✅ 服务已启动${NC}" + exit 0 +fi + +echo -e "${BLUE}1. 检查并安装依赖...${NC}" + +if ! command -v nginx &>/dev/null; then + echo -e "${YELLOW}安装 Nginx...${NC}" + apt-get update -qq + apt-get install -y nginx +fi + +if ! command -v certbot &>/dev/null; then + echo -e "${YELLOW}安装 Certbot (Let's Encrypt)...${NC}" + apt-get install -y certbot python3-certbot-nginx +fi + +echo -e "${BLUE}2. 配置 Nginx...${NC}" + +BACKUP_FILE="" +if [ -f "$NGINX_CONF_TARGET" ]; then + BACKUP_FILE="${NGINX_CONF_TARGET}.backup.$(date +%Y%m%d%H%M%S)" + cp "$NGINX_CONF_TARGET" "$BACKUP_FILE" + echo -e "${YELLOW}已备份现有配置: $BACKUP_FILE${NC}" +fi + +SSL_EXISTS=false +if [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ]; then + SSL_EXISTS=true +fi + +if [ "$SSL_EXISTS" = true ]; then + sed "s/__DOMAIN__/$DOMAIN/g; s/__BACKEND_PORT__/$BACKEND_PORT/g; s/__BACKEND_HOST__/$BACKEND_HOST/g; s/server_name _;/server_name $DOMAIN;/g" \ + "$NGINX_CONF_SOURCE" > "$NGINX_CONF_TARGET" + echo -e "${GREEN}✅ Nginx 配置已更新(含 SSL)${NC}" +else + cat > "$NGINX_CONF_TARGET" << EOF +server { + listen 80; + server_name $DOMAIN; + + location / { + proxy_pass http://$BACKEND_HOST:$BACKEND_PORT; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } +} +EOF + echo -e "${GREEN}✅ Nginx 临时配置已创建(HTTP-only)${NC}" +fi + +echo -e "${BLUE}3. 配置 SSL 证书...${NC}" + +if [ "$SSL_EXISTS" = true ]; then + echo -e "${GREEN}✅ SSL 证书已存在,跳过申请(certbot 会自动续期)${NC}" +else + ln -sf "$NGINX_CONF_TARGET" "$NGINX_ENABLED_SITES/new-api" + rm -f "$NGINX_ENABLED_SITES/default" + nginx -t && systemctl reload nginx + + echo -e "${YELLOW}为 $DOMAIN 申请 SSL 证书...${NC}" + + if certbot --nginx -d "$DOMAIN" --email "$EMAIL" --agree-tos --non-interactive --redirect; then + echo -e "${GREEN}✅ SSL 证书申请成功${NC}" + else + echo -e "${RED}❌ SSL 证书申请失败${NC}" + echo -e "${YELLOW}提示: 请确保域名已正确解析到此服务器${NC}" + exit 1 + fi +fi + +echo -e "${BLUE}4. 启用 Nginx 站点...${NC}" + +ln -sf "$NGINX_CONF_TARGET" "$NGINX_ENABLED_SITES/new-api" +rm -f "$NGINX_ENABLED_SITES/default" + +echo -e "${BLUE}5. 测试 Nginx 配置...${NC}" + +if ! nginx -t; then + echo -e "${RED}❌ Nginx 配置测试失败${NC}" + if [ -n "$BACKUP_FILE" ] && [ -f "$BACKUP_FILE" ]; then + echo -e "${YELLOW}恢复备份配置...${NC}" + cp "$BACKUP_FILE" "$NGINX_CONF_TARGET" + fi + exit 1 +fi + +echo -e "${GREEN}✅ Nginx 配置测试通过${NC}" + +echo -e "${BLUE}6. 启动服务...${NC}" + +if systemctl is-active --quiet $SERVICE_NAME; then + echo -e "${GREEN}✅ 服务正在运行${NC}" +else + systemctl start $SERVICE_NAME + systemctl enable $SERVICE_NAME +fi + +systemctl reload nginx +if systemctl is-active --quiet nginx; then + echo -e "${GREEN}✅ Nginx 正在运行${NC}" +else + systemctl start nginx + systemctl enable nginx + echo -e "${GREEN}✅ Nginx 已启动${NC}" +fi + +echo -e "${BLUE}7. 验证部署...${NC}" + +sleep 2 +if ss -tlnp 2>/dev/null | grep -q ":$BACKEND_PORT"; then + echo -e "${GREEN}✅ 后端服务监听端口 $BACKEND_PORT${NC}" +else + echo -e "${RED}❌ 后端服务未监听端口 $BACKEND_PORT${NC}" + echo "请检查日志: journalctl -u $SERVICE_NAME -n 20" +fi + +echo -e "${YELLOW}测试 HTTPS 访问...${NC}" +if curl -sSf "https://$DOMAIN" >/dev/null 2>&1 || curl -sSf "https://$DOMAIN/api/status" >/dev/null 2>&1; then + echo -e "${GREEN}✅ HTTPS 访问正常${NC}" +else + echo -e "${YELLOW}⚠️ HTTPS 测试失败(可能需要等待 DNS 生效)${NC}" +fi + +echo "" +echo -e "${GREEN}=== 部署完成! ===${NC}" +echo "" +echo -e "${BLUE}访问地址:${NC}" +echo -e " 后端 API: ${GREEN}https://$DOMAIN${NC}" +echo "" +echo -e "${BLUE}常用命令:${NC}" +echo -e " 查看日志: ${YELLOW}journalctl -u $SERVICE_NAME -f${NC}" +echo -e " 重启服务: ${YELLOW}systemctl restart $SERVICE_NAME${NC}" +echo -e " 续期证书: ${YELLOW}certbot renew${NC}" +echo "" diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100755 index 000000000000..30239ff78526 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,190 @@ +#!/bin/bash +# New API 部署脚本 +# Usage: sudo ./deploy.sh [domain] [backend_port] [backend_host] [ssl_email] +# Example: sudo ./deploy.sh /tmp/new-api-deploy api.4aicode.com 3000 127.0.0.1 ceo@richcalls.xyz + +set -e + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# 参数 +DEPLOY_DIR="${1:?请提供部署目录路径}" +DOMAIN="${2:-}" +BACKEND_PORT="${3:-3000}" +BACKEND_HOST="${4:-127.0.0.1}" +SSL_EMAIL="${5:-}" + +# 常量 +SERVICE_NAME="new-api" +INSTALL_DIR="/opt/new-api" +SERVICE_FILE="/etc/systemd/system/new-api.service" +NGINX_CONF="/etc/nginx/sites-available/new-api" +NGINX_CONF_SOURCE="deploy/nginx-new-api.conf" + +echo -e "${BLUE}=== New API 部署脚本 ===${NC}" +echo "" + +# 检查是否为 root +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}Error: 请使用 root 权限运行 (use sudo)${NC}" + exit 1 +fi + +# 检查部署目录 +if [ ! -d "$DEPLOY_DIR" ]; then + echo -e "${RED}Error: 部署目录不存在: $DEPLOY_DIR${NC}" + exit 1 +fi + +cd "$DEPLOY_DIR" + +# 检查二进制文件 +if [ ! -f "new-api" ]; then + echo -e "${RED}Error: 找不到二进制文件: new-api${NC}" + exit 1 +fi + +echo -e "${BLUE}1. 准备安装目录...${NC}" + +mkdir -p "$INSTALL_DIR"/{data,logs} + +# 创建服务用户(首次部署需要) +if ! id new-api &>/dev/null; then + useradd --system --no-create-home --shell /usr/sbin/nologin new-api +fi + +echo -e "${BLUE}2. 停止现有服务...${NC}" + +systemctl stop $SERVICE_NAME 2>/dev/null || true + +echo -e "${BLUE}3. 安装二进制文件...${NC}" + +# 备份旧二进制 +if [ -f "$INSTALL_DIR/new-api" ]; then + mv "$INSTALL_DIR/new-api" "$INSTALL_DIR/new-api.backup" + echo -e "${YELLOW}已备份旧版本${NC}" +fi + +# 安装新二进制 +cp new-api "$INSTALL_DIR/new-api" +chmod +x "$INSTALL_DIR/new-api" +chown new-api:new-api "$INSTALL_DIR/new-api" + +echo -e "${GREEN}✅ 二进制文件已安装${NC}" + +# 检查 .env 是否存在 +if [ ! -f "$INSTALL_DIR/.env" ]; then + echo -e "${RED}❌ 未找到 $INSTALL_DIR/.env,请先上传 .env 文件${NC}" + echo -e "${YELLOW}参考 deploy/.env.example 创建${NC}" + exit 1 +fi + +echo -e "${BLUE}4. 安装 systemd 服务...${NC}" + +cp deploy/new-api.service "$SERVICE_FILE" +systemctl daemon-reload +echo -e "${GREEN}✅ 服务文件已安装${NC}" + +echo -e "${BLUE}5. 检查部署模式...${NC}" + +# 判断是否首次部署(Nginx 配置不存在) +if [ ! -f "$NGINX_CONF" ] && [ -n "$DOMAIN" ]; then + echo -e "${YELLOW}检测到首次部署,执行完整配置(Nginx + SSL)...${NC}" + + chmod +x deploy/deploy-all.sh + ./deploy/deploy-all.sh "$DOMAIN" "$BACKEND_PORT" "$BACKEND_HOST" "$SSL_EMAIL" +else + echo -e "${YELLOW}更新部署,只重启服务...${NC}" + + # 启动服务 + systemctl start $SERVICE_NAME + systemctl enable $SERVICE_NAME + + # 更新 Nginx 配置(自动同步模板变更) + if [ -n "$DOMAIN" ]; then + if [ -f "$NGINX_CONF" ]; then + BACKUP_FILE="${NGINX_CONF}.backup.$(date +%Y%m%d%H%M%S)" + cp "$NGINX_CONF" "$BACKUP_FILE" + fi + + if [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ]; then + sed "s/__DOMAIN__/$DOMAIN/g; s/__BACKEND_PORT__/$BACKEND_PORT/g; s/__BACKEND_HOST__/$BACKEND_HOST/g; s/server_name _;/server_name $DOMAIN;/g" \ + "$NGINX_CONF_SOURCE" > "$NGINX_CONF" + else + cat > "$NGINX_CONF" << EOF +server { + listen 80; + server_name $DOMAIN; + + location / { + proxy_pass http://$BACKEND_HOST:$BACKEND_PORT; + proxy_set_header Host \$host; + proxy_set_header X-Real-IP \$remote_addr; + proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto \$scheme; + } +} +EOF + fi + else + if [ -f "$NGINX_CONF" ]; then + sed -i "s|proxy_pass http://[^;]*;|proxy_pass http://${BACKEND_HOST}:${BACKEND_PORT};|g" "$NGINX_CONF" + fi + fi + + # 重载 Nginx + if command -v nginx &>/dev/null && systemctl is-active --quiet nginx; then + if nginx -t; then + systemctl reload nginx + else + echo -e "${RED}❌ Nginx 配置测试失败${NC}" + exit 1 + fi + fi + + echo -e "${GREEN}✅ 服务已启动${NC}" +fi + +echo -e "${BLUE}6. 验证部署...${NC}" + +sleep 3 + +# 检查服务状态 +if systemctl is-active --quiet $SERVICE_NAME; then + echo -e "${GREEN}✅ 服务运行正常${NC}" +else + echo -e "${RED}❌ 服务启动失败${NC}" + echo -e "${YELLOW}尝试回滚...${NC}" + + if [ -f "$INSTALL_DIR/new-api.backup" ]; then + cp "$INSTALL_DIR/new-api.backup" "$INSTALL_DIR/new-api" + chown new-api:new-api "$INSTALL_DIR/new-api" + systemctl start $SERVICE_NAME + echo -e "${YELLOW}已回滚到旧版本${NC}" + fi + + echo "查看日志: journalctl -u $SERVICE_NAME -n 50" + exit 1 +fi + +# 检查端口监听 +if ss -tlnp 2>/dev/null | grep -q ":$BACKEND_PORT"; then + echo -e "${GREEN}✅ 端口 $BACKEND_PORT 监听正常${NC}" +else + echo -e "${YELLOW}⚠️ 端口 $BACKEND_PORT 未检测到监听${NC}" +fi + +echo -e "${BLUE}7. 清理部署文件...${NC}" + +cd / +rm -rf "$DEPLOY_DIR" +echo -e "${GREEN}✅ 清理完成${NC}" + +echo "" +echo -e "${GREEN}=== 部署成功! ===${NC}" +echo "" diff --git a/deploy/new-api.service b/deploy/new-api.service new file mode 100644 index 000000000000..1c330009c09d --- /dev/null +++ b/deploy/new-api.service @@ -0,0 +1,24 @@ +[Unit] +Description=New API Backend Service +After=network.target + +[Service] +Type=simple +User=new-api +Group=new-api +WorkingDirectory=/opt/new-api +ExecStart=/opt/new-api/new-api +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal + +# 环境变量文件 +EnvironmentFile=-/opt/new-api/.env + +# 安全选项 +NoNewPrivileges=yes +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/deploy/nginx-new-api.conf b/deploy/nginx-new-api.conf new file mode 100644 index 000000000000..1b6a686d78f3 --- /dev/null +++ b/deploy/nginx-new-api.conf @@ -0,0 +1,49 @@ +server { + listen 80; + server_name _; + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name _; + + # SSL 证书(Let's Encrypt) + ssl_certificate /etc/letsencrypt/live/__DOMAIN__/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/__DOMAIN__/privkey.pem; + + # SSL 安全配置 + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # 日志 + access_log /var/log/nginx/new-api_access.log; + error_log /var/log/nginx/new-api_error.log; + + client_max_body_size 64m; + + # 反向代理到后端 + location / { + proxy_pass http://__BACKEND_HOST__:__BACKEND_PORT__; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + + # WebSocket 支持 + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # 超时配置 + proxy_connect_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } +} diff --git a/makefile b/makefile index cbc4ea6ae22d..ce0818b00fc5 100644 --- a/makefile +++ b/makefile @@ -1,14 +1,30 @@ -FRONTEND_DIR = ./web -BACKEND_DIR = . +DEPLOY_HOST ?= hetzner -.PHONY: all build-frontend start-backend +.PHONY: build deploy logs status restart sync-upstream -all: build-frontend start-backend +# === 本地编译 === +build: + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$$(cat VERSION 2>/dev/null || echo dev)'" \ + -o bin/new-api -build-frontend: - @echo "Building frontend..." - @cd $(FRONTEND_DIR) && bun install && DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build +# === 手动部署(CI 自动部署时不需要) === +deploy: build + ssh $(DEPLOY_HOST) 'mkdir -p /tmp/new-api-deploy' + scp bin/new-api $(DEPLOY_HOST):/tmp/new-api-deploy/new-api + rsync -avz deploy/ $(DEPLOY_HOST):/tmp/new-api-deploy/deploy/ + ssh $(DEPLOY_HOST) 'cd /tmp/new-api-deploy && chmod +x deploy/deploy.sh && sudo ./deploy/deploy.sh /tmp/new-api-deploy' -start-backend: - @echo "Starting backend dev server..." - @cd $(BACKEND_DIR) && go run main.go & +# === 运维 === +logs: + ssh $(DEPLOY_HOST) 'sudo journalctl -u new-api -f' + +status: + ssh $(DEPLOY_HOST) 'sudo systemctl status new-api' + +restart: + ssh $(DEPLOY_HOST) 'sudo systemctl restart new-api' + +# === 同步上游代码 === +sync-upstream: + git fetch upstream && git merge upstream/main && git push origin main From 9198c641f98a137900e7a470454c3163e2c60e5c Mon Sep 17 00:00:00 2001 From: forecho Date: Tue, 14 Apr 2026 16:47:01 +0800 Subject: [PATCH 02/10] fix: address code review feedback and CI Slack notification failure - Use `|` as sed delimiter to avoid issues with paths containing `/` - Use timestamped backups instead of overwriting single backup file - Use precise regex for port matching to avoid false positives - Add continue-on-error to Slack notification steps (secrets not yet configured) - Parameterize GOARCH in Makefile for ARM support Co-Authored-By: Claude Opus 4.6 --- .github/workflows/deploy-backend.yml | 2 ++ .github/workflows/deploy-web.yml | 2 ++ deploy/deploy-all.sh | 4 ++-- deploy/deploy.sh | 11 ++++++----- makefile | 2 +- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index df2cee3002bd..ad8689ca2f48 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -41,6 +41,7 @@ jobs: - name: Slack 通知 if: always() + continue-on-error: true uses: forecho/slack-deploy-action@v1 with: slack_bot_token: ${{ secrets.SLACK_BOT_TOKEN }} @@ -91,6 +92,7 @@ jobs: - name: Slack 通知 if: always() + continue-on-error: true uses: forecho/slack-deploy-action@v1 with: slack_bot_token: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index c41cfa2c0c2e..0873125f50ed 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -53,6 +53,7 @@ jobs: - name: Slack 通知 if: always() + continue-on-error: true uses: forecho/slack-deploy-action@v1 with: slack_bot_token: ${{ secrets.SLACK_BOT_TOKEN }} @@ -94,6 +95,7 @@ jobs: - name: Slack 通知 if: always() + continue-on-error: true uses: forecho/slack-deploy-action@v1 with: slack_bot_token: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/deploy/deploy-all.sh b/deploy/deploy-all.sh index a36c1dbd950b..b16f193af83b 100755 --- a/deploy/deploy-all.sh +++ b/deploy/deploy-all.sh @@ -69,7 +69,7 @@ if [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ]; then fi if [ "$SSL_EXISTS" = true ]; then - sed "s/__DOMAIN__/$DOMAIN/g; s/__BACKEND_PORT__/$BACKEND_PORT/g; s/__BACKEND_HOST__/$BACKEND_HOST/g; s/server_name _;/server_name $DOMAIN;/g" \ + sed "s|__DOMAIN__|$DOMAIN|g; s|__BACKEND_PORT__|$BACKEND_PORT|g; s|__BACKEND_HOST__|$BACKEND_HOST|g; s|server_name _;|server_name $DOMAIN;|g" \ "$NGINX_CONF_SOURCE" > "$NGINX_CONF_TARGET" echo -e "${GREEN}✅ Nginx 配置已更新(含 SSL)${NC}" else @@ -149,7 +149,7 @@ fi echo -e "${BLUE}7. 验证部署...${NC}" sleep 2 -if ss -tlnp 2>/dev/null | grep -q ":$BACKEND_PORT"; then +if ss -tlnp 2>/dev/null | grep -qE ":$BACKEND_PORT([[:space:]]|$)"; then echo -e "${GREEN}✅ 后端服务监听端口 $BACKEND_PORT${NC}" else echo -e "${RED}❌ 后端服务未监听端口 $BACKEND_PORT${NC}" diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 30239ff78526..3bb1ddce03a1 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -66,7 +66,7 @@ echo -e "${BLUE}3. 安装二进制文件...${NC}" # 备份旧二进制 if [ -f "$INSTALL_DIR/new-api" ]; then - mv "$INSTALL_DIR/new-api" "$INSTALL_DIR/new-api.backup" + mv "$INSTALL_DIR/new-api" "$INSTALL_DIR/new-api.backup.$(date +%Y%m%d%H%M%S)" echo -e "${YELLOW}已备份旧版本${NC}" fi @@ -113,7 +113,7 @@ else fi if [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ]; then - sed "s/__DOMAIN__/$DOMAIN/g; s/__BACKEND_PORT__/$BACKEND_PORT/g; s/__BACKEND_HOST__/$BACKEND_HOST/g; s/server_name _;/server_name $DOMAIN;/g" \ + sed "s|__DOMAIN__|$DOMAIN|g; s|__BACKEND_PORT__|$BACKEND_PORT|g; s|__BACKEND_HOST__|$BACKEND_HOST|g; s|server_name _;|server_name $DOMAIN;|g" \ "$NGINX_CONF_SOURCE" > "$NGINX_CONF" else cat > "$NGINX_CONF" << EOF @@ -161,8 +161,9 @@ else echo -e "${RED}❌ 服务启动失败${NC}" echo -e "${YELLOW}尝试回滚...${NC}" - if [ -f "$INSTALL_DIR/new-api.backup" ]; then - cp "$INSTALL_DIR/new-api.backup" "$INSTALL_DIR/new-api" + LATEST_BACKUP=$(ls -t "$INSTALL_DIR"/new-api.backup.* 2>/dev/null | head -1) + if [ -n "$LATEST_BACKUP" ]; then + cp "$LATEST_BACKUP" "$INSTALL_DIR/new-api" chown new-api:new-api "$INSTALL_DIR/new-api" systemctl start $SERVICE_NAME echo -e "${YELLOW}已回滚到旧版本${NC}" @@ -173,7 +174,7 @@ else fi # 检查端口监听 -if ss -tlnp 2>/dev/null | grep -q ":$BACKEND_PORT"; then +if ss -tlnp 2>/dev/null | grep -qE ":$BACKEND_PORT([[:space:]]|$)"; then echo -e "${GREEN}✅ 端口 $BACKEND_PORT 监听正常${NC}" else echo -e "${YELLOW}⚠️ 端口 $BACKEND_PORT 未检测到监听${NC}" diff --git a/makefile b/makefile index ce0818b00fc5..1480dafecc50 100644 --- a/makefile +++ b/makefile @@ -4,7 +4,7 @@ DEPLOY_HOST ?= hetzner # === 本地编译 === build: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + CGO_ENABLED=0 GOOS=linux GOARCH=$${GOARCH:-amd64} go build \ -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$$(cat VERSION 2>/dev/null || echo dev)'" \ -o bin/new-api From 77a279de3628013bec8df05a655851bca15fe12c Mon Sep 17 00:00:00 2001 From: forecho Date: Tue, 14 Apr 2026 17:21:11 +0800 Subject: [PATCH 03/10] fix: use bun instead of npm for frontend CI/CD The web directory uses bun (bun.lock exists, no package-lock.json). Switched from actions/setup-node + npm ci to oven-sh/setup-bun + bun install. Also removed non-existent type-check step. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/deploy-web.yml | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 0873125f50ed..d1ebdde31576 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -25,25 +25,16 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - cache-dependency-path: web/package-lock.json + - name: Setup Bun + uses: oven-sh/setup-bun@v2 - name: 安装依赖 working-directory: ./web - run: npm ci - - - name: 类型检查 - working-directory: ./web - run: npm run type-check 2>/dev/null || true - continue-on-error: true + run: bun install --frozen-lockfile - name: 构建 working-directory: ./web - run: npm run build + run: bun run build - name: 保存构建产物 uses: actions/upload-artifact@v4 @@ -69,20 +60,16 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - cache-dependency-path: web/package-lock.json + - name: Setup Bun + uses: oven-sh/setup-bun@v2 - name: 安装依赖 working-directory: ./web - run: npm ci + run: bun install --frozen-lockfile - name: 构建 working-directory: ./web - run: npm run build + run: bun run build - name: 部署到 Cloudflare Pages uses: cloudflare/pages-action@v1 From 879006f43f529bbe87745ba4a7bf51a2591d3c58 Mon Sep 17 00:00:00 2001 From: forecho Date: Tue, 14 Apr 2026 17:44:05 +0800 Subject: [PATCH 04/10] fix: build frontend before Go compile (go:embed requires web/dist) main.go uses `//go:embed web/dist` which requires the frontend build output to exist. Added bun install + build step before Go test and build. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/deploy-backend.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index ad8689ca2f48..265aab02a123 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -23,6 +23,7 @@ env: BACKEND_PORT: '3002' BACKEND_HOST: '127.0.0.1' SSL_EMAIL: 'ceo@richcalls.xyz' + VITE_REACT_APP_SERVER: 'https://api.4aicode.com' jobs: test: @@ -30,6 +31,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: 构建前端(Go embed 依赖) + working-directory: ./web + run: bun install --frozen-lockfile && bun run build + - name: Set up Go uses: actions/setup-go@v5 with: @@ -57,6 +65,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: 构建前端(Go embed 依赖) + working-directory: ./web + run: bun install --frozen-lockfile && bun run build + - name: Set up Go uses: actions/setup-go@v5 with: From f1b2a11843bbf353f62030a669a47289883bd8cb Mon Sep 17 00:00:00 2001 From: forecho Date: Tue, 14 Apr 2026 17:59:52 +0800 Subject: [PATCH 05/10] fix: ensure correct file ownership for new-api service user - chown data/logs directories to new-api user after mkdir - chown and chmod .env file so the service can read it Fixes "failed to open log file" crash on startup. Co-Authored-By: Claude Opus 4.6 --- deploy/deploy.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 3bb1ddce03a1..f8a6e1f2bfbc 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -58,6 +58,9 @@ if ! id new-api &>/dev/null; then useradd --system --no-create-home --shell /usr/sbin/nologin new-api fi +# 确保目录权限正确(服务以 new-api 用户运行) +chown -R new-api:new-api "$INSTALL_DIR"/{data,logs} + echo -e "${BLUE}2. 停止现有服务...${NC}" systemctl stop $SERVICE_NAME 2>/dev/null || true @@ -77,12 +80,14 @@ chown new-api:new-api "$INSTALL_DIR/new-api" echo -e "${GREEN}✅ 二进制文件已安装${NC}" -# 检查 .env 是否存在 +# 检查 .env 是否存在,并确保权限正确 if [ ! -f "$INSTALL_DIR/.env" ]; then echo -e "${RED}❌ 未找到 $INSTALL_DIR/.env,请先上传 .env 文件${NC}" echo -e "${YELLOW}参考 deploy/.env.example 创建${NC}" exit 1 fi +chown new-api:new-api "$INSTALL_DIR/.env" +chmod 600 "$INSTALL_DIR/.env" echo -e "${BLUE}4. 安装 systemd 服务...${NC}" From a1dbad7cb10825e6dce6b04680ada339f02b094f Mon Sep 17 00:00:00 2001 From: forecho Date: Tue, 14 Apr 2026 18:07:38 +0800 Subject: [PATCH 06/10] docs: add PORT config to .env.example Default port 3000 conflicts with existing services on the server. Co-Authored-By: Claude Opus 4.6 --- deploy/.env.example | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deploy/.env.example b/deploy/.env.example index dceea1e9fd4d..65abf7983594 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -3,6 +3,10 @@ # scp .env hetzner:/opt/new-api/.env # ssh hetzner 'chmod 600 /opt/new-api/.env' +# ====== 服务端口 ====== +# 默认 3000,如果端口被占用需要修改 +PORT=3002 + # ====== 数据库 ====== # 格式: 用户名:密码@tcp(地址:端口)/数据库名 SQL_DSN=new_api:password@tcp(127.0.0.1:3306)/new_api From 44d97fd0f5b483a38a469bb5182457e6b05450eb Mon Sep 17 00:00:00 2001 From: forecho Date: Tue, 14 Apr 2026 18:15:16 +0800 Subject: [PATCH 07/10] fix: correct env var name VITE_REACT_APP_SERVER_URL Code uses VITE_REACT_APP_SERVER_URL but CI had VITE_REACT_APP_SERVER (missing _URL suffix), causing frontend to use relative paths instead of the API domain. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/deploy-backend.yml | 2 +- .github/workflows/deploy-web.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index 265aab02a123..96f984fd867a 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -23,7 +23,7 @@ env: BACKEND_PORT: '3002' BACKEND_HOST: '127.0.0.1' SSL_EMAIL: 'ceo@richcalls.xyz' - VITE_REACT_APP_SERVER: 'https://api.4aicode.com' + VITE_REACT_APP_SERVER_URL: 'https://api.4aicode.com' jobs: test: diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index d1ebdde31576..c292dc9745e5 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -17,7 +17,7 @@ permissions: pull-requests: write env: - VITE_REACT_APP_SERVER: 'https://api.4aicode.com' + VITE_REACT_APP_SERVER_URL: 'https://api.4aicode.com' jobs: build: From 185aaf71fb79e7284cc36c87b09d4a39ce99e3df Mon Sep 17 00:00:00 2001 From: forecho Date: Tue, 14 Apr 2026 18:20:09 +0800 Subject: [PATCH 08/10] fix: add CORS headers for Cloudflare Pages frontend Frontend at 4aicode.com needs cross-origin access to api.4aicode.com. Allow origins matching 4aicode.com and *.4aicode.pages.dev (preview). Co-Authored-By: Claude Opus 4.6 --- deploy/nginx-new-api.conf | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/deploy/nginx-new-api.conf b/deploy/nginx-new-api.conf index 1b6a686d78f3..6c707acb86b7 100644 --- a/deploy/nginx-new-api.conf +++ b/deploy/nginx-new-api.conf @@ -26,6 +26,21 @@ server { client_max_body_size 64m; + # CORS 配置(前端在 Cloudflare Pages,跨域访问后端 API) + set $cors_origin ""; + if ($http_origin ~* "^https://(4aicode\.com|.*\.4aicode\.pages\.dev)$") { + set $cors_origin $http_origin; + } + add_header Access-Control-Allow-Origin $cors_origin always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always; + add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" always; + add_header Access-Control-Allow-Credentials "true" always; + + # 预检请求直接返回 + if ($request_method = OPTIONS) { + return 204; + } + # 反向代理到后端 location / { proxy_pass http://__BACKEND_HOST__:__BACKEND_PORT__; From 1a1de1379679dda8777a9529f6c99e9a5722a65d Mon Sep 17 00:00:00 2001 From: forecho Date: Tue, 14 Apr 2026 18:26:00 +0800 Subject: [PATCH 09/10] fix: allow all request headers in CORS config Frontend sends custom header `new-api-user` which was not whitelisted. Use wildcard to avoid future issues with other custom headers. Co-Authored-By: Claude Opus 4.6 --- deploy/nginx-new-api.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/nginx-new-api.conf b/deploy/nginx-new-api.conf index 6c707acb86b7..b309ec109bfc 100644 --- a/deploy/nginx-new-api.conf +++ b/deploy/nginx-new-api.conf @@ -33,7 +33,7 @@ server { } add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always; - add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" always; + add_header Access-Control-Allow-Headers "*" always; add_header Access-Control-Allow-Credentials "true" always; # 预检请求直接返回 From b25ab490d320d9025d09385191584acb48d79d56 Mon Sep 17 00:00:00 2001 From: forecho Date: Thu, 16 Apr 2026 16:39:16 +0800 Subject: [PATCH 10/10] fix: make cookie SameSite configurable for cross-origin deployments When frontend (e.g. Cloudflare Pages) and backend are on different domains, SameSite=Strict causes browsers to not send session cookies on cross-origin requests, resulting in 401 after login. Add COOKIE_SAME_SITE and COOKIE_SECURE env vars to control cookie policy. Setting COOKIE_SAME_SITE=none automatically enables Secure. Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 4 ++++ main.go | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index b152c2a10012..452182910d56 100644 --- a/.env.example +++ b/.env.example @@ -68,6 +68,10 @@ # 会话密钥 # SESSION_SECRET=random_string +# Cookie 设置(前后端跨域部署时需要配置) +# COOKIE_SAME_SITE=none # 可选值: strict(默认), lax, none(跨域部署必须设为 none) +# COOKIE_SECURE=true # SameSite=none 时会自动开启,HTTPS 环境建议开启 + # 其他配置 # 生成默认token # GENERATE_DEFAULT_TOKEN=false diff --git a/main.go b/main.go index dbbf44a1826b..f2365d64f019 100644 --- a/main.go +++ b/main.go @@ -170,12 +170,23 @@ func main() { middleware.SetUpLogger(server) // Initialize session store store := cookie.NewStore([]byte(common.SessionSecret)) + sameSite := http.SameSiteStrictMode + secure := false + if os.Getenv("COOKIE_SAME_SITE") == "none" { + sameSite = http.SameSiteNoneMode + secure = true // SameSite=None requires Secure=true + } else if os.Getenv("COOKIE_SAME_SITE") == "lax" { + sameSite = http.SameSiteLaxMode + } + if os.Getenv("COOKIE_SECURE") == "true" { + secure = true + } store.Options(sessions.Options{ Path: "/", MaxAge: 2592000, // 30 days HttpOnly: true, - Secure: false, - SameSite: http.SameSiteStrictMode, + Secure: secure, + SameSite: sameSite, }) server.Use(sessions.Sessions("session", store))