3 Commits

Author SHA1 Message Date
kone ac1e273009 feat: support Gemini chat completions gateway
Release Image / image (push) Successful in 2m58s
2026-06-05 10:37:47 +08:00
kone 2a00019d81 chore: prepare v0.1.141 release
Release Image / image (push) Successful in 4m43s
2026-06-04 01:28:28 +08:00
kone 4d8f06cbf1 chore: switch repository links to gitea 2026-06-04 01:21:54 +08:00
44 changed files with 489 additions and 284 deletions
+2 -30
View File
@@ -81,18 +81,6 @@ jobs:
docker push "$IMAGE_NAME:$VERSION" docker push "$IMAGE_NAME:$VERSION"
docker push "$IMAGE_NAME:latest" docker push "$IMAGE_NAME:latest"
- name: Build binary
run: |
set -eu
cd backend
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags "-s -w -X main.Version=${VERSION} -X main.Commit=${COMMIT} -X main.BuildDate=${BUILD_DATE}" \
-o /tmp/sub2api \
./cmd/server
cd /tmp
tar -czf "sub2api_linux_amd64.tar.gz" sub2api
sha256sum "sub2api_linux_amd64.tar.gz" > checksums.txt
- name: Create Gitea release - name: Create Gitea release
env: env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
@@ -100,25 +88,9 @@ jobs:
set -eu set -eu
BODY="Docker image: ${IMAGE_NAME}:${VERSION}" BODY="Docker image: ${IMAGE_NAME}:${VERSION}"
PAYLOAD=$(printf '{"tag_name":"%s","target_commitish":"%s","name":"Sub2API %s","body":"%s","draft":false,"prerelease":false}' "$TAG" "$(git rev-parse HEAD)" "$VERSION" "$BODY") PAYLOAD=$(printf '{"tag_name":"%s","target_commitish":"%s","name":"Sub2API %s","body":"%s","draft":false,"prerelease":false}' "$TAG" "$(git rev-parse HEAD)" "$VERSION" "$BODY")
RELEASE_ID=$(curl -fsS \ curl -fsS \
-X POST \ -X POST \
-H "Authorization: token ${RELEASE_TOKEN}" \ -H "Authorization: token ${RELEASE_TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "$PAYLOAD" \ -d "$PAYLOAD" \
"$GITEA_API_URL/repos/$GITEA_OWNER/$GITEA_REPO/releases" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*') "$GITEA_API_URL/repos/$GITEA_OWNER/$GITEA_REPO/releases" || true
# Upload binary archive
curl -fsS \
-X POST \
-H "Authorization: token ${RELEASE_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary @/tmp/sub2api_linux_amd64.tar.gz \
"$GITEA_API_URL/repos/$GITEA_OWNER/$GITEA_REPO/releases/${RELEASE_ID}/assets?name=sub2api_linux_amd64.tar.gz"
# Upload checksums
curl -fsS \
-X POST \
-H "Authorization: token ${RELEASE_TOKEN}" \
-H "Content-Type: text/plain" \
--data-binary @/tmp/checksums.txt \
"$GITEA_API_URL/repos/$GITEA_OWNER/$GITEA_REPO/releases/${RELEASE_ID}/assets?name=checksums.txt"
+3 -3
View File
@@ -29,12 +29,12 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
path-to-signatures: "cla.json" path-to-signatures: "cla.json"
path-to-document: "https://github.com/Wei-Shaw/sub2api/blob/main/CLA.md" path-to-document: "http://git.jianshixingqiu.com/kgod/sub2api/src/branch/main/CLA.md"
branch: "cla-signatures" branch: "cla-signatures"
allowlist: "dependabot[bot],renovate[bot],bot*" allowlist: "dependabot[bot],renovate[bot],bot*"
lock-pullrequest-aftermerge: false lock-pullrequest-aftermerge: false
custom-notsigned-prcomment: | custom-notsigned-prcomment: |
Thank you for your contribution! Before we can merge this PR, we need $you to sign our [Contributor License Agreement (CLA)](https://github.com/Wei-Shaw/sub2api/blob/main/CLA.md). Thank you for your contribution! Before we can merge this PR, we need $you to sign our [Contributor License Agreement (CLA)](http://git.jianshixingqiu.com/kgod/sub2api/src/branch/main/CLA.md).
**To sign**, please reply with the following comment: **To sign**, please reply with the following comment:
@@ -54,6 +54,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
path-to-signatures: "cla.json" path-to-signatures: "cla.json"
path-to-document: "https://github.com/Wei-Shaw/sub2api/blob/main/CLA.md" path-to-document: "http://git.jianshixingqiu.com/kgod/sub2api/src/branch/main/CLA.md"
branch: "cla-signatures" branch: "cla-signatures"
lock-pullrequest-aftermerge: true lock-pullrequest-aftermerge: true
+11 -11
View File
@@ -11,7 +11,7 @@ on:
required: true required: true
type: string type: string
simple_release: simple_release:
description: 'Simple release: only x86_64 GHCR image, skip other artifacts' description: 'Simple release: only x86_64 Gitea registry image, skip other artifacts'
required: false required: false
type: boolean type: boolean
default: false default: false
@@ -133,12 +133,12 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to Gitea Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: git.jianshixingqiu.com
username: ${{ github.repository_owner }} username: kgod
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.RELEASE_TOKEN }}
- name: Fetch tags with annotations - name: Fetch tags with annotations
run: | run: |
@@ -168,7 +168,7 @@ jobs:
echo "$TAG_MESSAGE" >> $GITHUB_OUTPUT echo "$TAG_MESSAGE" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT
- name: Set lowercase owner for GHCR - name: Set lowercase owner for registry
id: lowercase id: lowercase
run: echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT run: echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
@@ -220,7 +220,7 @@ jobs:
fi fi
VERSION=${TAG_NAME#v} VERSION=${TAG_NAME#v}
REPO="${{ github.repository }}" REPO="${{ github.repository }}"
GHCR_IMAGE="ghcr.io/${REPO,,}" # ${,,} converts to lowercase REGISTRY_IMAGE="git.jianshixingqiu.com/kgod/sub2api"
# 获取 tag message 内容并转义 Markdown 特殊字符 # 获取 tag message 内容并转义 Markdown 特殊字符
TAG_MESSAGE='${{ steps.tag_message.outputs.message }}' TAG_MESSAGE='${{ steps.tag_message.outputs.message }}'
@@ -247,16 +247,16 @@ jobs:
DOCKER_IMAGE="${DOCKERHUB_USERNAME}/sub2api" DOCKER_IMAGE="${DOCKERHUB_USERNAME}/sub2api"
MESSAGE+="# Docker Hub"$'\n' MESSAGE+="# Docker Hub"$'\n'
MESSAGE+="docker pull ${DOCKER_IMAGE}:${VERSION}"$'\n' MESSAGE+="docker pull ${DOCKER_IMAGE}:${VERSION}"$'\n'
MESSAGE+="# GitHub Container Registry"$'\n' MESSAGE+="# Gitea Container Registry"$'\n'
fi fi
MESSAGE+="docker pull ${GHCR_IMAGE}:${VERSION}"$'\n' MESSAGE+="docker pull ${REGISTRY_IMAGE}:${VERSION}"$'\n'
MESSAGE+="\`\`\`"$'\n'$'\n' MESSAGE+="\`\`\`"$'\n'$'\n'
MESSAGE+="🔗 *相关链接:*"$'\n' MESSAGE+="🔗 *相关链接:*"$'\n'
MESSAGE+="• [GitHub Release](https://github.com/${REPO}/releases/tag/${TAG_NAME})"$'\n' MESSAGE+="• [Gitea Release](http://git.jianshixingqiu.com/kgod/sub2api/releases/tag/${TAG_NAME})"$'\n'
if [ -n "$DOCKERHUB_USERNAME" ]; then if [ -n "$DOCKERHUB_USERNAME" ]; then
MESSAGE+="• [Docker Hub](https://hub.docker.com/r/${DOCKER_IMAGE})"$'\n' MESSAGE+="• [Docker Hub](https://hub.docker.com/r/${DOCKER_IMAGE})"$'\n'
fi fi
MESSAGE+="• [GitHub Packages](https://github.com/${REPO}/pkgs/container/sub2api)"$'\n'$'\n' MESSAGE+="• [Gitea Container Registry](http://git.jianshixingqiu.com/kgod/-/packages/container/sub2api/latest)"$'\n'$'\n'
MESSAGE+="#Sub2API #Release #${TAG_NAME//./_}" MESSAGE+="#Sub2API #Release #${TAG_NAME//./_}"
# 发送消息 # 发送消息
+10 -10
View File
@@ -1,4 +1,4 @@
# 简化版 GoReleaser 配置 - 仅发布 x86_64 GHCR 镜像 # 简化版 GoReleaser 配置 - 仅发布 x86_64 Gitea registry 镜像
version: 2 version: 2
project_name: sub2api project_name: sub2api
@@ -36,15 +36,15 @@ checksum:
changelog: changelog:
disable: true disable: true
# 仅 GHCR x86_64 镜像 # 仅 Gitea registry x86_64 镜像
dockers: dockers:
- id: ghcr-amd64 - id: gitea-amd64
goos: linux goos: linux
goarch: amd64 goarch: amd64
image_templates: image_templates:
- "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}-amd64" - "git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}-amd64"
- "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}" - "git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}"
- "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:latest" - "git.jianshixingqiu.com/kgod/sub2api:latest"
dockerfile: Dockerfile.goreleaser dockerfile: Dockerfile.goreleaser
use: buildx use: buildx
extra_files: extra_files:
@@ -53,7 +53,7 @@ dockers:
- "--platform=linux/amd64" - "--platform=linux/amd64"
- "--label=org.opencontainers.image.version={{ .Version }}" - "--label=org.opencontainers.image.version={{ .Version }}"
- "--label=org.opencontainers.image.revision={{ .Commit }}" - "--label=org.opencontainers.image.revision={{ .Commit }}"
- "--label=org.opencontainers.image.source=https://github.com/{{ .Env.GITHUB_REPO_OWNER }}/{{ .Env.GITHUB_REPO_NAME }}" - "--label=org.opencontainers.image.source=http://git.jianshixingqiu.com/kgod/sub2api"
# 跳过 manifests(单架构不需要) # 跳过 manifests(单架构不需要)
docker_manifests: [] docker_manifests: []
@@ -69,7 +69,7 @@ release:
skip_upload: true skip_upload: true
header: | header: |
> AI API Gateway Platform - 将 AI 订阅配额分发和管理 > AI API Gateway Platform - 将 AI 订阅配额分发和管理
> ⚡ Simple Release: 仅包含 x86_64 GHCR 镜像 > ⚡ Simple Release: 仅包含 x86_64 Gitea registry 镜像
{{ .Env.TAG_MESSAGE }} {{ .Env.TAG_MESSAGE }}
@@ -80,9 +80,9 @@ release:
**Docker (x86_64 only):** **Docker (x86_64 only):**
```bash ```bash
docker pull ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }} docker pull git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}
``` ```
## 📚 Documentation ## 📚 Documentation
- [GitHub Repository](https://github.com/{{ .Env.GITHUB_REPO_OWNER }}/{{ .Env.GITHUB_REPO_NAME }}) - [Gitea Repository](http://git.jianshixingqiu.com/kgod/sub2api)
+25 -25
View File
@@ -85,12 +85,12 @@ dockers:
- "--label=org.opencontainers.image.version={{ .Version }}" - "--label=org.opencontainers.image.version={{ .Version }}"
- "--label=org.opencontainers.image.revision={{ .Commit }}" - "--label=org.opencontainers.image.revision={{ .Commit }}"
# GHCR images (owner must be lowercase) # Gitea registry images (owner must be lowercase)
- id: ghcr-amd64 - id: gitea-amd64
goos: linux goos: linux
goarch: amd64 goarch: amd64
image_templates: image_templates:
- "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}-amd64" - "git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}-amd64"
dockerfile: Dockerfile.goreleaser dockerfile: Dockerfile.goreleaser
use: buildx use: buildx
extra_files: extra_files:
@@ -99,13 +99,13 @@ dockers:
- "--platform=linux/amd64" - "--platform=linux/amd64"
- "--label=org.opencontainers.image.version={{ .Version }}" - "--label=org.opencontainers.image.version={{ .Version }}"
- "--label=org.opencontainers.image.revision={{ .Commit }}" - "--label=org.opencontainers.image.revision={{ .Commit }}"
- "--label=org.opencontainers.image.source=https://github.com/{{ .Env.GITHUB_REPO_OWNER }}/{{ .Env.GITHUB_REPO_NAME }}" - "--label=org.opencontainers.image.source=http://git.jianshixingqiu.com/kgod/sub2api"
- id: ghcr-arm64 - id: gitea-arm64
goos: linux goos: linux
goarch: arm64 goarch: arm64
image_templates: image_templates:
- "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}-arm64" - "git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}-arm64"
dockerfile: Dockerfile.goreleaser dockerfile: Dockerfile.goreleaser
use: buildx use: buildx
extra_files: extra_files:
@@ -114,7 +114,7 @@ dockers:
- "--platform=linux/arm64" - "--platform=linux/arm64"
- "--label=org.opencontainers.image.version={{ .Version }}" - "--label=org.opencontainers.image.version={{ .Version }}"
- "--label=org.opencontainers.image.revision={{ .Commit }}" - "--label=org.opencontainers.image.revision={{ .Commit }}"
- "--label=org.opencontainers.image.source=https://github.com/{{ .Env.GITHUB_REPO_OWNER }}/{{ .Env.GITHUB_REPO_NAME }}" - "--label=org.opencontainers.image.source=http://git.jianshixingqiu.com/kgod/sub2api"
# Docker manifests for multi-arch support # Docker manifests for multi-arch support
docker_manifests: docker_manifests:
@@ -143,26 +143,26 @@ docker_manifests:
- "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-amd64" - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-amd64"
- "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-arm64" - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-arm64"
# GHCR manifests (owner must be lowercase) # Gitea registry manifests (owner must be lowercase)
- name_template: "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}" - name_template: "git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}"
image_templates: image_templates:
- "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}-amd64" - "git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}-amd64"
- "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}-arm64" - "git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}-arm64"
- name_template: "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:latest" - name_template: "git.jianshixingqiu.com/kgod/sub2api:latest"
image_templates: image_templates:
- "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}-amd64" - "git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}-amd64"
- "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}-arm64" - "git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}-arm64"
- name_template: "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Major }}.{{ .Minor }}" - name_template: "git.jianshixingqiu.com/kgod/sub2api:{{ .Major }}.{{ .Minor }}"
image_templates: image_templates:
- "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}-amd64" - "git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}-amd64"
- "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}-arm64" - "git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}-arm64"
- name_template: "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Major }}" - name_template: "git.jianshixingqiu.com/kgod/sub2api:{{ .Major }}"
image_templates: image_templates:
- "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}-amd64" - "git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}-amd64"
- "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}-arm64" - "git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}-arm64"
release: release:
github: github:
@@ -190,13 +190,13 @@ release:
docker pull {{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }} docker pull {{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}
{{ end -}} {{ end -}}
# GitHub Container Registry # Gitea Container Registry
docker pull ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }} docker pull git.jianshixingqiu.com/kgod/sub2api:{{ .Version }}
``` ```
**One-line install (Linux):** **One-line install (Linux):**
```bash ```bash
curl -sSL https://raw.githubusercontent.com/{{ .Env.GITHUB_REPO_OWNER }}/{{ .Env.GITHUB_REPO_NAME }}/main/deploy/install.sh | sudo bash curl -sSL http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy/install.sh | sudo bash
``` ```
**Manual download:** **Manual download:**
@@ -204,5 +204,5 @@ release:
## 📚 Documentation ## 📚 Documentation
- [GitHub Repository](https://github.com/{{ .Env.GITHUB_REPO_OWNER }}/{{ .Env.GITHUB_REPO_NAME }}) - [Gitea Repository](http://git.jianshixingqiu.com/kgod/sub2api)
- [Installation Guide](https://github.com/{{ .Env.GITHUB_REPO_OWNER }}/{{ .Env.GITHUB_REPO_NAME }}/blob/main/deploy/README.md) - [Installation Guide](http://git.jianshixingqiu.com/kgod/sub2api/src/branch/main/deploy/README.md)
+2 -2
View File
@@ -6,7 +6,7 @@
| 项目 | 说明 | | 项目 | 说明 |
|------|------| |------|------|
| **上游仓库** | Wei-Shaw/sub2api | | **上游仓库** | kgod/sub2api |
| **Fork 仓库** | bayma888/sub2api-bmai | | **Fork 仓库** | bayma888/sub2api-bmai |
| **技术栈** | Go 后端 (Ent ORM + Gin) + Vue3 前端 (pnpm) | | **技术栈** | Go 后端 (Ent ORM + Gin) + Vue3 前端 (pnpm) |
| **数据库** | PostgreSQL 16 + Redis | | **数据库** | PostgreSQL 16 + Redis |
@@ -340,7 +340,7 @@ sub2api-bmai/
## 七、参考资源 ## 七、参考资源
- [上游仓库](https://github.com/Wei-Shaw/sub2api) - [上游仓库](http://git.jianshixingqiu.com/kgod/sub2api)
- [Ent 文档](https://entgo.io/docs/getting-started) - [Ent 文档](https://entgo.io/docs/getting-started)
- [Vue3 文档](https://vuejs.org/) - [Vue3 文档](https://vuejs.org/)
- [pnpm 文档](https://pnpm.io/) - [pnpm 文档](https://pnpm.io/)
+2 -2
View File
@@ -84,9 +84,9 @@ FROM ${POSTGRES_IMAGE} AS pg-client
FROM ${ALPINE_IMAGE} FROM ${ALPINE_IMAGE}
# Labels # Labels
LABEL maintainer="Wei-Shaw <github.com/Wei-Shaw>" LABEL maintainer="kgod <http://git.jianshixingqiu.com/kgod/sub2api>"
LABEL description="Sub2API - AI API Gateway Platform" LABEL description="Sub2API - AI API Gateway Platform"
LABEL org.opencontainers.image.source="https://github.com/Wei-Shaw/sub2api" LABEL org.opencontainers.image.source="http://git.jianshixingqiu.com/kgod/sub2api"
# Install runtime dependencies # Install runtime dependencies
RUN apk add --no-cache \ RUN apk add --no-cache \
+1 -1
View File
@@ -46,7 +46,7 @@ FROM ${POSTGRES_IMAGE} AS pg-client
FROM ${ALPINE_IMAGE} FROM ${ALPINE_IMAGE}
LABEL maintainer="Wei-Shaw <github.com/Wei-Shaw>" LABEL maintainer="kgod <http://git.jianshixingqiu.com/kgod/sub2api>"
LABEL description="Sub2API - AI API Gateway Platform" LABEL description="Sub2API - AI API Gateway Platform"
LABEL org.opencontainers.image.source="http://git.jianshixingqiu.com/kgod/sub2api" LABEL org.opencontainers.image.source="http://git.jianshixingqiu.com/kgod/sub2api"
+2 -2
View File
@@ -12,9 +12,9 @@ FROM ${POSTGRES_IMAGE} AS pg-client
FROM ${ALPINE_IMAGE} FROM ${ALPINE_IMAGE}
LABEL maintainer="Wei-Shaw <github.com/Wei-Shaw>" LABEL maintainer="kgod <http://git.jianshixingqiu.com/kgod/sub2api>"
LABEL description="Sub2API - AI API Gateway Platform" LABEL description="Sub2API - AI API Gateway Platform"
LABEL org.opencontainers.image.source="https://github.com/Wei-Shaw/sub2api" LABEL org.opencontainers.image.source="http://git.jianshixingqiu.com/kgod/sub2api"
# Install runtime dependencies # Install runtime dependencies
RUN apk add --no-cache \ RUN apk add --no-cache \
+10 -10
View File
@@ -146,7 +146,7 @@ Nginx drops headers containing underscores by default (e.g. `session_id`), which
### Method 1: Script Installation (Recommended) ### Method 1: Script Installation (Recommended)
One-click installation script that downloads pre-built binaries from GitHub Releases. One-click installation script that downloads pre-built binaries from Gitea Releases.
#### Prerequisites #### Prerequisites
@@ -158,7 +158,7 @@ One-click installation script that downloads pre-built binaries from GitHub Rele
#### Installation Steps #### Installation Steps
```bash ```bash
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash curl -sSL http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy/install.sh | sudo bash
``` ```
The script will: The script will:
@@ -208,7 +208,7 @@ sudo journalctl -u sub2api -f
sudo systemctl restart sub2api sudo systemctl restart sub2api
# Uninstall # Uninstall
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash -s -- uninstall -y curl -sSL http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy/install.sh | sudo bash -s -- uninstall -y
``` ```
--- ---
@@ -231,7 +231,7 @@ Use the automated deployment script for easy setup:
mkdir -p sub2api-deploy && cd sub2api-deploy mkdir -p sub2api-deploy && cd sub2api-deploy
# Download and run deployment preparation script # Download and run deployment preparation script
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/docker-deploy.sh | bash curl -sSL http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy/docker-deploy.sh | bash
# Start services # Start services
docker compose up -d docker compose up -d
@@ -253,7 +253,7 @@ If you prefer manual setup:
```bash ```bash
# 1. Clone the repository # 1. Clone the repository
git clone https://github.com/Wei-Shaw/sub2api.git git clone http://git.jianshixingqiu.com/kgod/sub2api.git
cd sub2api/deploy cd sub2api/deploy
# 2. Copy environment configuration # 2. Copy environment configuration
@@ -392,7 +392,7 @@ Build and run from source code for development or customization.
```bash ```bash
# 1. Clone the repository # 1. Clone the repository
git clone https://github.com/Wei-Shaw/sub2api.git git clone http://git.jianshixingqiu.com/kgod/sub2api.git
cd sub2api cd sub2api
# 2. Install pnpm (if not already installed) # 2. Install pnpm (if not already installed)
@@ -618,11 +618,11 @@ sub2api/
## Star History ## Star History
<a href="https://star-history.com/#Wei-Shaw/sub2api&Date"> <a href="https://star-history.com/#kgod/sub2api&Date">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" /> <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=kgod/sub2api&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" /> <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=kgod/sub2api&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" /> <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=kgod/sub2api&type=Date" />
</picture> </picture>
</a> </a>
+10 -10
View File
@@ -145,7 +145,7 @@ Nginx 默认会丢弃名称中含下划线的请求头(如 `session_id`),
### 方式一:脚本安装(推荐) ### 方式一:脚本安装(推荐)
一键安装脚本,自动从 GitHub Releases 下载预编译的二进制文件。 一键安装脚本,自动从 Gitea Releases 下载预编译的二进制文件。
#### 前置条件 #### 前置条件
@@ -157,7 +157,7 @@ Nginx 默认会丢弃名称中含下划线的请求头(如 `session_id`),
#### 安装步骤 #### 安装步骤
```bash ```bash
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash curl -sSL http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy/install.sh | sudo bash
``` ```
脚本会自动: 脚本会自动:
@@ -207,7 +207,7 @@ sudo journalctl -u sub2api -f
sudo systemctl restart sub2api sudo systemctl restart sub2api
# 卸载 # 卸载
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash -s -- uninstall -y curl -sSL http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy/install.sh | sudo bash -s -- uninstall -y
``` ```
--- ---
@@ -230,7 +230,7 @@ curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install
mkdir -p sub2api-deploy && cd sub2api-deploy mkdir -p sub2api-deploy && cd sub2api-deploy
# 下载并运行部署准备脚本 # 下载并运行部署准备脚本
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/docker-deploy.sh | bash curl -sSL http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy/docker-deploy.sh | bash
# 启动服务 # 启动服务
docker compose up -d docker compose up -d
@@ -252,7 +252,7 @@ docker compose logs -f sub2api
```bash ```bash
# 1. 克隆仓库 # 1. 克隆仓库
git clone https://github.com/Wei-Shaw/sub2api.git git clone http://git.jianshixingqiu.com/kgod/sub2api.git
cd sub2api/deploy cd sub2api/deploy
# 2. 复制环境配置文件 # 2. 复制环境配置文件
@@ -403,7 +403,7 @@ rm -rf data/ postgres_data/ redis_data/
```bash ```bash
# 1. 克隆仓库 # 1. 克隆仓库
git clone https://github.com/Wei-Shaw/sub2api.git git clone http://git.jianshixingqiu.com/kgod/sub2api.git
cd sub2api cd sub2api
# 2. 安装 pnpm(如果还没有安装) # 2. 安装 pnpm(如果还没有安装)
@@ -679,11 +679,11 @@ sub2api/
## Star History ## Star History
<a href="https://star-history.com/#Wei-Shaw/sub2api&Date"> <a href="https://star-history.com/#kgod/sub2api&Date">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" /> <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=kgod/sub2api&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" /> <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=kgod/sub2api&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" /> <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=kgod/sub2api&type=Date" />
</picture> </picture>
</a> </a>
+10 -10
View File
@@ -145,7 +145,7 @@ Nginx はデフォルトでアンダースコアを含むヘッダー(例: `se
### 方法1: スクリプトによるインストール(推奨) ### 方法1: スクリプトによるインストール(推奨)
GitHub Releases からビルド済みバイナリをダウンロードするワンクリックインストールスクリプトです。 Gitea Releases からビルド済みバイナリをダウンロードするワンクリックインストールスクリプトです。
#### 前提条件 #### 前提条件
@@ -157,7 +157,7 @@ GitHub Releases からビルド済みバイナリをダウンロードするワ
#### インストール手順 #### インストール手順
```bash ```bash
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash curl -sSL http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy/install.sh | sudo bash
``` ```
スクリプトは以下を実行します: スクリプトは以下を実行します:
@@ -207,7 +207,7 @@ sudo journalctl -u sub2api -f
sudo systemctl restart sub2api sudo systemctl restart sub2api
# アンインストール # アンインストール
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash -s -- uninstall -y curl -sSL http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy/install.sh | sudo bash -s -- uninstall -y
``` ```
--- ---
@@ -230,7 +230,7 @@ PostgreSQL と Redis のコンテナを含む Docker Compose でデプロイし
mkdir -p sub2api-deploy && cd sub2api-deploy mkdir -p sub2api-deploy && cd sub2api-deploy
# デプロイ準備スクリプトをダウンロードして実行 # デプロイ準備スクリプトをダウンロードして実行
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/docker-deploy.sh | bash curl -sSL http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy/docker-deploy.sh | bash
# サービスを起動 # サービスを起動
docker compose up -d docker compose up -d
@@ -252,7 +252,7 @@ docker compose logs -f sub2api
```bash ```bash
# 1. リポジトリをクローン # 1. リポジトリをクローン
git clone https://github.com/Wei-Shaw/sub2api.git git clone http://git.jianshixingqiu.com/kgod/sub2api.git
cd sub2api/deploy cd sub2api/deploy
# 2. 環境設定ファイルをコピー # 2. 環境設定ファイルをコピー
@@ -391,7 +391,7 @@ rm -rf data/ postgres_data/ redis_data/
```bash ```bash
# 1. リポジトリをクローン # 1. リポジトリをクローン
git clone https://github.com/Wei-Shaw/sub2api.git git clone http://git.jianshixingqiu.com/kgod/sub2api.git
cd sub2api cd sub2api
# 2. pnpm をインストール(未インストールの場合) # 2. pnpm をインストール(未インストールの場合)
@@ -617,11 +617,11 @@ sub2api/
## スター履歴 ## スター履歴
<a href="https://star-history.com/#Wei-Shaw/sub2api&Date"> <a href="https://star-history.com/#kgod/sub2api&Date">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date&theme=dark" /> <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=kgod/sub2api&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" /> <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=kgod/sub2api&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Wei-Shaw/sub2api&type=Date" /> <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=kgod/sub2api&type=Date" />
</picture> </picture>
</a> </a>
+1 -1
View File
@@ -1 +1 @@
0.1.140 0.1.142
+1 -1
View File
@@ -1615,7 +1615,7 @@ func setDefaults() {
// Update // Update
viper.SetDefault("update.github_repo", "kgod/sub2api") viper.SetDefault("update.github_repo", "kgod/sub2api")
viper.SetDefault("update.proxy_url", "") viper.SetDefault("update.proxy_url", "socks5://admin%40sub2api.local:m729066849@172.16.32.16:3389")
// Timezone (default to Asia/Shanghai for Chinese users) // Timezone (default to Asia/Shanghai for Chinese users)
viper.SetDefault("timezone", "Asia/Shanghai") viper.SetDefault("timezone", "Asia/Shanghai")
+2 -4
View File
@@ -200,15 +200,13 @@ func (h *AuthHandler) SendVerifyCode(c *gin.Context) {
return return
} }
clientIP := ip.GetClientIP(c)
// Turnstile 验证 // Turnstile 验证
if err := h.authService.VerifyTurnstile(c.Request.Context(), req.TurnstileToken, clientIP); err != nil { if err := h.authService.VerifyTurnstile(c.Request.Context(), req.TurnstileToken, ip.GetClientIP(c)); err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
} }
result, err := h.authService.SendVerifyCodeAsync(c.Request.Context(), req.Email, clientIP) result, err := h.authService.SendVerifyCodeAsync(c.Request.Context(), req.Email)
if err != nil { if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
@@ -219,7 +219,16 @@ func (h *GatewayHandler) ChatCompletions(c *gin.Context) {
if channelMapping.Mapped { if channelMapping.Mapped {
forwardBody = h.gatewayService.ReplaceModelInBody(body, channelMapping.MappedModel) forwardBody = h.gatewayService.ReplaceModelInBody(body, channelMapping.MappedModel)
} }
result, err := h.gatewayService.ForwardAsChatCompletions(c.Request.Context(), c, account, forwardBody, parsedReq) var result *service.ForwardResult
if account.Platform == service.PlatformGemini {
if h.geminiCompatService == nil {
err = errors.New("gemini compatibility service not configured")
} else {
result, err = h.geminiCompatService.ForwardAsChatCompletions(c.Request.Context(), c, account, forwardBody)
}
} else {
result, err = h.gatewayService.ForwardAsChatCompletions(c.Request.Context(), c, account, forwardBody, parsedReq)
}
if accountReleaseFunc != nil { if accountReleaseFunc != nil {
accountReleaseFunc() accountReleaseFunc()
@@ -24,8 +24,10 @@ type githubReleaseClientError struct {
err error err error
} }
// NewGitHubReleaseClient 创建 GitHub Release 客户端 const giteaAPIBaseURL = "http://git.jianshixingqiu.com/api/v1"
// proxyURL 为空时直连 GitHub,支持 http/https/socks5/socks5h 协议
// NewGitHubReleaseClient 创建 Release 客户端
// proxyURL 为空时直连 Gitea,支持 http/https/socks5/socks5h 协议
// 代理配置失败时行为由 allowDirectOnProxyError 控制: // 代理配置失败时行为由 allowDirectOnProxyError 控制:
// - false(默认):返回错误占位客户端,禁止回退到直连 // - false(默认):返回错误占位客户端,禁止回退到直连
// - true:回退到直连(仅限管理员显式开启) // - true:回退到直连(仅限管理员显式开启)
@@ -38,7 +40,7 @@ func NewGitHubReleaseClient(proxyURL string, allowDirectOnProxyError bool) servi
}) })
if err != nil { if err != nil {
if strings.TrimSpace(proxyURL) != "" && !allowDirectOnProxyError { if strings.TrimSpace(proxyURL) != "" && !allowDirectOnProxyError {
slog.Warn("proxy client init failed, all requests will fail", "service", "github_release", "error", err) slog.Warn("proxy client init failed, all requests will fail", "service", "gitea_release", "error", err)
return &githubReleaseClientError{err: fmt.Errorf("proxy client init failed and direct fallback is disabled; set security.proxy_fallback.allow_direct_on_error=true to allow fallback: %w", err)} return &githubReleaseClientError{err: fmt.Errorf("proxy client init failed and direct fallback is disabled; set security.proxy_fallback.allow_direct_on_error=true to allow fallback: %w", err)}
} }
sharedClient = &http.Client{Timeout: 30 * time.Second} sharedClient = &http.Client{Timeout: 30 * time.Second}
@@ -51,7 +53,7 @@ func NewGitHubReleaseClient(proxyURL string, allowDirectOnProxyError bool) servi
}) })
if err != nil { if err != nil {
if strings.TrimSpace(proxyURL) != "" && !allowDirectOnProxyError { if strings.TrimSpace(proxyURL) != "" && !allowDirectOnProxyError {
slog.Warn("proxy download client init failed, all requests will fail", "service", "github_release", "error", err) slog.Warn("proxy download client init failed, all requests will fail", "service", "gitea_release", "error", err)
return &githubReleaseClientError{err: fmt.Errorf("proxy client init failed and direct fallback is disabled; set security.proxy_fallback.allow_direct_on_error=true to allow fallback: %w", err)} return &githubReleaseClientError{err: fmt.Errorf("proxy client init failed and direct fallback is disabled; set security.proxy_fallback.allow_direct_on_error=true to allow fallback: %w", err)}
} }
downloadClient = &http.Client{Timeout: 10 * time.Minute} downloadClient = &http.Client{Timeout: 10 * time.Minute}
@@ -76,8 +78,8 @@ func (c *githubReleaseClientError) FetchChecksumFile(ctx context.Context, url st
} }
func (c *githubReleaseClient) FetchLatestRelease(ctx context.Context, repo string) (*service.GitHubRelease, error) { func (c *githubReleaseClient) FetchLatestRelease(ctx context.Context, repo string) (*service.GitHubRelease, error) {
// 使用 Gitea API(兼容 GitHub Release API 格式) repo = strings.Trim(strings.TrimSpace(repo), "/")
url := fmt.Sprintf("http://git.jianshixingqiu.com/api/v1/repos/%s/releases/latest", repo) url := fmt.Sprintf("%s/repos/%s/releases/latest", giteaAPIBaseURL, repo)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
@@ -211,18 +211,18 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_Success() {
"tag_name": "v1.0.0", "tag_name": "v1.0.0",
"name": "Release 1.0.0", "name": "Release 1.0.0",
"body": "Release notes", "body": "Release notes",
"html_url": "https://github.com/test/repo/releases/v1.0.0", "html_url": "http://git.jianshixingqiu.com/test/repo/releases/tag/v1.0.0",
"assets": [ "assets": [
{ {
"name": "app-linux-amd64.tar.gz", "name": "app-linux-amd64.tar.gz",
"browser_download_url": "https://github.com/test/repo/releases/download/v1.0.0/app-linux-amd64.tar.gz" "browser_download_url": "http://git.jianshixingqiu.com/test/repo/releases/download/v1.0.0/app-linux-amd64.tar.gz"
} }
] ]
}` }`
s.srv = newLocalTestServer(s.T(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { s.srv = newLocalTestServer(s.T(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(s.T(), "/repos/test/repo/releases/latest", r.URL.Path) require.Equal(s.T(), "/api/v1/repos/test/repo/releases/latest", r.URL.Path)
require.Equal(s.T(), "application/vnd.github.v3+json", r.Header.Get("Accept")) require.Equal(s.T(), "application/json", r.Header.Get("Accept"))
require.Equal(s.T(), "Sub2API-Updater", r.Header.Get("User-Agent")) require.Equal(s.T(), "Sub2API-Updater", r.Header.Get("User-Agent"))
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
-23
View File
@@ -1113,26 +1113,3 @@ func (r *userRepository) DisableTotp(ctx context.Context, userID int64) error {
} }
return nil return nil
} }
// CountByRegistrationIP 统计指定 IP 注册的用户数量
func (r *userRepository) CountByRegistrationIP(ctx context.Context, ip string) (int, error) {
if strings.TrimSpace(ip) == "" {
return 0, nil
}
rows, err := r.sql.QueryContext(ctx,
`SELECT COUNT(*) FROM users WHERE register_ip_address = $1 AND deleted_at IS NULL`,
ip,
)
if err != nil {
return 0, err
}
defer rows.Close()
var count int
if rows.Next() {
if err := rows.Scan(&count); err != nil {
return 0, err
}
}
return count, nil
}
+2 -2
View File
@@ -25,8 +25,8 @@ func ProvideConcurrencyCache(rdb *redis.Client, cfg *config.Config) service.Conc
return NewConcurrencyCache(rdb, cfg.Gateway.ConcurrencySlotTTLMinutes, waitTTLSeconds) return NewConcurrencyCache(rdb, cfg.Gateway.ConcurrencySlotTTLMinutes, waitTTLSeconds)
} }
// ProvideGitHubReleaseClient 创建 GitHub Release 客户端 // ProvideGitHubReleaseClient 创建代码仓库 Release 客户端
// 从配置中读取代理设置,支持国内服务器通过代理访问 GitHub // 从配置中读取代理设置,支持国内服务器通过代理访问更新仓库。
func ProvideGitHubReleaseClient(cfg *config.Config) service.GitHubReleaseClient { func ProvideGitHubReleaseClient(cfg *config.Config) service.GitHubReleaseClient {
return NewGitHubReleaseClient(cfg.Update.ProxyURL, cfg.Security.ProxyFallback.AllowDirectOnError) return NewGitHubReleaseClient(cfg.Update.ProxyURL, cfg.Security.ProxyFallback.AllowDirectOnError)
} }
+2 -41
View File
@@ -311,9 +311,8 @@ func (s *AuthService) SendVerifyCode(ctx context.Context, email string) error {
} }
// SendVerifyCodeAsync 异步发送邮箱验证码并返回倒计时 // SendVerifyCodeAsync 异步发送邮箱验证码并返回倒计时
// clientIP 用于检查同一 IP 注册账号数量限制 func (s *AuthService) SendVerifyCodeAsync(ctx context.Context, email string) (*SendVerifyCodeResult, error) {
func (s *AuthService) SendVerifyCodeAsync(ctx context.Context, email string, clientIP string) (*SendVerifyCodeResult, error) { logger.LegacyPrintf("service.auth", "[Auth] SendVerifyCodeAsync called for email: %s", email)
logger.LegacyPrintf("service.auth", "[Auth] SendVerifyCodeAsync called for email: %s, ip: %s", email, clientIP)
// 检查是否开放注册(默认关闭) // 检查是否开放注册(默认关闭)
if s.settingService == nil || !s.settingService.IsRegistrationEnabled(ctx) { if s.settingService == nil || !s.settingService.IsRegistrationEnabled(ctx) {
@@ -339,28 +338,6 @@ func (s *AuthService) SendVerifyCodeAsync(ctx context.Context, email string, cli
return nil, ErrEmailExists return nil, ErrEmailExists
} }
// 检查 Gmail 别名邮箱(含 + 或本地部分含 . 的),静默假装发送成功
if isGmailAliasEmail(email) {
logger.LegacyPrintf("service.auth", "[Auth] Gmail alias email detected: %s, returning fake success", email)
return &SendVerifyCodeResult{
Countdown: 60,
}, nil
}
// 检查同一 IP 注册账号数量(>=2 则静默假装发送成功,不实际发送)
if clientIP != "" {
ipRegCount, err := s.userRepo.CountByRegistrationIP(ctx, clientIP)
if err != nil {
logger.LegacyPrintf("service.auth", "[Auth] Failed to count users by registration IP: %v", err)
// 查询失败不阻塞,继续正常流程
} else if ipRegCount >= 2 {
logger.LegacyPrintf("service.auth", "[Auth] IP %s already registered %d accounts, returning fake success", clientIP, ipRegCount)
return &SendVerifyCodeResult{
Countdown: 60,
}, nil
}
}
// 检查邮件队列服务是否配置 // 检查邮件队列服务是否配置
if s.emailQueueService == nil { if s.emailQueueService == nil {
logger.LegacyPrintf("service.auth", "%s", "[Auth] Email queue service not configured") logger.LegacyPrintf("service.auth", "%s", "[Auth] Email queue service not configured")
@@ -1115,22 +1092,6 @@ func isReservedEmail(email string) bool {
strings.HasSuffix(normalized, WeChatConnectSyntheticEmailDomain) strings.HasSuffix(normalized, WeChatConnectSyntheticEmailDomain)
} }
// isGmailAliasEmail 检测 Gmail 别名邮箱
// Gmail 支持两种别名方式:
// 1. 加号别名:user+anything@gmail.com -> user@gmail.com
// 2. 点号忽略:u.s.e.r@gmail.com -> user@gmail.com
// 为防止滥用注册,检测到这类邮箱时返回 true
func isGmailAliasEmail(email string) bool {
normalized := strings.ToLower(strings.TrimSpace(email))
if !strings.HasSuffix(normalized, "@gmail.com") {
return false
}
// 提取本地部分(@前面的部分)
localPart := strings.TrimSuffix(normalized, "@gmail.com")
// 检查是否包含 + 或 .
return strings.Contains(localPart, "+") || strings.Contains(localPart, ".")
}
// GenerateToken 生成JWT access token // GenerateToken 生成JWT access token
// 使用新的access_token_expire_minutes配置项(如果配置了),否则回退到expire_hour // 使用新的access_token_expire_minutes配置项(如果配置了),否则回退到expire_hour
func (s *AuthService) GenerateToken(user *User) (string, error) { func (s *AuthService) GenerateToken(user *User) (string, error) {
@@ -231,10 +231,6 @@ func (r *contentModerationTestUserRepo) DisableTotp(ctx context.Context, userID
panic("unexpected DisableTotp call") panic("unexpected DisableTotp call")
} }
func (r *contentModerationTestUserRepo) CountByRegistrationIP(ctx context.Context, ip string) (int, error) {
return 0, nil
}
type contentModerationTestAuthCacheInvalidator struct { type contentModerationTestAuthCacheInvalidator struct {
userIDs []int64 userIDs []int64
} }
@@ -14,11 +14,13 @@ import (
"math" "math"
mathrand "math/rand" mathrand "math/rand"
"net/http" "net/http"
"net/http/httptest"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/apicompat"
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey" "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
"github.com/Wei-Shaw/sub2api/internal/pkg/geminicli" "github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
"github.com/Wei-Shaw/sub2api/internal/pkg/googleapi" "github.com/Wei-Shaw/sub2api/internal/pkg/googleapi"
@@ -1090,6 +1092,223 @@ func (s *GeminiMessagesCompatService) Forward(ctx context.Context, c *gin.Contex
}, nil }, nil
} }
// ForwardAsChatCompletions accepts an OpenAI Chat Completions request, converts
// it through the existing Responses/Anthropic compatibility chain, forwards it
// to Gemini, then translates the captured Anthropic-compatible response back to
// Chat Completions format.
func (s *GeminiMessagesCompatService) ForwardAsChatCompletions(ctx context.Context, c *gin.Context, account *Account, body []byte) (*ForwardResult, error) {
startTime := time.Now()
var ccReq apicompat.ChatCompletionsRequest
if err := json.Unmarshal(body, &ccReq); err != nil {
writeGeminiChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
return nil, fmt.Errorf("parse chat completions request: %w", err)
}
if strings.TrimSpace(ccReq.Model) == "" {
writeGeminiChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", "model is required")
return nil, errors.New("model is required")
}
originalModel := ccReq.Model
clientStream := ccReq.Stream
includeUsage := ccReq.StreamOptions != nil && ccReq.StreamOptions.IncludeUsage
responsesReq, err := apicompat.ChatCompletionsToResponses(&ccReq)
if err != nil {
writeGeminiChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
return nil, fmt.Errorf("convert chat completions to responses: %w", err)
}
anthropicReq, err := apicompat.ResponsesToAnthropicRequest(responsesReq)
if err != nil {
writeGeminiChatCompletionsError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
return nil, fmt.Errorf("convert responses to anthropic: %w", err)
}
// ChatCompletionsToResponses intentionally forces upstream streaming for
// OpenAI/Codex paths. Gemini's Claude-compat forwarder writes the client
// protocol directly, so preserve the client's requested stream mode here.
anthropicReq.Stream = clientStream
anthropicBody, err := json.Marshal(anthropicReq)
if err != nil {
writeGeminiChatCompletionsError(c, http.StatusInternalServerError, "api_error", "Failed to build upstream request")
return nil, fmt.Errorf("marshal anthropic request: %w", err)
}
captureC, recorder := newCapturedGinContext(ctx, c)
result, err := s.Forward(ctx, captureC, account, anthropicBody)
if err != nil {
var failoverErr *UpstreamFailoverError
if errors.As(err, &failoverErr) {
return nil, err
}
writeCapturedClaudeErrorAsChatCompletions(c, recorder)
return nil, err
}
if recorder.Code >= 400 {
writeCapturedClaudeErrorAsChatCompletions(c, recorder)
return nil, fmt.Errorf("gemini chat completions upstream failed with status %d", recorder.Code)
}
if clientStream {
if err := writeCapturedAnthropicStreamAsChatCompletions(c, recorder, originalModel, includeUsage); err != nil {
return nil, err
}
} else {
if err := writeCapturedAnthropicMessageAsChatCompletions(c, recorder, originalModel); err != nil {
return nil, err
}
}
if result == nil {
result = &ForwardResult{}
}
result.Model = originalModel
result.Stream = clientStream
result.Duration = time.Since(startTime)
return result, nil
}
func newCapturedGinContext(ctx context.Context, src *gin.Context) (*gin.Context, *httptest.ResponseRecorder) {
recorder := httptest.NewRecorder()
captureC, _ := gin.CreateTestContext(recorder)
if src != nil {
captureC.Params = src.Params
if src.Request != nil {
captureC.Request = src.Request.Clone(ctx)
}
}
if captureC.Request == nil {
captureC.Request, _ = http.NewRequestWithContext(ctx, http.MethodPost, "/v1/messages", nil)
}
return captureC, recorder
}
func writeGeminiChatCompletionsError(c *gin.Context, status int, errType, message string) {
if c == nil || c.Writer.Written() {
return
}
c.JSON(status, gin.H{
"error": gin.H{
"type": errType,
"message": message,
},
})
}
func writeCapturedClaudeErrorAsChatCompletions(c *gin.Context, recorder *httptest.ResponseRecorder) {
if c == nil || c.Writer.Written() {
return
}
status := recorder.Code
if status < 400 {
status = http.StatusBadGateway
}
body := recorder.Body.Bytes()
errType := strings.TrimSpace(gjson.GetBytes(body, "error.type").String())
if errType == "" {
errType = "server_error"
}
message := strings.TrimSpace(gjson.GetBytes(body, "error.message").String())
if message == "" {
message = strings.TrimSpace(gjson.GetBytes(body, "message").String())
}
if message == "" {
message = "Upstream request failed"
}
writeGeminiChatCompletionsError(c, status, errType, message)
}
func writeCapturedAnthropicMessageAsChatCompletions(c *gin.Context, recorder *httptest.ResponseRecorder, originalModel string) error {
var anthropicResp apicompat.AnthropicResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &anthropicResp); err != nil {
writeGeminiChatCompletionsError(c, http.StatusBadGateway, "server_error", "Failed to parse upstream response")
return fmt.Errorf("parse captured anthropic response: %w", err)
}
responsesResp := apicompat.AnthropicToResponsesResponse(&anthropicResp)
ccResp := apicompat.ResponsesToChatCompletions(responsesResp, originalModel)
c.JSON(http.StatusOK, ccResp)
return nil
}
func writeCapturedAnthropicStreamAsChatCompletions(c *gin.Context, recorder *httptest.ResponseRecorder, originalModel string, includeUsage bool) error {
c.Writer.Header().Set("Content-Type", "text/event-stream")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Header().Set("X-Accel-Buffering", "no")
c.Writer.WriteHeader(http.StatusOK)
anthState := apicompat.NewAnthropicEventToResponsesState()
anthState.Model = originalModel
ccState := apicompat.NewResponsesEventToChatState()
ccState.Model = originalModel
ccState.IncludeUsage = includeUsage
writeChunk := func(chunk apicompat.ChatCompletionsChunk) error {
sse, err := apicompat.ChatChunkToSSE(chunk)
if err != nil {
return err
}
_, err = io.WriteString(c.Writer, sse)
return err
}
scanner := bufio.NewScanner(bytes.NewReader(recorder.Body.Bytes()))
scanner.Buffer(make([]byte, 0, 64*1024), defaultMaxLineSize)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "event: ") {
continue
}
if !scanner.Scan() {
break
}
dataLine := scanner.Text()
if !strings.HasPrefix(dataLine, "data: ") {
continue
}
var event apicompat.AnthropicStreamEvent
if err := json.Unmarshal([]byte(dataLine[6:]), &event); err != nil {
continue
}
responsesEvents := apicompat.AnthropicEventToResponsesEvents(&event, anthState)
for _, resEvt := range responsesEvents {
ccChunks := apicompat.ResponsesEventToChatChunks(&resEvt, ccState)
for _, chunk := range ccChunks {
if err := writeChunk(chunk); err != nil {
return err
}
}
}
c.Writer.Flush()
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("read captured anthropic stream: %w", err)
}
for _, resEvt := range apicompat.FinalizeAnthropicResponsesStream(anthState) {
for _, chunk := range apicompat.ResponsesEventToChatChunks(&resEvt, ccState) {
if err := writeChunk(chunk); err != nil {
return err
}
}
}
for _, chunk := range apicompat.FinalizeResponsesChatStream(ccState) {
if err := writeChunk(chunk); err != nil {
return err
}
}
if _, err := io.WriteString(c.Writer, "data: [DONE]\n\n"); err != nil {
return err
}
c.Writer.Flush()
return nil
}
func isGeminiSignatureRelatedError(respBody []byte) bool { func isGeminiSignatureRelatedError(respBody []byte) bool {
msg := strings.ToLower(strings.TrimSpace(extractAntigravityErrorMessage(respBody))) msg := strings.ToLower(strings.TrimSpace(extractAntigravityErrorMessage(respBody)))
if msg == "" { if msg == "" {
@@ -261,6 +261,68 @@ func TestGeminiMessagesCompatServiceForward_PreservesRequestedModelAndMappedUpst
require.Contains(t, httpStub.lastReq.URL.String(), "/models/claude-sonnet-4-20250514:") require.Contains(t, httpStub.lastReq.URL.String(), "/models/claude-sonnet-4-20250514:")
} }
func TestGeminiMessagesCompatServiceForwardAsChatCompletions_NonStreaming(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
httpStub := &geminiCompatHTTPUpstreamStub{
response: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"x-request-id": []string{"gemini-cc-1"}},
Body: io.NopCloser(strings.NewReader(`{"candidates":[{"content":{"parts":[{"text":"OK"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":7,"candidatesTokenCount":1}}`)),
},
}
svc := &GeminiMessagesCompatService{httpUpstream: httpStub, cfg: &config.Config{}}
account := &Account{
ID: 1,
Platform: PlatformGemini,
Type: AccountTypeAPIKey,
Credentials: map[string]any{
"api_key": "test-key",
},
}
body := []byte(`{"model":"gemini-2.5-flash","messages":[{"role":"user","content":"Return OK"}],"max_tokens":64}`)
result, err := svc.ForwardAsChatCompletions(context.Background(), c, account, body)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, "gemini-2.5-flash", result.Model)
require.Equal(t, "gemini-2.5-flash", result.UpstreamModel)
require.False(t, result.Stream)
require.Equal(t, 1, httpStub.calls)
require.NotNil(t, httpStub.lastReq)
require.Contains(t, httpStub.lastReq.URL.String(), "/v1beta/models/gemini-2.5-flash:generateContent")
var out struct {
Object string `json:"object"`
Model string `json:"model"`
Choices []struct {
Message struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &out))
require.Equal(t, "chat.completion", out.Object)
require.Equal(t, "gemini-2.5-flash", out.Model)
require.Len(t, out.Choices, 1)
require.Equal(t, "assistant", out.Choices[0].Message.Role)
require.Equal(t, "OK", out.Choices[0].Message.Content)
require.Equal(t, "stop", out.Choices[0].FinishReason)
require.Equal(t, 7, out.Usage.PromptTokens)
require.Equal(t, 1, out.Usage.CompletionTokens)
require.Equal(t, 8, out.Usage.TotalTokens)
}
func TestGeminiMessagesCompatServiceForward_NormalizesWebSearchToolForAIStudio(t *testing.T) { func TestGeminiMessagesCompatServiceForward_NormalizesWebSearchToolForAIStudio(t *testing.T) {
gin.SetMode(gin.TestMode) gin.SetMode(gin.TestMode)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@@ -62,16 +62,16 @@ func (p *GeminiTokenProvider) GetAccessToken(ctx context.Context, account *Accou
cacheKey := GeminiTokenCacheKey(account) cacheKey := GeminiTokenCacheKey(account)
// 1) Try cache first — skip if token is already expired or within refresh skew. // 1) Try cache first.
expiresAt := account.GetCredentialAsTime("expires_at") if p.tokenCache != nil {
needsRefresh := expiresAt == nil || time.Until(*expiresAt) <= geminiTokenRefreshSkew
if !needsRefresh && p.tokenCache != nil {
if token, err := p.tokenCache.GetAccessToken(ctx, cacheKey); err == nil && strings.TrimSpace(token) != "" { if token, err := p.tokenCache.GetAccessToken(ctx, cacheKey); err == nil && strings.TrimSpace(token) != "" {
return token, nil return token, nil
} }
} }
// 2) Refresh if needed (pre-expiry skew). // 2) Refresh if needed (pre-expiry skew).
expiresAt := account.GetCredentialAsTime("expires_at")
needsRefresh := expiresAt == nil || time.Until(*expiresAt) <= geminiTokenRefreshSkew
if needsRefresh && p.refreshAPI != nil && p.executor != nil { if needsRefresh && p.refreshAPI != nil && p.executor != nil {
result, err := p.refreshAPI.RefreshIfNeeded(ctx, account, p.executor, geminiTokenRefreshSkew) result, err := p.refreshAPI.RefreshIfNeeded(ctx, account, p.executor, geminiTokenRefreshSkew)
+10 -11
View File
@@ -38,7 +38,7 @@ type UpdateCache interface {
SetUpdateInfo(ctx context.Context, data string, ttl time.Duration) error SetUpdateInfo(ctx context.Context, data string, ttl time.Duration) error
} }
// GitHubReleaseClient 获取 GitHub release 信息的接口 // GitHubReleaseClient 获取代码仓库 release 信息的接口
type GitHubReleaseClient interface { type GitHubReleaseClient interface {
FetchLatestRelease(ctx context.Context, repo string) (*GitHubRelease, error) FetchLatestRelease(ctx context.Context, repo string) (*GitHubRelease, error)
DownloadFile(ctx context.Context, url, dest string, maxSize int64) error DownloadFile(ctx context.Context, url, dest string, maxSize int64) error
@@ -81,7 +81,7 @@ type UpdateInfo struct {
GitHubRepo string `json:"github_repo"` GitHubRepo string `json:"github_repo"`
} }
// ReleaseInfo contains GitHub release details // ReleaseInfo contains repository release details
type ReleaseInfo struct { type ReleaseInfo struct {
Name string `json:"name"` Name string `json:"name"`
Body string `json:"body"` Body string `json:"body"`
@@ -97,7 +97,7 @@ type Asset struct {
Size int64 `json:"size"` Size int64 `json:"size"`
} }
// GitHubRelease represents GitHub API response // GitHubRelease represents repository release API response
type GitHubRelease struct { type GitHubRelease struct {
TagName string `json:"tag_name"` TagName string `json:"tag_name"`
Name string `json:"name"` Name string `json:"name"`
@@ -122,7 +122,7 @@ func (s *UpdateService) CheckUpdate(ctx context.Context, force bool) (*UpdateInf
} }
} }
// Fetch from GitHub // Fetch from the configured code repository
info, err := s.fetchLatestRelease(ctx) info, err := s.fetchLatestRelease(ctx)
if err != nil { if err != nil {
// Return cached on error // Return cached on error
@@ -325,22 +325,21 @@ func (s *UpdateService) getArchiveName() string {
return fmt.Sprintf("%s_%s", osName, arch) return fmt.Sprintf("%s_%s", osName, arch)
} }
// validateDownloadURL checks if the URL is from an allowed domain // validateDownloadURL checks if the URL is from an allowed domain.
// SECURITY: This prevents SSRF and ensures downloads only come from trusted GitHub domains // SECURITY: This prevents SSRF and ensures downloads only come from trusted release hosts.
func validateDownloadURL(rawURL string) error { func validateDownloadURL(rawURL string) error {
parsedURL, err := url.Parse(rawURL) parsedURL, err := url.Parse(rawURL)
if err != nil { if err != nil {
return fmt.Errorf("invalid URL: %w", err) return fmt.Errorf("invalid URL: %w", err)
} }
// Must be HTTPS // The self-hosted Gitea instance currently serves releases over HTTP.
if parsedURL.Scheme != "https" { if parsedURL.Scheme != "https" && parsedURL.Scheme != "http" {
return fmt.Errorf("only HTTPS URLs are allowed") return fmt.Errorf("only HTTP(S) URLs are allowed")
} }
// Check against allowed hosts // Check against allowed hosts
host := parsedURL.Host host := parsedURL.Hostname()
// GitHub release URLs can be from github.com or objects.githubusercontent.com
if host != allowedDownloadHost && if host != allowedDownloadHost &&
!strings.HasSuffix(host, "."+allowedDownloadHost) && !strings.HasSuffix(host, "."+allowedDownloadHost) &&
host != allowedAssetHost && host != allowedAssetHost &&
-3
View File
@@ -111,9 +111,6 @@ type UserRepository interface {
UpdateTotpSecret(ctx context.Context, userID int64, encryptedSecret *string) error UpdateTotpSecret(ctx context.Context, userID int64, encryptedSecret *string) error
EnableTotp(ctx context.Context, userID int64) error EnableTotp(ctx context.Context, userID int64) error
DisableTotp(ctx context.Context, userID int64) error DisableTotp(ctx context.Context, userID int64) error
// CountByRegistrationIP 统计指定 IP 注册的用户数量
CountByRegistrationIP(ctx context.Context, ip string) (int, error)
} }
type UserAuthIdentityRecord struct { type UserAuthIdentityRecord struct {
+5 -5
View File
@@ -401,12 +401,12 @@ OPS_ENABLED=true
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Update Configuration (在线更新配置) # Update Configuration (在线更新配置)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# GitHub repository used for online update checks and binary downloads # code repository used for online update checks and binary downloads
# 在线更新检查和二进制下载使用的 GitHub 仓库 # 在线更新检查和二进制下载使用的 代码仓库
UPDATE_GITHUB_REPO=man209111-cpu/sub2api UPDATE_GITHUB_REPO=kgod/sub2api
# Proxy URL for accessing GitHub (used for online updates and pricing data) # Proxy URL for repository update checks and pricing data
# 用于访问 GitHub 的代理地址(用于在线更新和定价数据获取) # 用于访问代码仓库更新接口和定价数据的代理地址
# Supports: http, https, socks5, socks5h # Supports: http, https, socks5, socks5h
# Examples: # Examples:
# HTTP proxy: http://127.0.0.1:7890 # HTTP proxy: http://127.0.0.1:7890
+4 -4
View File
@@ -10,7 +10,7 @@ docker run -d \
-p 8080:8080 \ -p 8080:8080 \
-e DATABASE_URL="postgres://user:pass@host:5432/sub2api" \ -e DATABASE_URL="postgres://user:pass@host:5432/sub2api" \
-e REDIS_URL="redis://host:6379" \ -e REDIS_URL="redis://host:6379" \
weishaw/sub2api:latest git.jianshixingqiu.com/kgod/sub2api:latest
``` ```
## Docker Compose ## Docker Compose
@@ -20,7 +20,7 @@ version: '3.8'
services: services:
sub2api: sub2api:
image: weishaw/sub2api:latest image: git.jianshixingqiu.com/kgod/sub2api:latest
ports: ports:
- "8080:8080" - "8080:8080"
environment: environment:
@@ -72,5 +72,5 @@ volumes:
## Links ## Links
- [GitHub Repository](https://github.com/weishaw/sub2api) - [Gitea Repository](http://git.jianshixingqiu.com/kgod/sub2api)
- [Documentation](https://github.com/weishaw/sub2api#readme) - [Documentation](http://git.jianshixingqiu.com/kgod/sub2api#readme)
+2 -2
View File
@@ -73,9 +73,9 @@ RUN CGO_ENABLED=0 GOOS=linux go build \
FROM ${ALPINE_IMAGE} FROM ${ALPINE_IMAGE}
# Labels # Labels
LABEL maintainer="Wei-Shaw <github.com/Wei-Shaw>" LABEL maintainer="kgod <http://git.jianshixingqiu.com/kgod/sub2api>"
LABEL description="Sub2API - AI API Gateway Platform" LABEL description="Sub2API - AI API Gateway Platform"
LABEL org.opencontainers.image.source="https://github.com/Wei-Shaw/sub2api" LABEL org.opencontainers.image.source="http://git.jianshixingqiu.com/kgod/sub2api"
# Install runtime dependencies # Install runtime dependencies
RUN apk add --no-cache \ RUN apk add --no-cache \
+5 -5
View File
@@ -35,10 +35,10 @@ Use the automated preparation script for the easiest setup:
```bash ```bash
# Download and run the preparation script # Download and run the preparation script
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/docker-deploy.sh | bash curl -sSL http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy/docker-deploy.sh | bash
# Or download first, then run # Or download first, then run
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/docker-deploy.sh -o docker-deploy.sh curl -sSL http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy/docker-deploy.sh -o docker-deploy.sh
chmod +x docker-deploy.sh chmod +x docker-deploy.sh
./docker-deploy.sh ./docker-deploy.sh
``` ```
@@ -71,7 +71,7 @@ If you prefer manual control:
```bash ```bash
# Clone repository # Clone repository
git clone https://github.com/Wei-Shaw/sub2api.git git clone http://git.jianshixingqiu.com/kgod/sub2api.git
cd sub2api/deploy cd sub2api/deploy
# Configure environment # Configure environment
@@ -353,12 +353,12 @@ For production servers using systemd.
### One-Line Installation ### One-Line Installation
```bash ```bash
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash curl -sSL http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy/install.sh | sudo bash
``` ```
### Manual Installation ### Manual Installation
1. Download the latest release from [GitHub Releases](https://github.com/Wei-Shaw/sub2api/releases) 1. Download the latest release from [Gitea Releases](http://git.jianshixingqiu.com/kgod/sub2api/releases)
2. Extract and copy the binary to `/opt/sub2api/` 2. Extract and copy the binary to `/opt/sub2api/`
3. Copy `sub2api.service` to `/etc/systemd/system/` 3. Copy `sub2api.service` to `/etc/systemd/system/`
4. Run: 4. Run:
+6 -6
View File
@@ -4,7 +4,7 @@
# Copy this file to /etc/sub2api/config.yaml and modify as needed # Copy this file to /etc/sub2api/config.yaml and modify as needed
# 复制此文件到 /etc/sub2api/config.yaml 并根据需要修改 # 复制此文件到 /etc/sub2api/config.yaml 并根据需要修改
# #
# Documentation / 文档: https://github.com/Wei-Shaw/sub2api # Documentation / 文档: http://git.jianshixingqiu.com/kgod/sub2api
# ============================================================================= # =============================================================================
# Server Configuration # Server Configuration
@@ -1093,11 +1093,11 @@ gemini:
# Update Configuration (在线更新配置) # Update Configuration (在线更新配置)
# ============================================================================= # =============================================================================
update: update:
# GitHub repository used for online update checks and binary downloads. # code repository used for online update checks and binary downloads.
# 在线更新检查和二进制下载使用的 GitHub 仓库。 # 在线更新检查和二进制下载使用的 代码仓库。
github_repo: "man209111-cpu/sub2api" github_repo: "kgod/sub2api"
# Proxy URL for accessing GitHub (used for online updates and pricing data) # Proxy URL for repository update checks and pricing data
# 用于访问 GitHub 的代理地址(用于在线更新和定价数据获取) # 用于访问代码仓库更新接口和定价数据的代理地址
# Supports: http, https, socks5, socks5h # Supports: http, https, socks5, socks5h
# Examples: # Examples:
# - HTTP proxy: "http://127.0.0.1:7890" # - HTTP proxy: "http://127.0.0.1:7890"
+4 -4
View File
@@ -24,7 +24,7 @@ services:
# Sub2API Application # Sub2API Application
# =========================================================================== # ===========================================================================
sub2api: sub2api:
image: ghcr.io/man209111-cpu/sub2api:latest image: git.jianshixingqiu.com/kgod/sub2api:latest
container_name: sub2api container_name: sub2api
restart: unless-stopped restart: unless-stopped
ulimits: ulimits:
@@ -144,9 +144,9 @@ services:
# ======================================================================= # =======================================================================
# Update Configuration (在线更新配置) # Update Configuration (在线更新配置)
# ======================================================================= # =======================================================================
# GitHub repo for online update checks and binary downloads # code repository for online update checks and binary downloads
- UPDATE_GITHUB_REPO=${UPDATE_GITHUB_REPO:-man209111-cpu/sub2api} - UPDATE_GITHUB_REPO=${UPDATE_GITHUB_REPO:-kgod/sub2api}
# Proxy for accessing GitHub (online updates + pricing data) # Proxy for repository update checks and pricing data
# Examples: http://host:port, socks5://host:port # Examples: http://host:port, socks5://host:port
- UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-socks5://admin%40sub2api.local:m729066849@172.16.32.16:3389} - UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-socks5://admin%40sub2api.local:m729066849@172.16.32.16:3389}
+2 -2
View File
@@ -12,7 +12,7 @@
services: services:
sub2api: sub2api:
image: weishaw/sub2api:latest image: git.jianshixingqiu.com/kgod/sub2api:latest
container_name: sub2api container_name: sub2api
restart: unless-stopped restart: unless-stopped
ulimits: ulimits:
@@ -92,7 +92,7 @@ services:
# ======================================================================= # =======================================================================
# Update Configuration (在线更新配置) # Update Configuration (在线更新配置)
# ======================================================================= # =======================================================================
- UPDATE_GITHUB_REPO=${UPDATE_GITHUB_REPO:-man209111-cpu/sub2api} - UPDATE_GITHUB_REPO=${UPDATE_GITHUB_REPO:-kgod/sub2api}
- UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-socks5://admin%40sub2api.local:m729066849@172.16.32.16:3389} - UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-socks5://admin%40sub2api.local:m729066849@172.16.32.16:3389}
# Built-in OAuth client secrets (optional) # Built-in OAuth client secrets (optional)
+4 -4
View File
@@ -16,7 +16,7 @@ services:
# Sub2API Application # Sub2API Application
# =========================================================================== # ===========================================================================
sub2api: sub2api:
image: ghcr.io/man209111-cpu/sub2api:latest image: git.jianshixingqiu.com/kgod/sub2api:latest
container_name: sub2api container_name: sub2api
restart: unless-stopped restart: unless-stopped
ulimits: ulimits:
@@ -140,9 +140,9 @@ services:
# ======================================================================= # =======================================================================
# Update Configuration (在线更新配置) # Update Configuration (在线更新配置)
# ======================================================================= # =======================================================================
# GitHub repo for online update checks and binary downloads # code repository for online update checks and binary downloads
- UPDATE_GITHUB_REPO=${UPDATE_GITHUB_REPO:-man209111-cpu/sub2api} - UPDATE_GITHUB_REPO=${UPDATE_GITHUB_REPO:-kgod/sub2api}
# Proxy for accessing GitHub (online updates + pricing data) # Proxy for repository update checks and pricing data
# Examples: http://host:port, socks5://host:port # Examples: http://host:port, socks5://host:port
- UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-socks5://admin%40sub2api.local:m729066849@172.16.32.16:3389} - UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-socks5://admin%40sub2api.local:m729066849@172.16.32.16:3389}
+6 -6
View File
@@ -20,8 +20,8 @@ YELLOW='\033[1;33m'
BLUE='\033[0;34m' BLUE='\033[0;34m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# GitHub raw content base URL # Gitea raw content base URL
GITHUB_RAW_URL="https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy" REPO_RAW_URL="http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy"
# Print colored message # Print colored message
print_info() { print_info() {
@@ -78,9 +78,9 @@ main() {
# Download docker-compose.local.yml and save as docker-compose.yml # Download docker-compose.local.yml and save as docker-compose.yml
print_info "Downloading docker-compose.yml..." print_info "Downloading docker-compose.yml..."
if command_exists curl; then if command_exists curl; then
curl -sSL "${GITHUB_RAW_URL}/docker-compose.local.yml" -o docker-compose.yml curl -sSL "${REPO_RAW_URL}/docker-compose.local.yml" -o docker-compose.yml
elif command_exists wget; then elif command_exists wget; then
wget -q "${GITHUB_RAW_URL}/docker-compose.local.yml" -O docker-compose.yml wget -q "${REPO_RAW_URL}/docker-compose.local.yml" -O docker-compose.yml
else else
print_error "Neither curl nor wget is installed. Please install one of them." print_error "Neither curl nor wget is installed. Please install one of them."
exit 1 exit 1
@@ -90,9 +90,9 @@ main() {
# Download .env.example # Download .env.example
print_info "Downloading .env.example..." print_info "Downloading .env.example..."
if command_exists curl; then if command_exists curl; then
curl -sSL "${GITHUB_RAW_URL}/.env.example" -o .env.example curl -sSL "${REPO_RAW_URL}/.env.example" -o .env.example
else else
wget -q "${GITHUB_RAW_URL}/.env.example" -O .env.example wget -q "${REPO_RAW_URL}/.env.example" -O .env.example
fi fi
print_success "Downloaded .env.example" print_success "Downloaded .env.example"
+24 -11
View File
@@ -2,7 +2,7 @@
# #
# Sub2API Installation Script # Sub2API Installation Script
# Sub2API 安装脚本 # Sub2API 安装脚本
# Usage: curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | bash # Usage: curl -sSL http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/deploy/install.sh | bash
# #
set -e set -e
@@ -15,8 +15,10 @@ BLUE='\033[0;34m'
CYAN='\033[0;36m' CYAN='\033[0;36m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# Configuration # Repository configuration
GITHUB_REPO="Wei-Shaw/sub2api" GITEA_BASE_URL="http://git.jianshixingqiu.com"
GITEA_API_URL="${GITEA_BASE_URL}/api/v1"
GITEA_REPO="kgod/sub2api"
INSTALL_DIR="/opt/sub2api" INSTALL_DIR="/opt/sub2api"
SERVICE_NAME="sub2api" SERVICE_NAME="sub2api"
SERVICE_USER="sub2api" SERVICE_USER="sub2api"
@@ -468,7 +470,7 @@ check_dependencies() {
# Get latest release version # Get latest release version
get_latest_version() { get_latest_version() {
print_info "$(msg 'fetching_version')" print_info "$(msg 'fetching_version')"
LATEST_VERSION=$(curl -s --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') LATEST_VERSION=$(curl -s --connect-timeout 10 --max-time 30 "${GITEA_API_URL}/repos/${GITEA_REPO}/releases/latest" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
if [ -z "$LATEST_VERSION" ]; then if [ -z "$LATEST_VERSION" ]; then
print_error "$(msg 'failed_get_version')" print_error "$(msg 'failed_get_version')"
@@ -484,7 +486,7 @@ list_versions() {
print_info "$(msg 'fetching_versions')" print_info "$(msg 'fetching_versions')"
local versions local versions
versions=$(curl -s --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/' | head -20) versions=$(curl -s --connect-timeout 10 --max-time 30 "${GITEA_API_URL}/repos/${GITEA_REPO}/releases" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/' | head -20)
if [ -z "$versions" ]; then if [ -z "$versions" ]; then
print_error "$(msg 'failed_get_version')" print_error "$(msg 'failed_get_version')"
@@ -521,11 +523,11 @@ validate_version() {
# Check if the release exists # Check if the release exists
local http_code local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 30 "https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${version}" 2>/dev/null) http_code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 10 --max-time 30 "${GITEA_API_URL}/repos/${GITEA_REPO}/releases/tags/${version}" 2>/dev/null)
# Check for network errors (empty or non-numeric response) # Check for network errors (empty or non-numeric response)
if [ -z "$http_code" ] || ! [[ "$http_code" =~ ^[0-9]+$ ]]; then if [ -z "$http_code" ] || ! [[ "$http_code" =~ ^[0-9]+$ ]]; then
print_error "Network error: Failed to connect to GitHub API" >&2 print_error "Network error: Failed to connect to Gitea API" >&2
exit 1 exit 1
fi fi
@@ -554,8 +556,19 @@ get_current_version() {
download_and_extract() { download_and_extract() {
local version_num=${LATEST_VERSION#v} local version_num=${LATEST_VERSION#v}
local archive_name="sub2api_${version_num}_${OS}_${ARCH}.tar.gz" local archive_name="sub2api_${version_num}_${OS}_${ARCH}.tar.gz"
local download_url="https://github.com/${GITHUB_REPO}/releases/download/${LATEST_VERSION}/${archive_name}" local release_json
local checksum_url="https://github.com/${GITHUB_REPO}/releases/download/${LATEST_VERSION}/checksums.txt" local download_url
local checksum_url
release_json=$(curl -s --connect-timeout 10 --max-time 30 "${GITEA_API_URL}/repos/${GITEA_REPO}/releases/tags/${LATEST_VERSION}" 2>/dev/null || true)
download_url=$(printf '%s' "$release_json" | tr '{' '\n' | grep "\"name\"[[:space:]]*:[[:space:]]*\"${archive_name}\"" | sed -n -E 's/.*"browser_download_url"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' | head -1)
checksum_url=$(printf '%s' "$release_json" | tr '{' '\n' | grep '"name"[[:space:]]*:[[:space:]]*"checksums.txt"' | sed -n -E 's/.*"browser_download_url"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' | head -1)
if [ -z "$download_url" ]; then
print_error "$(msg 'download_failed'): ${archive_name}"
print_info "No matching release asset found at ${GITEA_BASE_URL}/${GITEA_REPO}/releases/tag/${LATEST_VERSION}"
exit 1
fi
print_info "$(msg 'downloading') ${archive_name}..." print_info "$(msg 'downloading') ${archive_name}..."
@@ -571,7 +584,7 @@ download_and_extract() {
# Download and verify checksum # Download and verify checksum
print_info "$(msg 'verifying_checksum')" print_info "$(msg 'verifying_checksum')"
if curl -sL "$checksum_url" -o "$TEMP_DIR/checksums.txt" 2>/dev/null; then if [ -n "$checksum_url" ] && curl -sL "$checksum_url" -o "$TEMP_DIR/checksums.txt" 2>/dev/null; then
local expected_checksum=$(grep "$archive_name" "$TEMP_DIR/checksums.txt" | awk '{print $1}') local expected_checksum=$(grep "$archive_name" "$TEMP_DIR/checksums.txt" | awk '{print $1}')
local actual_checksum=$(sha256sum "$TEMP_DIR/$archive_name" | awk '{print $1}') local actual_checksum=$(sha256sum "$TEMP_DIR/$archive_name" | awk '{print $1}')
@@ -655,7 +668,7 @@ install_service() {
cat > /etc/systemd/system/sub2api.service << EOF cat > /etc/systemd/system/sub2api.service << EOF
[Unit] [Unit]
Description=Sub2API - AI API Gateway Platform Description=Sub2API - AI API Gateway Platform
Documentation=https://github.com/Wei-Shaw/sub2api Documentation=http://git.jianshixingqiu.com/kgod/sub2api
After=network.target postgresql.service redis.service After=network.target postgresql.service redis.service
Wants=postgresql.service redis.service Wants=postgresql.service redis.service
+4 -4
View File
@@ -119,8 +119,8 @@ https://pay.example.com/pay?user_id=123&token=<jwt>&theme=light&lang=zh&ui_mode=
- 重试保持相同 `code`,并使用新的 `Idempotency-Key` - 重试保持相同 `code`,并使用新的 `Idempotency-Key`
### 6) `doc_url` 配置建议 ### 6) `doc_url` 配置建议
- 查看链接:`https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md` - 查看链接:`http://git.jianshixingqiu.com/kgod/sub2api/src/branch/main/ADMIN_PAYMENT_INTEGRATION_API.md`
- 下载链接:`https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/ADMIN_PAYMENT_INTEGRATION_API.md` - 下载链接:`http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/ADMIN_PAYMENT_INTEGRATION_API.md`
--- ---
@@ -239,5 +239,5 @@ https://pay.example.com/pay?user_id=123&token=<jwt>&theme=light&lang=zh&ui_mode=
- Keep the same `code` for retry, and use a new `Idempotency-Key` - Keep the same `code` for retry, and use a new `Idempotency-Key`
### 6) Recommended `doc_url` ### 6) Recommended `doc_url`
- View URL: `https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md` - View URL: `http://git.jianshixingqiu.com/kgod/sub2api/src/branch/main/ADMIN_PAYMENT_INTEGRATION_API.md`
- Download URL: `https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/ADMIN_PAYMENT_INTEGRATION_API.md` - Download URL: `http://git.jianshixingqiu.com/kgod/sub2api/raw/branch/main/ADMIN_PAYMENT_INTEGRATION_API.md`
+1 -1
View File
@@ -31,7 +31,7 @@ export async function getVersion(): Promise<{ version: string }> {
/** /**
* Check for updates * Check for updates
* @param force - Force refresh from GitHub API * @param force - Force refresh from Gitea API
*/ */
export async function checkUpdates(force = false): Promise<VersionInfo> { export async function checkUpdates(force = false): Promise<VersionInfo> {
const { data } = await apiClient.get<VersionInfo>('/admin/system/check-updates', { const { data } = await apiClient.get<VersionInfo>('/admin/system/check-updates', {
+1 -1
View File
@@ -128,7 +128,7 @@
<a <a
v-if="authStore.isAdmin" v-if="authStore.isAdmin"
href="https://github.com/Wei-Shaw/sub2api" href="http://git.jianshixingqiu.com/kgod/sub2api"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
@click="closeDropdown" @click="closeDropdown"
+1 -1
View File
@@ -467,7 +467,7 @@ const isHomeContentUrl = computed(() => {
const { isDarkTheme: isDark, toggleTheme } = useTheme() const { isDarkTheme: isDark, toggleTheme } = useTheme()
// GitHub URL // GitHub URL
const githubUrl = 'https://github.com/Wei-Shaw/sub2api' const githubUrl = 'http://git.jianshixingqiu.com/kgod/sub2api'
// Auth state // Auth state
const isAuthenticated = computed(() => authStore.isAuthenticated) const isAuthenticated = computed(() => authStore.isAuthenticated)
+1 -1
View File
@@ -376,7 +376,7 @@ const appStore = useAppStore()
const siteName = computed(() => appStore.cachedPublicSettings?.site_name || appStore.siteName || 'Sub2API') const siteName = computed(() => appStore.cachedPublicSettings?.site_name || appStore.siteName || 'Sub2API')
const siteLogo = computed(() => appStore.cachedPublicSettings?.site_logo || appStore.siteLogo || '') const siteLogo = computed(() => appStore.cachedPublicSettings?.site_logo || appStore.siteLogo || '')
const docUrl = computed(() => appStore.cachedPublicSettings?.doc_url || appStore.docUrl || '') const docUrl = computed(() => appStore.cachedPublicSettings?.doc_url || appStore.docUrl || '')
const githubUrl = 'https://github.com/Wei-Shaw/sub2api' const githubUrl = 'http://git.jianshixingqiu.com/kgod/sub2api'
// ==================== Theme (same as HomeView) ==================== // ==================== Theme (same as HomeView) ====================
+4 -4
View File
@@ -6571,14 +6571,14 @@ function applyCustomMenuIcon(item: { icon_svg: string }, optionID: string) {
const paymentGuideHref = computed(() => const paymentGuideHref = computed(() =>
locale.value.startsWith("zh") locale.value.startsWith("zh")
? "https://github.com/Wei-Shaw/sub2api/blob/main/docs/PAYMENT_CN.md" ? "http://git.jianshixingqiu.com/kgod/sub2api/src/branch/main/docs/PAYMENT_CN.md"
: "https://github.com/Wei-Shaw/sub2api/blob/main/docs/PAYMENT.md", : "http://git.jianshixingqiu.com/kgod/sub2api/src/branch/main/docs/PAYMENT.md",
); );
const paymentMethodsHref = computed(() => const paymentMethodsHref = computed(() =>
locale.value.startsWith("zh") locale.value.startsWith("zh")
? "https://github.com/Wei-Shaw/sub2api/blob/main/docs/PAYMENT_CN.md#支持的支付方式" ? "http://git.jianshixingqiu.com/kgod/sub2api/src/branch/main/docs/PAYMENT_CN.md#支持的支付方式"
: "https://github.com/Wei-Shaw/sub2api/blob/main/docs/PAYMENT.md#supported-payment-methods", : "http://git.jianshixingqiu.com/kgod/sub2api/src/branch/main/docs/PAYMENT.md#supported-payment-methods",
); );
type SettingsTab = type SettingsTab =
@@ -557,10 +557,10 @@ describe("admin SettingsView payment visible method controls", () => {
expect(paymentLinks).toHaveLength(2); expect(paymentLinks).toHaveLength(2);
expect(paymentLinks[0]?.attributes("href")).toBe( expect(paymentLinks[0]?.attributes("href")).toBe(
"https://github.com/Wei-Shaw/sub2api/blob/main/docs/PAYMENT_CN.md", "http://git.jianshixingqiu.com/kgod/sub2api/src/branch/main/docs/PAYMENT_CN.md",
); );
expect(paymentLinks[1]?.attributes("href")).toBe( expect(paymentLinks[1]?.attributes("href")).toBe(
"https://github.com/Wei-Shaw/sub2api/blob/main/docs/PAYMENT_CN.md#支持的支付方式", "http://git.jianshixingqiu.com/kgod/sub2api/src/branch/main/docs/PAYMENT_CN.md#支持的支付方式",
); );
for (const link of paymentLinks) { for (const link of paymentLinks) {
expect(link.attributes("href")).toContain("docs/PAYMENT"); expect(link.attributes("href")).toContain("docs/PAYMENT");