From 9d30ceae8dce4be08ab45d3ef7f318fbe35d1eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=80=E5=88=80?= Date: Thu, 25 Dec 2025 14:47:19 +0800 Subject: [PATCH] =?UTF-8?q?CC=20400=20=E8=BF=94=E5=9B=9E=E5=85=B7=E4=BD=93?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E4=BF=A1=E6=81=AF=20&&=20=E9=9D=9E=20CC=20?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E6=97=B6=E5=A2=9E=E5=8A=A0=20system=20prompt?= =?UTF-8?q?=20(#26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: http 400 返回具体错误 * 更新 workflows * 优化打包/docker 构建流程 * 400 是返回 原始错误 - json 格式 * feat: 非 cc请求时补充 system * go mod tidy --- .github/workflows/release.yml | 94 ++++----------------- .goreleaser.yaml | 58 +++++++++++-- Dockerfile.goreleaser | 40 +++++++++ backend/go.mod | 6 +- backend/go.sum | 9 ++ backend/internal/service/gateway_service.go | 17 ++++ 6 files changed, 142 insertions(+), 82 deletions(-) create mode 100644 Dockerfile.goreleaser diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1dcb9d13..a6a5c09b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,6 +85,19 @@ jobs: go-version: '1.24' cache-dependency-path: backend/go.sum + # Docker setup for GoReleaser + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Fetch tags with annotations run: | # 确保获取完整的 annotated tag 信息 @@ -117,87 +130,16 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG_MESSAGE: ${{ steps.tag_message.outputs.message }} + GITHUB_REPO_OWNER: ${{ github.repository_owner }} + GITHUB_REPO_NAME: ${{ github.event.repository.name }} + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - # =========================================================================== - # Docker Build and Push - # =========================================================================== - docker: - needs: [update-version, build-frontend] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Download VERSION artifact - uses: actions/download-artifact@v4 - with: - name: version-file - path: backend/cmd/server/ - - - name: Download frontend artifact - uses: actions/download-artifact@v4 - with: - name: frontend-dist - path: backend/internal/web/dist/ - - # Extract version from tag - - name: Extract version - id: version - run: | - VERSION=${GITHUB_REF#refs/tags/v} - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Version: $VERSION" - - # Set up Docker Buildx for multi-platform builds - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - # Login to DockerHub - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Extract metadata for Docker - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: | - weishaw/sub2api - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=raw,value=latest,enable={{is_default_branch}} - - # Build and push Docker image - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - build-args: | - VERSION=${{ steps.version.outputs.version }} - COMMIT=${{ github.sha }} - DATE=${{ github.event.head_commit.timestamp }} - cache-from: type=gha - cache-to: type=gha,mode=max - - # Update DockerHub description (optional) + # Update DockerHub description - name: Update DockerHub description uses: peter-evans/dockerhub-description@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - repository: weishaw/sub2api + repository: ${{ secrets.DOCKERHUB_USERNAME }}/sub2api short-description: "Sub2API - AI API Gateway Platform" readme-filepath: ./deploy/DOCKER.md diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 5a0586ab..e899a40a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -52,10 +52,58 @@ changelog: # 禁用自动 changelog,完全使用 tag 消息 disable: true +# Docker images +dockers: + - id: amd64 + goos: linux + goarch: amd64 + image_templates: + - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-amd64" + dockerfile: Dockerfile.goreleaser + use: buildx + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.revision={{ .Commit }}" + + - id: arm64 + goos: linux + goarch: arm64 + image_templates: + - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-arm64" + dockerfile: Dockerfile.goreleaser + use: buildx + build_flag_templates: + - "--platform=linux/arm64" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.revision={{ .Commit }}" + +# Docker manifests for multi-arch support +docker_manifests: + - name_template: "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}" + image_templates: + - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-amd64" + - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-arm64" + + - name_template: "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:latest" + image_templates: + - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-amd64" + - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-arm64" + + - name_template: "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Major }}.{{ .Minor }}" + image_templates: + - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-amd64" + - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-arm64" + + - name_template: "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Major }}" + image_templates: + - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-amd64" + - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-arm64" + release: github: - owner: Wei-Shaw - name: sub2api + owner: "{{ .Env.GITHUB_REPO_OWNER }}" + name: "{{ .Env.GITHUB_REPO_NAME }}" draft: false prerelease: auto name_template: "Sub2API {{.Version}}" @@ -73,7 +121,7 @@ release: **One-line install (Linux):** ```bash - curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash + curl -sSL https://raw.githubusercontent.com/{{ .Env.GITHUB_REPO_OWNER }}/{{ .Env.GITHUB_REPO_NAME }}/main/deploy/install.sh | sudo bash ``` **Manual download:** @@ -81,5 +129,5 @@ release: ## 📚 Documentation - - [GitHub Repository](https://github.com/Wei-Shaw/sub2api) - - [Installation Guide](https://github.com/Wei-Shaw/sub2api/blob/main/deploy/README.md) + - [GitHub Repository](https://github.com/{{ .Env.GITHUB_REPO_OWNER }}/{{ .Env.GITHUB_REPO_NAME }}) + - [Installation Guide](https://github.com/{{ .Env.GITHUB_REPO_OWNER }}/{{ .Env.GITHUB_REPO_NAME }}/blob/main/deploy/README.md) diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser new file mode 100644 index 00000000..2242c162 --- /dev/null +++ b/Dockerfile.goreleaser @@ -0,0 +1,40 @@ +# ============================================================================= +# Sub2API Dockerfile for GoReleaser +# ============================================================================= +# This Dockerfile is used by GoReleaser to build Docker images. +# It only packages the pre-built binary, no compilation needed. +# ============================================================================= + +FROM alpine:3.19 + +LABEL maintainer="Wei-Shaw " +LABEL description="Sub2API - AI API Gateway Platform" +LABEL org.opencontainers.image.source="https://github.com/Wei-Shaw/sub2api" + +# Install runtime dependencies +RUN apk add --no-cache \ + ca-certificates \ + tzdata \ + curl \ + && rm -rf /var/cache/apk/* + +# Create non-root user +RUN addgroup -g 1000 sub2api && \ + adduser -u 1000 -G sub2api -s /bin/sh -D sub2api + +WORKDIR /app + +# Copy pre-built binary from GoReleaser +COPY sub2api /app/sub2api + +# Create data directory +RUN mkdir -p /app/data && chown -R sub2api:sub2api /app + +USER sub2api + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:${SERVER_PORT:-8080}/health || exit 1 + +ENTRYPOINT ["/app/sub2api"] diff --git a/backend/go.mod b/backend/go.mod index aa9793c1..1eee0a24 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -8,10 +8,13 @@ require ( github.com/gin-gonic/gin v1.9.1 github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/uuid v1.6.0 + github.com/google/wire v0.7.0 github.com/imroc/req/v3 v3.56.0 github.com/lib/pq v1.10.9 github.com/redis/go-redis/v9 v9.3.0 github.com/spf13/viper v1.18.2 + github.com/tidwall/gjson v1.18.0 + github.com/tidwall/sjson v1.2.5 golang.org/x/crypto v0.44.0 golang.org/x/net v0.47.0 golang.org/x/term v0.37.0 @@ -35,7 +38,6 @@ require ( github.com/goccy/go-json v0.10.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/subcommands v1.2.0 // indirect - github.com/google/wire v0.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/icholy/digest v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -64,6 +66,8 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect go.uber.org/atomic v1.9.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 0198ecfc..efbb96d0 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -139,6 +139,15 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 71f290a6..d8a7a8b9 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -20,6 +20,8 @@ import ( "github.com/Wei-Shaw/sub2api/internal/model" "github.com/Wei-Shaw/sub2api/internal/pkg/claude" "github.com/Wei-Shaw/sub2api/internal/service/ports" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" "github.com/gin-gonic/gin" ) @@ -390,6 +392,18 @@ func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *m return nil, fmt.Errorf("parse request: %w", err) } + if !gjson.GetBytes(body, "system").Exists() { + body, _ = sjson.SetBytes(body, "system", []any{ + map[string]any{ + "type": "text", + "text": "You are Claude Code, Anthropic's official CLI for Claude.", + "cache_control": map[string]string{ + "type": "ephemeral", + }, + }, + }) + } + // 应用模型映射(仅对apikey类型账号) originalModel := req.Model if account.Type == model.AccountTypeApiKey { @@ -622,6 +636,9 @@ func (s *GatewayService) handleErrorResponse(ctx context.Context, resp *http.Res var statusCode int switch resp.StatusCode { + case 400: + c.Data(http.StatusBadRequest, "application/json", body) + return nil, fmt.Errorf("upstream error: %d", resp.StatusCode) case 401: statusCode = http.StatusBadGateway errType = "upstream_error"