Prechádzať zdrojové kódy

Merge branch 'main' into feat_subscribe_sp1

Little Write 2 mesiacov pred
rodič
commit
ac3baacec7
100 zmenil súbory, kde vykonal 3767 pridanie a 723 odobranie
  1. 2 1
      .dockerignore
  2. 26 0
      .github/ISSUE_TEMPLATE/bug_report_en.md
  3. 22 0
      .github/ISSUE_TEMPLATE/feature_request_en.md
  4. 0 4
      .github/PULL_REQUEST_TEMPLATE/pull_request_template.md
  5. 108 19
      .github/workflows/docker-image-alpha.yml
  6. 104 22
      .github/workflows/docker-image-arm64.yml
  7. 141 0
      .github/workflows/electron-build.yml
  8. 0 59
      .github/workflows/linux-release.yml
  9. 0 51
      .github/workflows/macos-release.yml
  10. 136 0
      .github/workflows/release.yml
  11. 91 0
      .github/workflows/sync-to-gitee.yml
  12. 0 53
      .github/workflows/windows-release.yml
  13. 6 1
      .gitignore
  14. 8 6
      Dockerfile
  15. 29 20
      README.en.md
  16. 225 0
      README.fr.md
  17. 226 0
      README.ja.md
  18. 33 25
      README.md
  19. 5 1
      common/api_type.go
  20. 1 0
      common/constants.go
  21. 1 0
      common/crypto.go
  22. 1 1
      common/database.go
  23. 3 0
      common/email.go
  24. 2 1
      common/embed-file-system.go
  25. 2 1
      common/endpoint_defaults.go
  26. 3 1
      common/endpoint_type.go
  27. 26 1
      common/gin.go
  28. 2 1
      common/gopool.go
  29. 20 5
      common/init.go
  30. 2 1
      common/json.go
  31. 3 2
      common/limiter/limiter.go
  32. 2 1
      common/pprof.go
  33. 2 1
      common/verification.go
  34. 4 2
      constant/api_type.go
  35. 68 0
      constant/channel.go
  36. 2 0
      constant/endpoint_type.go
  37. 3 0
      constant/env.go
  38. 23 7
      controller/billing.go
  39. 15 6
      controller/channel-billing.go
  40. 238 141
      controller/channel-test.go
  41. 16 38
      controller/channel.go
  42. 3 2
      controller/console_migrate.go
  43. 3 2
      controller/github.go
  44. 4 3
      controller/group.go
  45. 3 2
      controller/linuxdo.go
  46. 3 2
      controller/log.go
  47. 8 7
      controller/midjourney.go
  48. 55 20
      controller/misc.go
  49. 2 1
      controller/missing_models.go
  50. 14 13
      controller/model.go
  51. 3 3
      controller/model_meta.go
  52. 2 2
      controller/model_sync.go
  53. 4 3
      controller/oidc.go
  54. 7 6
      controller/option.go
  55. 497 0
      controller/passkey.go
  56. 6 5
      controller/playground.go
  57. 2 2
      controller/prefill_group.go
  58. 3 3
      controller/pricing.go
  59. 2 1
      controller/ratio_config.go
  60. 5 4
      controller/ratio_sync.go
  61. 3 2
      controller/redemption.go
  62. 25 17
      controller/relay.go
  63. 314 0
      controller/secure_verification.go
  64. 7 6
      controller/setup.go
  65. 7 6
      controller/task.go
  66. 136 15
      controller/task_video.go
  67. 4 3
      controller/telegram.go
  68. 3 2
      controller/token.go
  69. 94 18
      controller/topup.go
  70. 17 14
      controller/topup_stripe.go
  71. 3 2
      controller/twofa.go
  72. 2 1
      controller/uptime_kuma.go
  73. 3 2
      controller/usedata.go
  74. 61 7
      controller/user.go
  75. 2 2
      controller/vendor_meta.go
  76. 130 0
      controller/video_proxy.go
  77. 3 2
      controller/wechat.go
  78. 51 19
      docker-compose.yml
  79. 107 0
      docs/translation-glossary.fr.md
  80. 86 0
      docs/translation-glossary.md
  81. 107 0
      docs/translation-glossary.ru.md
  82. 11 6
      dto/audio.go
  83. 19 0
      dto/channel_settings.go
  84. 17 8
      dto/claude.go
  85. 2 1
      dto/embedding.go
  86. 1 1
      dto/error.go
  87. 14 7
      dto/gemini.go
  88. 10 8
      dto/openai_image.go
  89. 35 15
      dto/openai_request.go
  90. 12 1
      dto/openai_response.go
  91. 52 0
      dto/openai_video.go
  92. 1 1
      dto/pricing.go
  93. 1 1
      dto/realtime.go
  94. 1 1
      dto/request_common.go
  95. 3 2
      dto/rerank.go
  96. 4 0
      dto/user_settings.go
  97. 73 0
      electron/README.md
  98. 41 0
      electron/build.sh
  99. 60 0
      electron/create-tray-icon.js
  100. 18 0
      electron/entitlements.mac.plist

+ 2 - 1
.dockerignore

@@ -5,4 +5,5 @@
 .gitignore
 Makefile
 docs
-.eslintcache
+.eslintcache
+.gocache

+ 26 - 0
.github/ISSUE_TEMPLATE/bug_report_en.md

@@ -0,0 +1,26 @@
+---
+name: Bug Report
+about: Describe the issue you encountered with clear and detailed language
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**Routine Checks**
+
+[//]: # (Remove the space in the box and fill with an x)
++ [ ] I have confirmed there are no similar issues currently
++ [ ] I have confirmed I have upgraded to the latest version
++ [ ] I have thoroughly read the project README, especially the FAQ section
++ [ ] I understand and am willing to follow up on this issue, assist with testing and provide feedback 
++ [ ] I understand and acknowledge the above, and understand that project maintainers have limited time and energy, **issues that do not follow the rules may be ignored or closed directly**
+
+**Issue Description**
+
+**Steps to Reproduce**
+
+**Expected Result**
+
+**Related Screenshots**
+If none, please delete this section.

+ 22 - 0
.github/ISSUE_TEMPLATE/feature_request_en.md

@@ -0,0 +1,22 @@
+---
+name: Feature Request
+about: Describe the new feature you would like to add with clear and detailed language
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Routine Checks**
+
+[//]: # (Remove the space in the box and fill with an x)
++ [ ] I have confirmed there are no similar issues currently
++ [ ] I have confirmed I have upgraded to the latest version
++ [ ] I have thoroughly read the project README and confirmed the current version cannot meet my needs
++ [ ] I understand and am willing to follow up on this issue, assist with testing and provide feedback
++ [ ] I understand and acknowledge the above, and understand that project maintainers have limited time and energy, **issues that do not follow the rules may be ignored or closed directly**
+
+**Feature Description**
+
+**Use Case**
+

+ 0 - 4
.github/PULL_REQUEST_TEMPLATE/pull_request_template.md

@@ -13,7 +13,3 @@
 ### PR 描述
 
 **请在下方详细描述您的 PR,包括目的、实现细节等。**
-
-### **重要提示**
-
-**所有 PR 都必须提交到 `alpha` 分支。请确保您的 PR 目标分支是 `alpha`。**

+ 108 - 19
.github/workflows/docker-image-alpha.yml

@@ -11,19 +11,42 @@ on:
         required: false
 
 jobs:
-  push_to_registries:
-    name: Push Docker image to multiple registries
-    runs-on: ubuntu-latest
+  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
     steps:
-      - name: Check out the repo
+      - name: Check out (shallow)
         uses: actions/checkout@v4
+        with:
+          fetch-depth: 1
 
-      - name: Save version info
+      - name: Determine alpha version
+        id: version
         run: |
-          echo "alpha-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)" > VERSION
+          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@v3
 
       - name: Log in to Docker Hub
         uses: docker/login-action@v3
@@ -31,32 +54,98 @@ jobs:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
 
-      - name: Log in to the Container registry
+      - name: Log in to GHCR
         uses: docker/login-action@v3
         with:
           registry: ghcr.io
           username: ${{ github.actor }}
           password: ${{ secrets.GITHUB_TOKEN }}
 
-      - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v3
-
-      - name: Extract metadata (tags, labels) for Docker
+      - name: Extract metadata (labels)
         id: meta
         uses: docker/metadata-action@v5
         with:
           images: |
             calciumion/new-api
-            ghcr.io/${{ github.repository }}
-          tags: |
-            type=raw,value=alpha
-            type=raw,value=alpha-{{date 'YYYYMMDD'}}-{{sha}}
+            ghcr.io/${{ env.GHCR_REPOSITORY }}
 
-      - name: Build and push Docker images
-        uses: docker/build-push-action@v5
+      - name: Build & push single-arch (to both registries)
+        uses: docker/build-push-action@v6
         with:
           context: .
-          platforms: linux/amd64,linux/arm64
+          platforms: ${{ matrix.platform }}
           push: true
-          tags: ${{ steps.meta.outputs.tags }}
+          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: false
+          sbom: false
+
+  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@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@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@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

+ 104 - 22
.github/workflows/docker-image-arm64.yml

@@ -1,26 +1,46 @@
-name: Publish Docker image (Multi Registries)
+name: Publish Docker image (Multi Registries, native amd64+arm64)
 
 on:
   push:
     tags:
       - '*'
+
 jobs:
-  push_to_registries:
-    name: Push Docker image to multiple registries
-    runs-on: ubuntu-latest
+  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
+
     steps:
-      - name: Check out the repo
+      - name: Check out (shallow)
         uses: actions/checkout@v4
+        with:
+          fetch-depth: 1
 
-      - name: Save version info
+      - name: Resolve tag & write VERSION
         run: |
-          git describe --tags > VERSION 
+          git fetch --tags --force --depth=1
+          TAG=${GITHUB_REF#refs/tags/}
+          echo "TAG=$TAG" >> $GITHUB_ENV
+          echo "$TAG" > VERSION
+          echo "Building tag: $TAG for ${{ matrix.arch }}"
+
 
-      - name: Set up QEMU
-        uses: docker/setup-qemu-action@v3
+#      - name: Normalize GHCR repository
+#        run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
 
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v3
@@ -31,26 +51,88 @@ jobs:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
 
-      - name: Log in to the Container registry
-        uses: docker/login-action@v3
-        with:
-          registry: ghcr.io
-          username: ${{ github.actor }}
-          password: ${{ secrets.GITHUB_TOKEN }}
+#      - name: Log in to GHCR
+#        uses: docker/login-action@v3
+#        with:
+#          registry: ghcr.io
+#          username: ${{ github.actor }}
+#          password: ${{ secrets.GITHUB_TOKEN }}
 
-      - name: Extract metadata (tags, labels) for Docker
+      - name: Extract metadata (labels)
         id: meta
         uses: docker/metadata-action@v5
         with:
           images: |
             calciumion/new-api
-            ghcr.io/${{ github.repository }}
+#            ghcr.io/${{ env.GHCR_REPOSITORY }}
 
-      - name: Build and push Docker images
-        uses: docker/build-push-action@v5
+      - name: Build & push single-arch (to both registries)
+        uses: docker/build-push-action@v6
         with:
           context: .
-          platforms: linux/amd64,linux/arm64
+          platforms: ${{ matrix.platform }}
           push: true
-          tags: ${{ steps.meta.outputs.tags }}
-          labels: ${{ steps.meta.outputs.labels }}
+          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: false
+          sbom: false
+
+  create_manifests:
+    name: Create multi-arch manifests (Docker Hub)
+    needs: [build_single_arch]
+    runs-on: ubuntu-latest
+    if: startsWith(github.ref, 'refs/tags/')
+    steps:
+      - name: Extract tag
+        run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
+#
+#      - name: Normalize GHCR repository
+#        run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
+
+      - name: Log in to Docker Hub
+        uses: docker/login-action@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
+
+      # ---- GHCR ----
+#      - name: Log in to GHCR
+#        uses: docker/login-action@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

+ 141 - 0
.github/workflows/electron-build.yml

@@ -0,0 +1,141 @@
+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 }}

+ 0 - 59
.github/workflows/linux-release.yml

@@ -1,59 +0,0 @@
-name: Linux Release
-permissions:
-  contents: write
-
-on:
-  workflow_dispatch:
-    inputs:
-      name:
-        description: 'reason'
-        required: false
-  push:
-    tags:
-      - '*'
-      - '!*-alpha*'
-jobs:
-  release:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-        with:
-          fetch-depth: 0
-      - uses: oven-sh/setup-bun@v2
-        with:
-          bun-version: latest
-      - name: Build Frontend
-        env:
-          CI: ""
-        run: |
-          cd web
-          bun install
-          DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
-          cd ..
-      - name: Set up Go
-        uses: actions/setup-go@v3
-        with:
-          go-version: '>=1.18.0'
-      - name: Build Backend (amd64)
-        run: |
-          go mod download
-          go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api
-
-      - 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 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api-arm64
-
-      - name: Release
-        uses: softprops/action-gh-release@v1
-        if: startsWith(github.ref, 'refs/tags/')
-        with:
-          files: |
-            new-api
-            new-api-arm64
-          draft: true
-          generate_release_notes: true
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 0 - 51
.github/workflows/macos-release.yml

@@ -1,51 +0,0 @@
-name: macOS Release
-permissions:
-  contents: write
-
-on:
-  workflow_dispatch:
-    inputs:
-      name:
-        description: 'reason'
-        required: false
-  push:
-    tags:
-      - '*'
-      - '!*-alpha*'
-jobs:
-  release:
-    runs-on: macos-latest
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-        with:
-          fetch-depth: 0
-      - uses: oven-sh/setup-bun@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=$(git describe --tags) bun run build
-          cd ..
-      - name: Set up Go
-        uses: actions/setup-go@v3
-        with:
-          go-version: '>=1.18.0'
-      - name: Build Backend
-        run: |
-          go mod download
-          go build -ldflags "-X 'one-api/common.Version=$(git describe --tags)'" -o new-api-macos
-      - name: Release
-        uses: softprops/action-gh-release@v1
-        if: startsWith(github.ref, 'refs/tags/')
-        with:
-          files: new-api-macos
-          draft: true
-          generate_release_notes: true
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 136 - 0
.github/workflows/release.yml

@@ -0,0 +1,136 @@
+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@v3
+        with:
+          fetch-depth: 0
+      - uses: oven-sh/setup-bun@v2
+        with:
+          bun-version: latest
+      - name: Build Frontend
+        env:
+          CI: ""
+        run: |
+          cd web
+          bun install
+          DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
+          cd ..
+      - name: Set up Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: '>=1.25.1'
+      - name: Build Backend (amd64)
+        run: |
+          go mod download
+          VERSION=$(git describe --tags)
+          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
+          VERSION=$(git describe --tags)
+          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: Release
+        uses: softprops/action-gh-release@v2
+        if: startsWith(github.ref, 'refs/tags/')
+        with:
+          files: |
+            new-api-*
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+  macos:
+    name: macOS Release
+    runs-on: macos-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+      - uses: oven-sh/setup-bun@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=$(git describe --tags) bun run build
+          cd ..
+      - name: Set up Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: '>=1.25.1'
+      - name: Build Backend
+        run: |
+          go mod download
+          VERSION=$(git describe --tags)
+          go build -ldflags "-X 'new-api/common.Version=$VERSION'" -o new-api-macos-$VERSION
+      - name: Release
+        uses: softprops/action-gh-release@v2
+        if: startsWith(github.ref, 'refs/tags/')
+        with:
+          files: new-api-macos-*
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+  windows:
+    name: Windows Release
+    runs-on: windows-latest
+    defaults:
+      run:
+        shell: bash
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+      - uses: oven-sh/setup-bun@v2
+        with:
+          bun-version: latest
+      - name: Build Frontend
+        env:
+          CI: ""
+        run: |
+          cd web
+          bun install
+          DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
+          cd ..
+      - name: Set up Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: '>=1.25.1'
+      - name: Build Backend
+        run: |
+          go mod download
+          VERSION=$(git describe --tags)
+          go build -ldflags "-s -w -X 'new-api/common.Version=$VERSION'" -o new-api-$VERSION.exe
+      - name: Release
+        uses: softprops/action-gh-release@v2
+        if: startsWith(github.ref, 'refs/tags/')
+        with:
+          files: new-api-*.exe
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+

+ 91 - 0
.github/workflows/sync-to-gitee.yml

@@ -0,0 +1,91 @@
+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<<EOF"
+            echo "$RELEASE_INFO" | jq -r '.body'
+            echo "EOF"
+          } >> $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/[email protected]
+        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/[email protected]
+        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 }}"
+

+ 0 - 53
.github/workflows/windows-release.yml

@@ -1,53 +0,0 @@
-name: Windows Release
-permissions:
-  contents: write
-
-on:
-  workflow_dispatch:
-    inputs:
-      name:
-        description: 'reason'
-        required: false
-  push:
-    tags:
-      - '*'
-      - '!*-alpha*'
-jobs:
-  release:
-    runs-on: windows-latest
-    defaults:
-      run:
-        shell: bash
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-        with:
-          fetch-depth: 0
-      - uses: oven-sh/setup-bun@v2
-        with:
-          bun-version: latest
-      - name: Build Frontend
-        env:
-          CI: ""
-        run: |
-          cd web
-          bun install
-          DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build
-          cd ..
-      - name: Set up Go
-        uses: actions/setup-go@v3
-        with:
-          go-version: '>=1.18.0'
-      - name: Build Backend
-        run: |
-          go mod download
-          go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)'" -o new-api.exe
-      - name: Release
-        uses: softprops/action-gh-release@v1
-        if: startsWith(github.ref, 'refs/tags/')
-        with:
-          files: new-api.exe
-          draft: true
-          generate_release_notes: true
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 6 - 1
.gitignore

@@ -9,6 +9,11 @@ logs
 web/dist
 .env
 one-api
+new-api
 .DS_Store
 tiktoken_cache
-.eslintcache
+.eslintcache
+.gocache
+
+electron/node_modules
+electron/dist

+ 8 - 6
Dockerfile

@@ -9,10 +9,12 @@ COPY ./VERSION .
 RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
 
 FROM golang:alpine AS builder2
+ENV GO111MODULE=on CGO_ENABLED=0
+
+ARG TARGETOS
+ARG TARGETARCH
+ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64}
 
-ENV GO111MODULE=on \
-    CGO_ENABLED=0 \
-    GOOS=linux
 
 WORKDIR /build
 
@@ -21,7 +23,7 @@ RUN go mod download
 
 COPY . .
 COPY --from=builder /build/dist ./web/dist
-RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)'" -o one-api
+RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api
 
 FROM alpine
 
@@ -29,7 +31,7 @@ RUN apk upgrade --no-cache \
     && apk add --no-cache ca-certificates tzdata ffmpeg \
     && update-ca-certificates
 
-COPY --from=builder2 /build/one-api /
+COPY --from=builder2 /build/new-api /
 EXPOSE 3000
 WORKDIR /data
-ENTRYPOINT ["/one-api"]
+ENTRYPOINT ["/new-api"]

+ 29 - 20
README.en.md

@@ -1,6 +1,10 @@
 <p align="right">
-   <a href="./README.md">中文</a> | <strong>English</strong>
+   <a href="./README.md">中文</a> | <strong>English</strong> | <a href="./README.fr.md">Français</a> | <a href="./README.ja.md">日本語</a>
 </p>
+
+> [!NOTE]
+> **MT (Machine Translation)**: This document is machine translated. For the most accurate information, please refer to the [Chinese version](./README.md).
+
 <div align="center">
 
 ![new-api](/web/public/logo.png)
@@ -75,7 +79,7 @@ New API offers a wide range of features, please refer to [Features Introduction]
 
 1. 🎨 Brand new UI interface
 2. 🌍 Multi-language support
-3. 💰 Online recharge functionality (YiPay)
+3. 💰 Online recharge functionality, currently supports EPay and Stripe
 4. 🔍 Support for querying usage quotas with keys (works with [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
 5. 🔄 Compatible with the original One API database
 6. 💵 Support for pay-per-use model pricing
@@ -85,18 +89,23 @@ New API offers a wide range of features, please refer to [Features Introduction]
 10. 🤖 Support for more authorization login methods (LinuxDO, Telegram, OIDC)
 11. 🔄 Support for Rerank models (Cohere and Jina), [API Documentation](https://docs.newapi.pro/api/jinaai-rerank)
 12. ⚡ Support for OpenAI Realtime API (including Azure channels), [API Documentation](https://docs.newapi.pro/api/openai-realtime)
-13. ⚡ Support for Claude Messages format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat)
-14. Support for entering chat interface via /chat2link route
-15. 🧠 Support for setting reasoning effort through model name suffixes:
+13. ⚡ Support for **OpenAI Responses** format, [API Documentation](https://docs.newapi.pro/api/openai-responses)
+14. ⚡ Support for **Claude Messages** format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat)
+15. ⚡ Support for **Google Gemini** format, [API Documentation](https://docs.newapi.pro/api/google-gemini-chat/)
+16. 🧠 Support for setting reasoning effort through model name suffixes:
     1. OpenAI o-series models
         - Add `-high` suffix for high reasoning effort (e.g.: `o3-mini-high`)
         - Add `-medium` suffix for medium reasoning effort (e.g.: `o3-mini-medium`)
         - Add `-low` suffix for low reasoning effort (e.g.: `o3-mini-low`)
     2. Claude thinking models
         - Add `-thinking` suffix to enable thinking mode (e.g.: `claude-3-7-sonnet-20250219-thinking`)
-16. 🔄 Thinking-to-content functionality
-17. 🔄 Model rate limiting for users
-18. 💰 Cache billing support, which allows billing at a set ratio when cache is hit:
+17. 🔄 Thinking-to-content functionality
+18. 🔄 Model rate limiting for users
+19. 🔄 Request format conversion functionality, supporting the following three format conversions:
+    1. OpenAI Chat Completions => Claude Messages
+    2. Claude Messages => OpenAI Chat Completions (can be used for Claude Code to call third-party models)
+    3. OpenAI Chat Completions => Gemini Chat
+20. 💰 Cache billing support, which allows billing at a set ratio when cache is hit:
     1. Set the `Prompt Cache Ratio` option in `System Settings-Operation Settings`
     2. Set `Prompt Cache Ratio` in the channel, range 0-1, e.g., setting to 0.5 means billing at 50% when cache is hit
     3. Supported channels:
@@ -115,7 +124,9 @@ This version supports multiple models, please refer to [API Documentation-Relay
 4. Custom channels, supporting full call address input
 5. Rerank models ([Cohere](https://cohere.ai/) and [Jina](https://jina.ai/)), [API Documentation](https://docs.newapi.pro/api/jinaai-rerank)
 6. Claude Messages format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat)
-7. Dify, currently only supports chatflow
+7. Google Gemini format, [API Documentation](https://docs.newapi.pro/api/google-gemini-chat/)
+8. Dify, currently only supports chatflow
+9. For more interfaces, please refer to [API Documentation](https://docs.newapi.pro/api)
 
 ## Environment Variable Configuration
 
@@ -124,14 +135,12 @@ For detailed configuration instructions, please refer to [Installation Guide-Env
 - `GENERATE_DEFAULT_TOKEN`: Whether to generate initial tokens for newly registered users, default is `false`
 - `STREAMING_TIMEOUT`: Streaming response timeout, default is 300 seconds
 - `DIFY_DEBUG`: Whether to output workflow and node information for Dify channels, default is `true`
-- `FORCE_STREAM_OPTION`: Whether to override client stream_options parameter, default is `true`
 - `GET_MEDIA_TOKEN`: Whether to count image tokens, default is `true`
 - `GET_MEDIA_TOKEN_NOT_STREAM`: Whether to count image tokens in non-streaming cases, default is `true`
 - `UPDATE_TASK`: Whether to update asynchronous tasks (Midjourney, Suno), default is `true`
-- `COHERE_SAFETY_SETTING`: Cohere model safety settings, options are `NONE`, `CONTEXTUAL`, `STRICT`, default is `NONE`
 - `GEMINI_VISION_MAX_IMAGE_NUM`: Maximum number of images for Gemini models, default is `16`
 - `MAX_FILE_DOWNLOAD_MB`: Maximum file download size in MB, default is `20`
-- `CRYPTO_SECRET`: Encryption key used for encrypting database content
+- `CRYPTO_SECRET`: Encryption key used for encrypting Redis database content
 - `AZURE_DEFAULT_API_VERSION`: Azure channel default API version, default is `2025-04-01-preview`
 - `NOTIFICATION_LIMIT_DURATION_MINUTE`: Notification limit duration, default is `10` minutes
 - `NOTIFY_LIMIT_COUNT`: Maximum number of user notifications within the specified duration, default is `2`
@@ -178,7 +187,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
 ```
 
 ## Channel Retry and Cache
-Channel retry functionality has been implemented, you can set the number of retries in `Settings->Operation Settings->General Settings`. It is **recommended to enable caching**.
+Channel retry functionality has been implemented, you can set the number of retries in `Settings->Operation Settings->General Settings->Failure Retry Count`, **recommended to enable caching** functionality.
 
 ### Cache Configuration Method
 1. `REDIS_CONN_STRING`: Set Redis as cache
@@ -188,21 +197,21 @@ Channel retry functionality has been implemented, you can set the number of retr
 
 For detailed API documentation, please refer to [API Documentation](https://docs.newapi.pro/api):
 
-- [Chat API](https://docs.newapi.pro/api/openai-chat)
-- [Image API](https://docs.newapi.pro/api/openai-image)
-- [Rerank API](https://docs.newapi.pro/api/jinaai-rerank)
-- [Realtime API](https://docs.newapi.pro/api/openai-realtime)
-- [Claude Chat API (messages)](https://docs.newapi.pro/api/anthropic-chat)
+- [Chat API (Chat Completions)](https://docs.newapi.pro/api/openai-chat)
+- [Response API (Responses)](https://docs.newapi.pro/api/openai-responses)
+- [Image API (Image)](https://docs.newapi.pro/api/openai-image)
+- [Rerank API (Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
+- [Realtime Chat API (Realtime)](https://docs.newapi.pro/api/openai-realtime)
+- [Claude Chat API](https://docs.newapi.pro/api/anthropic-chat)
+- [Google Gemini Chat API](https://docs.newapi.pro/api/google-gemini-chat)
 
 ## Related Projects
 - [One API](https://github.com/songquanpeng/one-api): Original project
 - [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy): Midjourney interface support
-- [chatnio](https://github.com/Deeptrain-Community/chatnio): Next-generation AI one-stop B/C-end solution
 - [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool): Query usage quota with key
 
 Other projects based on New API:
 - [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon): High-performance optimized version of New API
-- [VoAPI](https://github.com/VoAPI/VoAPI): Frontend beautified version based on New API
 
 ## Help and Support
 

+ 225 - 0
README.fr.md

@@ -0,0 +1,225 @@
+<p align="right">
+   <a href="./README.md">中文</a> | <a href="./README.en.md">English</a> | <strong>Français</strong> | <a href="./README.ja.md">日本語</a>
+</p>
+
+> [!NOTE]
+> **MT (Traduction Automatique)**: Ce document est traduit automatiquement. Pour les informations les plus précises, veuillez vous référer à la [version chinoise](./README.md).
+
+<div align="center">
+
+![new-api](/web/public/logo.png)
+
+# New API
+
+🍥 Passerelle de modèles étendus de nouvelle génération et système de gestion d'actifs d'IA
+
+<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
+
+<p align="center">
+  <a href="https://raw.githubusercontent.com/Calcium-Ion/new-api/main/LICENSE">
+    <img src="https://img.shields.io/github/license/Calcium-Ion/new-api?color=brightgreen" alt="licence">
+  </a>
+  <a href="https://github.com/Calcium-Ion/new-api/releases/latest">
+    <img src="https://img.shields.io/github/v/release/Calcium-Ion/new-api?color=brightgreen&include_prereleases" alt="version">
+  </a>
+  <a href="https://github.com/users/Calcium-Ion/packages/container/package/new-api">
+    <img src="https://img.shields.io/badge/docker-ghcr.io-blue" alt="docker">
+  </a>
+  <a href="https://hub.docker.com/r/CalciumIon/new-api">
+    <img src="https://img.shields.io/badge/docker-dockerHub-blue" alt="docker">
+  </a>
+  <a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
+    <img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
+  </a>
+</p>
+</div>
+
+## 📝 Description du projet
+
+> [!NOTE]
+> Il s'agit d'un projet open-source développé sur la base de [One API](https://github.com/songquanpeng/one-api)
+
+> [!IMPORTANT]
+> - Ce projet est uniquement destiné à des fins d'apprentissage personnel, sans garantie de stabilité ni de support technique.
+> - Les utilisateurs doivent se conformer aux [Conditions d'utilisation](https://openai.com/policies/terms-of-use) d'OpenAI et aux **lois et réglementations applicables**, et ne doivent pas l'utiliser à des fins illégales.
+> - Conformément aux [《Mesures provisoires pour la gestion des services d'intelligence artificielle générative》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm), veuillez ne fournir aucun service d'IA générative non enregistré au public en Chine.
+
+<h2>🤝 Partenaires de confiance</h2>
+<p id="premium-sponsors">&nbsp;</p>
+<p align="center"><strong>Sans ordre particulier</strong></p>
+<p align="center">
+  <a href="https://www.cherry-ai.com/" target=_blank><img
+    src="./docs/images/cherry-studio.png" alt="Cherry Studio" height="120"
+  /></a>
+  <a href="https://bda.pku.edu.cn/" target=_blank><img
+    src="./docs/images/pku.png" alt="Université de Pékin" height="120"
+  /></a>
+  <a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target=_blank><img
+    src="./docs/images/ucloud.png" alt="UCloud" height="120"
+  /></a>
+  <a href="https://www.aliyun.com/" target=_blank><img
+    src="./docs/images/aliyun.png" alt="Alibaba Cloud" height="120"
+  /></a>
+  <a href="https://io.net/" target=_blank><img
+    src="./docs/images/io-net.png" alt="IO.NET" height="120"
+  /></a>
+</p>
+<p>&nbsp;</p>
+
+## 📚 Documentation
+
+Pour une documentation détaillée, veuillez consulter notre Wiki officiel : [https://docs.newapi.pro/](https://docs.newapi.pro/)
+
+Vous pouvez également accéder au DeepWiki généré par l'IA :
+[![Demander à DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/QuantumNous/new-api)
+
+## ✨ Fonctionnalités clés
+
+New API offre un large éventail de fonctionnalités, veuillez vous référer à [Présentation des fonctionnalités](https://docs.newapi.pro/wiki/features-introduction) pour plus de détails :
+
+1. 🎨 Nouvelle interface utilisateur
+2. 🌍 Prise en charge multilingue
+3. 💰 Fonctionnalité de recharge en ligne, prend actuellement en charge EPay et Stripe
+4. 🔍 Prise en charge de la recherche de quotas d'utilisation avec des clés (fonctionne avec [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
+5. 🔄 Compatible avec la base de données originale de One API
+6. 💵 Prise en charge de la tarification des modèles de paiement à l'utilisation
+7. ⚖️ Prise en charge de la sélection aléatoire pondérée des canaux
+8. 📈 Tableau de bord des données (console)
+9. 🔒 Regroupement de jetons et restrictions de modèles
+10. 🤖 Prise en charge de plus de méthodes de connexion par autorisation (LinuxDO, Telegram, OIDC)
+11. 🔄 Prise en charge des modèles Rerank (Cohere et Jina), [Documentation de l'API](https://docs.newapi.pro/api/jinaai-rerank)
+12. ⚡ Prise en charge de l'API OpenAI Realtime (y compris les canaux Azure), [Documentation de l'API](https://docs.newapi.pro/api/openai-realtime)
+13. ⚡ Prise en charge du format **OpenAI Responses**, [Documentation de l'API](https://docs.newapi.pro/api/openai-responses)
+14. ⚡ Prise en charge du format **Claude Messages**, [Documentation de l'API](https://docs.newapi.pro/api/anthropic-chat)
+15. ⚡ Prise en charge du format **Google Gemini**, [Documentation de l'API](https://docs.newapi.pro/api/google-gemini-chat/)
+16. 🧠 Prise en charge de la définition de l'effort de raisonnement via les suffixes de nom de modèle :
+    1. Modèles de la série o d'OpenAI
+        - Ajouter le suffixe `-high` pour un effort de raisonnement élevé (par exemple : `o3-mini-high`)
+        - Ajouter le suffixe `-medium` pour un effort de raisonnement moyen (par exemple : `o3-mini-medium`)
+        - Ajouter le suffixe `-low` pour un effort de raisonnement faible (par exemple : `o3-mini-low`)
+    2. Modèles de pensée de Claude
+        - Ajouter le suffixe `-thinking` pour activer le mode de pensée (par exemple : `claude-3-7-sonnet-20250219-thinking`)
+17. 🔄 Fonctionnalité de la pensée au contenu
+18. 🔄 Limitation du débit du modèle pour les utilisateurs
+19. 🔄 Fonctionnalité de conversion de format de requête, prenant en charge les trois conversions de format suivantes :
+    1. OpenAI Chat Completions => Claude Messages
+    2. Claude Messages => OpenAI Chat Completions (peut être utilisé pour Claude Code pour appeler des modèles tiers)
+    3. OpenAI Chat Completions => Gemini Chat
+20. 💰 Prise en charge de la facturation du cache, qui permet de facturer à un ratio défini lorsque le cache est atteint :
+    1. Définir l'option `Ratio de cache d'invite` dans `Paramètres système->Paramètres de fonctionnement`
+    2. Définir le `Ratio de cache d'invite` dans le canal, plage de 0 à 1, par exemple, le définir sur 0,5 signifie facturer à 50 % lorsque le cache est atteint
+    3. Canaux pris en charge :
+        - [x] OpenAI
+        - [x] Azure
+        - [x] DeepSeek
+        - [x] Claude
+
+## Prise en charge des modèles
+
+Cette version prend en charge plusieurs modèles, veuillez vous référer à [Documentation de l'API-Interface de relais](https://docs.newapi.pro/api) pour plus de détails :
+
+1. Modèles tiers **gpts** (gpt-4-gizmo-*)
+2. Canal tiers [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy), [Documentation de l'API](https://docs.newapi.pro/api/midjourney-proxy-image)
+3. Canal tiers [Suno API](https://github.com/Suno-API/Suno-API), [Documentation de l'API](https://docs.newapi.pro/api/suno-music)
+4. Canaux personnalisés, prenant en charge la saisie complète de l'adresse d'appel
+5. Modèles Rerank ([Cohere](https://cohere.ai/) et [Jina](https://jina.ai/)), [Documentation de l'API](https://docs.newapi.pro/api/jinaai-rerank)
+6. Format de messages Claude, [Documentation de l'API](https://docs.newapi.pro/api/anthropic-chat)
+7. Format Google Gemini, [Documentation de l'API](https://docs.newapi.pro/api/google-gemini-chat/)
+8. Dify, ne prend actuellement en charge que chatflow
+9. Pour plus d'interfaces, veuillez vous référer à la [Documentation de l'API](https://docs.newapi.pro/api)
+
+## Configuration des variables d'environnement
+
+Pour des instructions de configuration détaillées, veuillez vous référer à [Guide d'installation-Configuration des variables d'environnement](https://docs.newapi.pro/installation/environment-variables) :
+
+- `GENERATE_DEFAULT_TOKEN` : S'il faut générer des jetons initiaux pour les utilisateurs nouvellement enregistrés, la valeur par défaut est `false`
+- `STREAMING_TIMEOUT` : Délai d'expiration de la réponse en streaming, la valeur par défaut est de 300 secondes
+- `DIFY_DEBUG` : S'il faut afficher les informations sur le flux de travail et les nœuds pour les canaux Dify, la valeur par défaut est `true`
+- `GET_MEDIA_TOKEN` : S'il faut compter les jetons d'image, la valeur par défaut est `true`
+- `GET_MEDIA_TOKEN_NOT_STREAM` : S'il faut compter les jetons d'image dans les cas sans streaming, la valeur par défaut est `true`
+- `UPDATE_TASK` : S'il faut mettre à jour les tâches asynchrones (Midjourney, Suno), la valeur par défaut est `true`
+- `GEMINI_VISION_MAX_IMAGE_NUM` : Nombre maximum d'images pour les modèles Gemini, la valeur par défaut est `16`
+- `MAX_FILE_DOWNLOAD_MB` : Taille maximale de téléchargement de fichier en Mo, la valeur par défaut est `20`
+- `CRYPTO_SECRET` : Clé de chiffrement utilisée pour chiffrer le contenu de la base de données Redis
+- `AZURE_DEFAULT_API_VERSION` : Version de l'API par défaut du canal Azure, la valeur par défaut est `2025-04-01-preview`
+- `NOTIFICATION_LIMIT_DURATION_MINUTE` : Durée de la limite de notification, la valeur par défaut est de `10` minutes
+- `NOTIFY_LIMIT_COUNT` : Nombre maximal de notifications utilisateur dans la durée spécifiée, la valeur par défaut est `2`
+- `ERROR_LOG_ENABLED=true` : S'il faut enregistrer et afficher les journaux d'erreurs, la valeur par défaut est `false`
+
+## Déploiement
+
+Pour des guides de déploiement détaillés, veuillez vous référer à [Guide d'installation-Méthodes de déploiement](https://docs.newapi.pro/installation) :
+
+> [!TIP]
+> Dernière image Docker : `calciumion/new-api:latest`
+
+### Considérations sur le déploiement multi-machines
+- La variable d'environnement `SESSION_SECRET` doit être définie, sinon l'état de connexion sera incohérent sur plusieurs machines
+- Si vous partagez Redis, `CRYPTO_SECRET` doit être défini, sinon le contenu de Redis ne pourra pas être consulté sur plusieurs machines
+
+### Exigences de déploiement
+- Base de données locale (par défaut) : SQLite (le déploiement Docker doit monter le répertoire `/data`)
+- Base de données distante : MySQL version >= 5.7.8, PgSQL version >= 9.6
+
+### Méthodes de déploiement
+
+#### Utilisation de la fonctionnalité Docker du panneau BaoTa
+Installez le panneau BaoTa (version **9.2.0** ou supérieure), recherchez **New-API** dans le magasin d'applications et installez-le.
+[Tutoriel avec des images](./docs/BT.md)
+
+#### Utilisation de Docker Compose (recommandé)
+```shell
+# Télécharger le projet
+git clone https://github.com/Calcium-Ion/new-api.git
+cd new-api
+# Modifier docker-compose.yml si nécessaire
+# Démarrer
+docker-compose up -d
+```
+
+#### Utilisation directe de l'image Docker
+```shell
+# Utilisation de SQLite
+docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
+
+# Utilisation de MySQL
+docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
+```
+
+## Nouvelle tentative de canal et cache
+La fonctionnalité de nouvelle tentative de canal a été implémentée, vous pouvez définir le nombre de tentatives dans `Paramètres->Paramètres de fonctionnement->Paramètres généraux->Nombre de tentatives en cas d'échec`, **recommandé d'activer la fonctionnalité de mise en cache**.
+
+### Méthode de configuration du cache
+1. `REDIS_CONN_STRING` : Définir Redis comme cache
+2. `MEMORY_CACHE_ENABLED` : Activer le cache mémoire (pas besoin de le définir manuellement si Redis est défini)
+
+## Documentation de l'API
+
+Pour une documentation détaillée de l'API, veuillez vous référer à [Documentation de l'API](https://docs.newapi.pro/api) :
+
+- [API de discussion (Chat Completions)](https://docs.newapi.pro/api/openai-chat)
+- [API de réponse (Responses)](https://docs.newapi.pro/api/openai-responses)
+- [API d'image (Image)](https://docs.newapi.pro/api/openai-image)
+- [API de rerank (Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
+- [API de discussion en temps réel (Realtime)](https://docs.newapi.pro/api/openai-realtime)
+- [API de discussion Claude](https://docs.newapi.pro/api/anthropic-chat)
+- [API de discussion Google Gemini](https://docs.newapi.pro/api/google-gemini-chat)
+
+## Projets connexes
+- [One API](https://github.com/songquanpeng/one-api) : Projet original
+- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy) : Prise en charge de l'interface Midjourney
+- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool) : Interroger le quota d'utilisation avec une clé
+
+Autres projets basés sur New API :
+- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon) : Version optimisée hautes performances de New API
+
+## Aide et support
+
+Si vous avez des questions, veuillez vous référer à [Aide et support](https://docs.newapi.pro/support) :
+- [Interaction avec la communauté](https://docs.newapi.pro/support/community-interaction)
+- [Commentaires sur les problèmes](https://docs.newapi.pro/support/feedback-issues)
+- [FAQ](https://docs.newapi.pro/support/faq)
+
+## 🌟 Historique des étoiles
+
+[![Graphique de l'historique des étoiles](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date)

+ 226 - 0
README.ja.md

@@ -0,0 +1,226 @@
+<p align="right">
+   <a href="./README.md">中文</a> | <a href="./README.en.md">English</a> | <a href="./README.fr.md">Français</a> | <strong>日本語</strong>
+</p>
+
+> [!NOTE]
+> **MT(機械翻訳)**: この文書は機械翻訳されています。最も正確な情報については、[中国語版](./README.md)を参照してください。
+
+<div align="center">
+
+![new-api](/web/public/logo.png)
+
+# New API
+
+🍥次世代大規模モデルゲートウェイとAI資産管理システム
+
+<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
+
+<p align="center">
+  <a href="https://raw.githubusercontent.com/Calcium-Ion/new-api/main/LICENSE">
+    <img src="https://img.shields.io/github/license/Calcium-Ion/new-api?color=brightgreen" alt="license">
+  </a>
+  <a href="https://github.com/Calcium-Ion/new-api/releases/latest">
+    <img src="https://img.shields.io/github/v/release/Calcium-Ion/new-api?color=brightgreen&include_prereleases" alt="release">
+  </a>
+  <a href="https://github.com/users/Calcium-Ion/packages/container/package/new-api">
+    <img src="https://img.shields.io/badge/docker-ghcr.io-blue" alt="docker">
+  </a>
+  <a href="https://hub.docker.com/r/CalciumIon/new-api">
+    <img src="https://img.shields.io/badge/docker-dockerHub-blue" alt="docker">
+  </a>
+  <a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
+    <img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
+  </a>
+</p>
+</div>
+
+## 📝 プロジェクト説明
+
+> [!NOTE]  
+> 本プロジェクトは、[One API](https://github.com/songquanpeng/one-api)をベースに二次開発されたオープンソースプロジェクトです
+
+> [!IMPORTANT]  
+> - 本プロジェクトは個人学習用のみであり、安定性の保証や技術サポートは提供しません。
+> - ユーザーは、OpenAIの[利用規約](https://openai.com/policies/terms-of-use)および**法律法規**を遵守する必要があり、違法な目的で使用してはいけません。
+> - [《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)の要求に従い、中国地域の公衆に未登録の生成式AI サービスを提供しないでください。
+
+<h2>🤝 信頼できるパートナー</h2>
+<p id="premium-sponsors">&nbsp;</p>
+<p align="center"><strong>順不同</strong></p>
+<p align="center">
+  <a href="https://www.cherry-ai.com/" target=_blank><img
+    src="./docs/images/cherry-studio.png" alt="Cherry Studio" height="120"
+  /></a>
+  <a href="https://bda.pku.edu.cn/" target=_blank><img
+    src="./docs/images/pku.png" alt="北京大学" height="120"
+  /></a>
+  <a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target=_blank><img
+    src="./docs/images/ucloud.png" alt="UCloud 優刻得" height="120"
+  /></a>
+  <a href="https://www.aliyun.com/" target=_blank><img
+    src="./docs/images/aliyun.png" alt="Alibaba Cloud" height="120"
+  /></a>
+  <a href="https://io.net/" target=_blank><img
+    src="./docs/images/io-net.png" alt="IO.NET" height="120"
+  /></a>
+</p>
+<p>&nbsp;</p>
+
+## 📚 ドキュメント
+
+詳細なドキュメントは公式Wikiをご覧ください:[https://docs.newapi.pro/](https://docs.newapi.pro/)
+
+AIが生成したDeepWikiにもアクセスできます:
+[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/QuantumNous/new-api)
+
+## ✨ 主な機能
+
+New APIは豊富な機能を提供しています。詳細な機能については[機能説明](https://docs.newapi.pro/wiki/features-introduction)を参照してください:
+
+1. 🎨 全く新しいUIインターフェース
+2. 🌍 多言語サポート
+3. 💰 オンラインチャージ機能をサポート、現在EPayとStripeをサポート
+4. 🔍 キーによる使用量クォータの照会をサポート([neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)と連携)
+5. 🔄 オリジナルのOne APIデータベースと互換性あり
+6. 💵 モデルの従量課金をサポート
+7. ⚖️ チャネルの重み付けランダムをサポート
+8. 📈 データダッシュボード(コンソール)
+9. 🔒 トークングループ化、モデル制限
+10. 🤖 より多くの認証ログイン方法をサポート(LinuxDO、Telegram、OIDC)
+11. 🔄 Rerankモデルをサポート(CohereとJina)、[API ドキュメント](https://docs.newapi.pro/api/jinaai-rerank)
+12. ⚡ OpenAI Realtime APIをサポート(Azureチャネルを含む)、[APIドキュメント](https://docs.newapi.pro/api/openai-realtime)
+13. ⚡ **OpenAI Responses**形式をサポート、[APIドキュメント](https://docs.newapi.pro/api/openai-responses)
+14. ⚡ **Claude Messages**形式をサポート、[APIドキュメント](https://docs.newapi.pro/api/anthropic-chat)
+15. ⚡ **Google Gemini**形式をサポート、[APIドキュメント](https://docs.newapi.pro/api/google-gemini-chat/)
+16. 🧠 モデル名のサフィックスを通じてreasoning effortを設定することをサポート:
+    1. OpenAI oシリーズモデル
+        - `-high`サフィックスを追加してhigh reasoning effortに設定(例:`o3-mini-high`)
+        - `-medium`サフィックスを追加してmedium reasoning effortに設定(例:`o3-mini-medium`)
+        - `-low`サフィックスを追加してlow reasoning effortに設定(例:`o3-mini-low`)
+    2. Claude思考モデル
+        - `-thinking`サフィックスを追加して思考モードを有効にする(例:`claude-3-7-sonnet-20250219-thinking`)
+17. 🔄 思考からコンテンツへの機能
+18. 🔄 ユーザーに対するモデルレート制限機能
+19. 🔄 リクエストフォーマット変換機能、以下の3つのフォーマット変換をサポート:
+    1. OpenAI Chat Completions => Claude Messages
+    2. Claude Messages => OpenAI Chat Completions(Claude Codeがサードパーティモデルを呼び出す際に使用可能)
+    3. OpenAI Chat Completions => Gemini Chat
+20. 💰 キャッシュ課金サポート、有効にするとキャッシュがヒットした際に設定された比率で課金できます:
+    1. `システム設定-運営設定`で`プロンプトキャッシュ倍率`オプションを設定
+    2. チャネルで`プロンプトキャッシュ倍率`を設定、範囲は0-1、例えば0.5に設定するとキャッシュがヒットした際に50%で課金
+    3. サポートされているチャネル:
+        - [x] OpenAI
+        - [x] Azure
+        - [x] DeepSeek
+        - [x] Claude
+
+## モデルサポート
+
+このバージョンは複数のモデルをサポートしています。詳細は[APIドキュメント-中継インターフェース](https://docs.newapi.pro/api)を参照してください:
+
+1. サードパーティモデル **gpts**(gpt-4-gizmo-*)
+2. サードパーティチャネル[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)インターフェース、[APIドキュメント](https://docs.newapi.pro/api/midjourney-proxy-image)
+3. サードパーティチャネル[Suno API](https://github.com/Suno-API/Suno-API)インターフェース、[APIドキュメント](https://docs.newapi.pro/api/suno-music)
+4. カスタムチャネル、完全な呼び出しアドレスの入力をサポート
+5. Rerankモデル([Cohere](https://cohere.ai/)と[Jina](https://jina.ai/))、[APIドキュメント](https://docs.newapi.pro/api/jinaai-rerank)
+6. Claude Messages形式、[APIドキュメント](https://docs.newapi.pro/api/anthropic-chat)
+7. Google Gemini形式、[APIドキュメント](https://docs.newapi.pro/api/google-gemini-chat/)
+8. Dify、現在はchatflowのみをサポート
+9. その他のインターフェースについては[APIドキュメント](https://docs.newapi.pro/api)を参照してください
+
+## 環境変数設定
+
+詳細な設定説明については[インストールガイド-環境変数設定](https://docs.newapi.pro/installation/environment-variables)を参照してください:
+
+- `GENERATE_DEFAULT_TOKEN`:新規登録ユーザーに初期トークンを生成するかどうか、デフォルトは`false`
+- `STREAMING_TIMEOUT`:ストリーミング応答のタイムアウト時間、デフォルトは300秒
+- `DIFY_DEBUG`:Difyチャネルがワークフローとノード情報を出力するかどうか、デフォルトは`true`
+- `GET_MEDIA_TOKEN`:画像トークンを統計するかどうか、デフォルトは`true`
+- `GET_MEDIA_TOKEN_NOT_STREAM`:非ストリーミングの場合に画像トークンを統計するかどうか、デフォルトは`true`
+- `UPDATE_TASK`:非同期タスク(Midjourney、Suno)を更新するかどうか、デフォルトは`true`
+- `GEMINI_VISION_MAX_IMAGE_NUM`:Geminiモデルの最大画像数、デフォルトは`16`
+- `MAX_FILE_DOWNLOAD_MB`: 最大ファイルダウンロードサイズ、単位MB、デフォルトは`20`
+- `CRYPTO_SECRET`:暗号化キー、Redisデータベースの内容を暗号化するために使用
+- `AZURE_DEFAULT_API_VERSION`:Azureチャネルのデフォルトのバージョン、デフォルトは`2025-04-01-preview`
+- `NOTIFICATION_LIMIT_DURATION_MINUTE`:メールなどの通知制限の継続時間、デフォルトは`10`分
+- `NOTIFY_LIMIT_COUNT`:指定された継続時間内のユーザー通知の最大数、デフォルトは`2`
+- `ERROR_LOG_ENABLED=true`: エラーログを記録して表示するかどうか、デフォルトは`false`
+
+## デプロイ
+
+詳細なデプロイガイドについては[インストールガイド-デプロイ方法](https://docs.newapi.pro/installation)を参照してください:
+
+> [!TIP]
+> 最新のDockerイメージ:`calciumion/new-api:latest`  
+
+### マルチマシンデプロイの注意事項
+- 環境変数`SESSION_SECRET`を設定する必要があります。そうしないとマルチマシンデプロイ時にログイン状態が不一致になります
+- Redisを共有する場合、`CRYPTO_SECRET`を設定する必要があります。そうしないとマルチマシンデプロイ時にRedisの内容を取得できません
+
+### デプロイ要件
+- ローカルデータベース(デフォルト):SQLite(Dockerデプロイの場合は`/data`ディレクトリをマウントする必要があります)
+- リモートデータベース:MySQLバージョン >= 5.7.8、PgSQLバージョン >= 9.6
+
+### デプロイ方法
+
+#### 宝塔パネルのDocker機能を使用してデプロイ
+宝塔パネル(**9.2.0バージョン**以上)をインストールし、アプリケーションストアで**New-API**を見つけてインストールします。
+[画像付きチュートリアル](./docs/BT.md)
+
+#### Docker Composeを使用してデプロイ(推奨)
+```shell
+# プロジェクトをダウンロード
+git clone https://github.com/Calcium-Ion/new-api.git
+cd new-api
+# 必要に応じてdocker-compose.ymlを編集
+# 起動
+docker-compose up -d
+```
+
+#### Dockerイメージを直接使用
+```shell
+# SQLiteを使用
+docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
+
+# MySQLを使用
+docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
+```
+
+## チャネルリトライとキャッシュ
+チャネルリトライ機能はすでに実装されており、`設定->運営設定->一般設定->失敗リトライ回数`でリトライ回数を設定できます。**キャッシュ機能を有効にすることを推奨します**。
+
+### キャッシュ設定方法
+1. `REDIS_CONN_STRING`:Redisをキャッシュとして設定
+2. `MEMORY_CACHE_ENABLED`:メモリキャッシュを有効にする(Redisを設定した場合は手動設定不要)
+
+## APIドキュメント
+
+詳細なAPIドキュメントについては[APIドキュメント](https://docs.newapi.pro/api)を参照してください:
+
+- [チャットインターフェース(Chat Completions)](https://docs.newapi.pro/api/openai-chat)
+- [レスポンスインターフェース(Responses)](https://docs.newapi.pro/api/openai-responses)
+- [画像インターフェース(Image)](https://docs.newapi.pro/api/openai-image)
+- [再ランク付けインターフェース(Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
+- [リアルタイム対話インターフェース(Realtime)](https://docs.newapi.pro/api/openai-realtime)
+- [Claudeチャットインターフェース](https://docs.newapi.pro/api/anthropic-chat)
+- [Google Geminiチャットインターフェース](https://docs.newapi.pro/api/google-gemini-chat)
+
+## 関連プロジェクト
+- [One API](https://github.com/songquanpeng/one-api):オリジナルプロジェクト
+- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourneyインターフェースサポート
+- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool):キーを使用して使用量クォータを照会
+
+New APIベースのその他のプロジェクト:
+- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon):New API高性能最適化版
+
+## ヘルプサポート
+
+問題がある場合は、[ヘルプサポート](https://docs.newapi.pro/support)を参照してください:
+- [コミュニティ交流](https://docs.newapi.pro/support/community-interaction)
+- [問題のフィードバック](https://docs.newapi.pro/support/feedback-issues)
+- [よくある質問](https://docs.newapi.pro/support/faq)
+
+## 🌟 Star History
+
+[![Star History Chart](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date)
+

+ 33 - 25
README.md

@@ -1,5 +1,5 @@
 <p align="right">
-   <strong>中文</strong> | <a href="./README.en.md">English</a>
+   <strong>中文</strong> | <a href="./README.en.md">English</a> | <a href="./README.fr.md">Français</a> | <a href="./README.ja.md">日本語</a>
 </p>
 <div align="center">
 
@@ -75,7 +75,7 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
 
 1. 🎨 全新的UI界面
 2. 🌍 多语言支持
-3. 💰 支持在线充值功能(易支付)
+3. 💰 支持在线充值功能,当前支持易支付和Stripe
 4. 🔍 支持用key查询使用额度(配合[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
 5. 🔄 兼容原版One API的数据库
 6. 💵 支持模型按次数收费
@@ -85,22 +85,23 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
 10. 🤖 支持更多授权登陆方式(LinuxDO,Telegram、OIDC)
 11. 🔄 支持Rerank模型(Cohere和Jina),[接口文档](https://docs.newapi.pro/api/jinaai-rerank)
 12. ⚡ 支持OpenAI Realtime API(包括Azure渠道),[接口文档](https://docs.newapi.pro/api/openai-realtime)
-13. ⚡ 支持Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
-14. 支持使用路由/chat2link进入聊天界面
-15. 🧠 支持通过模型名称后缀设置 reasoning effort:
+13. ⚡ 支持 **OpenAI Responses** 格式,[接口文档](https://docs.newapi.pro/api/openai-responses)
+14. ⚡ 支持 **Claude Messages** 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
+15. ⚡ 支持 **Google Gemini** 格式,[接口文档](https://docs.newapi.pro/api/google-gemini-chat/)
+16. 🧠 支持通过模型名称后缀设置 reasoning effort:
     1. OpenAI o系列模型
         - 添加后缀 `-high` 设置为 high reasoning effort (例如: `o3-mini-high`)
         - 添加后缀 `-medium` 设置为 medium reasoning effort (例如: `o3-mini-medium`)
         - 添加后缀 `-low` 设置为 low reasoning effort (例如: `o3-mini-low`)
     2. Claude 思考模型
         - 添加后缀 `-thinking` 启用思考模式 (例如: `claude-3-7-sonnet-20250219-thinking`)
-16. 🔄 思考转内容功能
-17. 🔄 针对用户的模型限流功能
-18. 🔄 请求格式转换功能,支持以下三种格式转换:
-    1. OpenAI Chat Completions => Claude Messages
+17. 🔄 思考转内容功能
+18. 🔄 针对用户的模型限流功能
+19. 🔄 请求格式转换功能,支持以下三种格式转换:
+    1. OpenAI Chat Completions => Claude Messages (OpenAI格式调用Claude模型)
     2. Clade Messages => OpenAI Chat Completions (可用于Claude Code调用第三方模型)
-    3. OpenAI Chat Completions => Gemini Chat
-19. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费:
+    3. OpenAI Chat Completions => Gemini Chat (OpenAI格式调用Gemini模型)
+20. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费:
     1. 在 `系统设置-运营设置` 中设置 `提示缓存倍率` 选项
     2. 在渠道中设置 `提示缓存倍率`,范围 0-1,例如设置为 0.5 表示缓存命中时按照 50% 计费
     3. 支持的渠道:
@@ -119,7 +120,9 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
 4. 自定义渠道,支持填入完整调用地址
 5. Rerank模型([Cohere](https://cohere.ai/)和[Jina](https://jina.ai/)),[接口文档](https://docs.newapi.pro/api/jinaai-rerank)
 6. Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
-7. Dify,当前仅支持chatflow
+7. Google Gemini格式,[接口文档](https://docs.newapi.pro/api/google-gemini-chat/)
+8. Dify,当前仅支持chatflow
+9. 更多接口请参考[接口文档](https://docs.newapi.pro/api)
 
 ## 环境变量配置
 
@@ -128,16 +131,14 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
 - `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`
 - `STREAMING_TIMEOUT`:流式回复超时时间,默认300秒
 - `DIFY_DEBUG`:Dify渠道是否输出工作流和节点信息,默认 `true`
-- `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,默认 `true`
 - `GET_MEDIA_TOKEN`:是否统计图片token,默认 `true`
 - `GET_MEDIA_TOKEN_NOT_STREAM`:非流情况下是否统计图片token,默认 `true`
 - `UPDATE_TASK`:是否更新异步任务(Midjourney、Suno),默认 `true`
-- `COHERE_SAFETY_SETTING`:Cohere模型安全设置,可选值为 `NONE`, `CONTEXTUAL`, `STRICT`,默认 `NONE`
 - `GEMINI_VISION_MAX_IMAGE_NUM`:Gemini模型最大图片数量,默认 `16`
 - `MAX_FILE_DOWNLOAD_MB`: 最大文件下载大小,单位MB,默认 `20`
-- `CRYPTO_SECRET`:加密密钥,用于加密数据库内容
+- `CRYPTO_SECRET`:加密密钥,用于加密Redis数据库内容
 - `AZURE_DEFAULT_API_VERSION`:Azure渠道默认API版本,默认 `2025-04-01-preview`
-- `NOTIFICATION_LIMIT_DURATION_MINUTE`:通知限制持续时间,默认 `10`分钟
+- `NOTIFICATION_LIMIT_DURATION_MINUTE`:邮件等通知限制持续时间,默认 `10`分钟
 - `NOTIFY_LIMIT_COUNT`:用户通知在指定持续时间内的最大数量,默认 `2`
 - `ERROR_LOG_ENABLED=true`: 是否记录并显示错误日志,默认`false`
 
@@ -164,12 +165,18 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
 
 #### 使用Docker Compose部署(推荐)
 ```shell
-# 下载项目
-git clone https://github.com/Calcium-Ion/new-api.git
+# 下载项目源码
+git clone https://github.com/QuantumNous/new-api.git
+
+# 进入项目目录
 cd new-api
-# 按需编辑docker-compose.yml
-# 启动
-docker-compose up -d
+
+# 根据需要编辑 docker-compose.yml 文件
+# 使用nano编辑器
+nano docker-compose.yml
+# 或使用vim编辑器
+# vim docker-compose.yml
+
 ```
 
 #### 直接使用Docker镜像
@@ -182,7 +189,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
 ```
 
 ## 渠道重试与缓存
-渠道重试功能已经实现,可以在`设置->运营设置->通用设置`设置重试次数,**建议开启缓存**功能。
+渠道重试功能已经实现,可以在`设置->运营设置->通用设置->失败重试次数`设置重试次数,**建议开启缓存**功能。
 
 ### 缓存设置方法
 1. `REDIS_CONN_STRING`:设置Redis作为缓存
@@ -192,16 +199,17 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
 
 详细接口文档请参考[接口文档](https://docs.newapi.pro/api):
 
-- [聊天接口(Chat)](https://docs.newapi.pro/api/openai-chat)
+- [聊天接口(Chat Completions)](https://docs.newapi.pro/api/openai-chat)
+- [响应接口 (Responses)](https://docs.newapi.pro/api/openai-responses)
 - [图像接口(Image)](https://docs.newapi.pro/api/openai-image)
 - [重排序接口(Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
 - [实时对话接口(Realtime)](https://docs.newapi.pro/api/openai-realtime)
-- [Claude聊天接口(messages)](https://docs.newapi.pro/api/anthropic-chat)
+- [Claude聊天接口](https://docs.newapi.pro/api/anthropic-chat)
+- [Google Gemini聊天接口](https://docs.newapi.pro/api/google-gemini-chat)
 
 ## 相关项目
 - [One API](https://github.com/songquanpeng/one-api):原版项目
 - [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourney接口支持
-- [chatnio](https://github.com/Deeptrain-Community/chatnio):下一代AI一站式B/C端解决方案
 - [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool):用key查询使用额度
 
 其他基于New API的项目:

+ 5 - 1
common/api_type.go

@@ -1,6 +1,6 @@
 package common
 
-import "one-api/constant"
+import "github.com/QuantumNous/new-api/constant"
 
 func ChannelType2APIType(channelType int) (int, bool) {
 	apiType := -1
@@ -67,6 +67,10 @@ func ChannelType2APIType(channelType int) (int, bool) {
 		apiType = constant.APITypeJimeng
 	case constant.ChannelTypeMoonshot:
 		apiType = constant.APITypeMoonshot
+	case constant.ChannelTypeSubmodel:
+		apiType = constant.APITypeSubmodel
+	case constant.ChannelTypeMiniMax:
+		apiType = constant.APITypeMiniMax
 	}
 	if apiType == -1 {
 		return constant.APITypeOpenAI, false

+ 1 - 0
common/constants.go

@@ -19,6 +19,7 @@ var TopUpLink = ""
 // var ChatLink = ""
 // var ChatLink2 = ""
 var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
+// 保留旧变量以兼容历史逻辑,实际展示由 general_setting.quota_display_type 控制
 var DisplayInCurrencyEnabled = true
 var DisplayTokenStatEnabled = true
 var DrawingEnabled = true

+ 1 - 0
common/crypto.go

@@ -4,6 +4,7 @@ import (
 	"crypto/hmac"
 	"crypto/sha256"
 	"encoding/hex"
+
 	"golang.org/x/crypto/bcrypt"
 )
 

+ 1 - 1
common/database.go

@@ -12,4 +12,4 @@ var LogSqlType = DatabaseTypeSQLite // Default to SQLite for logging SQL queries
 var UsingMySQL = false
 var UsingClickHouse = false
 
-var SQLitePath = "one-api.db?_busy_timeout=30000"
+var SQLitePath = "one-api.db?_busy_timeout=30000"

+ 3 - 0
common/email.go

@@ -86,5 +86,8 @@ func SendEmail(subject string, receiver string, content string) error {
 	} else {
 		err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
 	}
+	if err != nil {
+		SysError(fmt.Sprintf("failed to send email to %s: %v", receiver, err))
+	}
 	return err
 }

+ 2 - 1
common/embed-file-system.go

@@ -2,9 +2,10 @@ package common
 
 import (
 	"embed"
-	"github.com/gin-contrib/static"
 	"io/fs"
 	"net/http"
+
+	"github.com/gin-contrib/static"
 )
 
 // Credit: https://github.com/gin-contrib/static/issues/19

+ 2 - 1
common/endpoint_defaults.go

@@ -1,6 +1,6 @@
 package common
 
-import "one-api/constant"
+import "github.com/QuantumNous/new-api/constant"
 
 // EndpointInfo 描述单个端点的默认请求信息
 // path: 上游路径
@@ -23,6 +23,7 @@ var defaultEndpointInfoMap = map[constant.EndpointType]EndpointInfo{
 	constant.EndpointTypeGemini:          {Path: "/v1beta/models/{model}:generateContent", Method: "POST"},
 	constant.EndpointTypeJinaRerank:      {Path: "/rerank", Method: "POST"},
 	constant.EndpointTypeImageGeneration: {Path: "/v1/images/generations", Method: "POST"},
+	constant.EndpointTypeEmbeddings:      {Path: "/v1/embeddings", Method: "POST"},
 }
 
 // GetDefaultEndpointInfo 返回指定端点类型的默认信息以及是否存在

+ 3 - 1
common/endpoint_type.go

@@ -1,6 +1,6 @@
 package common
 
-import "one-api/constant"
+import "github.com/QuantumNous/new-api/constant"
 
 // GetEndpointTypesByChannelType 获取渠道最优先端点类型(所有的渠道都支持 OpenAI 端点)
 func GetEndpointTypesByChannelType(channelType int, modelName string) []constant.EndpointType {
@@ -26,6 +26,8 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant
 		endpointTypes = []constant.EndpointType{constant.EndpointTypeGemini, constant.EndpointTypeOpenAI}
 	case constant.ChannelTypeOpenRouter: // OpenRouter 只支持 OpenAI 端点
 		endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI}
+	case constant.ChannelTypeSora:
+		endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAIVideo}
 	default:
 		if IsOpenAIResponseOnlyModel(modelName) {
 			endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAIResponse}

+ 26 - 1
common/gin.go

@@ -3,11 +3,13 @@ package common
 import (
 	"bytes"
 	"io"
+	"mime/multipart"
 	"net/http"
-	"one-api/constant"
 	"strings"
 	"time"
 
+	"github.com/QuantumNous/new-api/constant"
+
 	"github.com/gin-gonic/gin"
 )
 
@@ -113,3 +115,26 @@ func ApiSuccess(c *gin.Context, data any) {
 		"data":    data,
 	})
 }
+
+func ParseMultipartFormReusable(c *gin.Context) (*multipart.Form, error) {
+	requestBody, err := GetRequestBody(c)
+	if err != nil {
+		return nil, err
+	}
+
+	contentType := c.Request.Header.Get("Content-Type")
+	boundary := ""
+	if idx := strings.Index(contentType, "boundary="); idx != -1 {
+		boundary = contentType[idx+9:]
+	}
+
+	reader := multipart.NewReader(bytes.NewReader(requestBody), boundary)
+	form, err := reader.ReadForm(32 << 20) // 32 MB max memory
+	if err != nil {
+		return nil, err
+	}
+
+	// Reset request body
+	c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
+	return form, nil
+}

+ 2 - 1
common/gopool.go

@@ -3,8 +3,9 @@ package common
 import (
 	"context"
 	"fmt"
-	"github.com/bytedance/gopkg/util/gopool"
 	"math"
+
+	"github.com/bytedance/gopkg/util/gopool"
 )
 
 var relayGoPool gopool.Pool

+ 20 - 5
common/init.go

@@ -4,11 +4,13 @@ import (
 	"flag"
 	"fmt"
 	"log"
-	"one-api/constant"
 	"os"
 	"path/filepath"
 	"strconv"
+	"strings"
 	"time"
+
+	"github.com/QuantumNous/new-api/constant"
 )
 
 var (
@@ -19,10 +21,10 @@ var (
 )
 
 func printHelp() {
-	fmt.Println("New API " + Version + " - All in one API service for OpenAI API.")
-	fmt.Println("Copyright (C) 2023 JustSong. All rights reserved.")
-	fmt.Println("GitHub: https://github.com/songquanpeng/one-api")
-	fmt.Println("Usage: one-api [--port <port>] [--log-dir <log directory>] [--version] [--help]")
+	fmt.Println("NewAPI(Based OneAPI) " + Version + " - The next-generation LLM gateway and AI asset management system supports multiple languages.")
+	fmt.Println("Original Project: OneAPI by JustSong - https://github.com/songquanpeng/one-api")
+	fmt.Println("Maintainer: QuantumNous - https://github.com/QuantumNous/new-api")
+	fmt.Println("Usage: newapi [--port <port>] [--log-dir <log directory>] [--version] [--help]")
 }
 
 func InitEnv() {
@@ -117,4 +119,17 @@ func initConstantEnv() {
 	constant.GenerateDefaultToken = GetEnvOrDefaultBool("GENERATE_DEFAULT_TOKEN", false)
 	// 是否启用错误日志
 	constant.ErrorLogEnabled = GetEnvOrDefaultBool("ERROR_LOG_ENABLED", false)
+
+	soraPatchStr := GetEnvOrDefaultString("TASK_PRICE_PATCH", "")
+	if soraPatchStr != "" {
+		var taskPricePatches []string
+		soraPatches := strings.Split(soraPatchStr, ",")
+		for _, patch := range soraPatches {
+			trimmedPatch := strings.TrimSpace(patch)
+			if trimmedPatch != "" {
+				taskPricePatches = append(taskPricePatches, trimmedPatch)
+			}
+		}
+		constant.TaskPricePatches = taskPricePatches
+	}
 }

+ 2 - 1
common/json.go

@@ -3,6 +3,7 @@ package common
 import (
 	"bytes"
 	"encoding/json"
+	"io"
 )
 
 func Unmarshal(data []byte, v any) error {
@@ -13,7 +14,7 @@ func UnmarshalJsonStr(data string, v any) error {
 	return json.Unmarshal(StringToByteSlice(data), v)
 }
 
-func DecodeJson(reader *bytes.Reader, v any) error {
+func DecodeJson(reader io.Reader, v any) error {
 	return json.NewDecoder(reader).Decode(v)
 }
 

+ 3 - 2
common/limiter/limiter.go

@@ -4,9 +4,10 @@ import (
 	"context"
 	_ "embed"
 	"fmt"
-	"github.com/go-redis/redis/v8"
-	"one-api/common"
 	"sync"
+
+	"github.com/QuantumNous/new-api/common"
+	"github.com/go-redis/redis/v8"
 )
 
 //go:embed lua/rate_limit.lua

+ 2 - 1
common/pprof.go

@@ -2,10 +2,11 @@ package common
 
 import (
 	"fmt"
-	"github.com/shirou/gopsutil/cpu"
 	"os"
 	"runtime/pprof"
 	"time"
+
+	"github.com/shirou/gopsutil/cpu"
 )
 
 // Monitor 定时监控cpu使用率,超过阈值输出pprof文件

+ 2 - 1
common/verification.go

@@ -1,10 +1,11 @@
 package common
 
 import (
-	"github.com/google/uuid"
 	"strings"
 	"sync"
 	"time"
+
+	"github.com/google/uuid"
 )
 
 type verificationValue struct {

+ 4 - 2
constant/api_type.go

@@ -31,6 +31,8 @@ const (
 	APITypeXai
 	APITypeCoze
 	APITypeJimeng
-	APITypeMoonshot // this one is only for count, do not add any channel after this
-	APITypeDummy    // this one is only for count, do not add any channel after this
+	APITypeMoonshot
+	APITypeSubmodel
+	APITypeMiniMax
+	APITypeDummy // this one is only for count, do not add any channel after this
 )

+ 68 - 0
constant/channel.go

@@ -50,6 +50,9 @@ const (
 	ChannelTypeKling          = 50
 	ChannelTypeJimeng         = 51
 	ChannelTypeVidu           = 52
+	ChannelTypeSubmodel       = 53
+	ChannelTypeDoubaoVideo    = 54
+	ChannelTypeSora           = 55
 	ChannelTypeDummy          // this one is only for count, do not add any channel after this
 
 )
@@ -108,4 +111,69 @@ var ChannelBaseURLs = []string{
 	"https://api.klingai.com",                   //50
 	"https://visual.volcengineapi.com",          //51
 	"https://api.vidu.cn",                       //52
+	"https://llm.submodel.ai",                   //53
+	"https://ark.cn-beijing.volces.com",         //54
+	"https://api.openai.com",                    //55
+}
+
+var ChannelTypeNames = map[int]string{
+	ChannelTypeUnknown:        "Unknown",
+	ChannelTypeOpenAI:         "OpenAI",
+	ChannelTypeMidjourney:     "Midjourney",
+	ChannelTypeAzure:          "Azure",
+	ChannelTypeOllama:         "Ollama",
+	ChannelTypeMidjourneyPlus: "MidjourneyPlus",
+	ChannelTypeOpenAIMax:      "OpenAIMax",
+	ChannelTypeOhMyGPT:        "OhMyGPT",
+	ChannelTypeCustom:         "Custom",
+	ChannelTypeAILS:           "AILS",
+	ChannelTypeAIProxy:        "AIProxy",
+	ChannelTypePaLM:           "PaLM",
+	ChannelTypeAPI2GPT:        "API2GPT",
+	ChannelTypeAIGC2D:         "AIGC2D",
+	ChannelTypeAnthropic:      "Anthropic",
+	ChannelTypeBaidu:          "Baidu",
+	ChannelTypeZhipu:          "Zhipu",
+	ChannelTypeAli:            "Ali",
+	ChannelTypeXunfei:         "Xunfei",
+	ChannelType360:            "360",
+	ChannelTypeOpenRouter:     "OpenRouter",
+	ChannelTypeAIProxyLibrary: "AIProxyLibrary",
+	ChannelTypeFastGPT:        "FastGPT",
+	ChannelTypeTencent:        "Tencent",
+	ChannelTypeGemini:         "Gemini",
+	ChannelTypeMoonshot:       "Moonshot",
+	ChannelTypeZhipu_v4:       "ZhipuV4",
+	ChannelTypePerplexity:     "Perplexity",
+	ChannelTypeLingYiWanWu:    "LingYiWanWu",
+	ChannelTypeAws:            "AWS",
+	ChannelTypeCohere:         "Cohere",
+	ChannelTypeMiniMax:        "MiniMax",
+	ChannelTypeSunoAPI:        "SunoAPI",
+	ChannelTypeDify:           "Dify",
+	ChannelTypeJina:           "Jina",
+	ChannelCloudflare:         "Cloudflare",
+	ChannelTypeSiliconFlow:    "SiliconFlow",
+	ChannelTypeVertexAi:       "VertexAI",
+	ChannelTypeMistral:        "Mistral",
+	ChannelTypeDeepSeek:       "DeepSeek",
+	ChannelTypeMokaAI:         "MokaAI",
+	ChannelTypeVolcEngine:     "VolcEngine",
+	ChannelTypeBaiduV2:        "BaiduV2",
+	ChannelTypeXinference:     "Xinference",
+	ChannelTypeXai:            "xAI",
+	ChannelTypeCoze:           "Coze",
+	ChannelTypeKling:          "Kling",
+	ChannelTypeJimeng:         "Jimeng",
+	ChannelTypeVidu:           "Vidu",
+	ChannelTypeSubmodel:       "Submodel",
+	ChannelTypeDoubaoVideo:    "DoubaoVideo",
+	ChannelTypeSora:           "Sora",
+}
+
+func GetChannelTypeName(channelType int) string {
+	if name, ok := ChannelTypeNames[channelType]; ok {
+		return name
+	}
+	return "Unknown"
 }

+ 2 - 0
constant/endpoint_type.go

@@ -9,6 +9,8 @@ const (
 	EndpointTypeGemini          EndpointType = "gemini"
 	EndpointTypeJinaRerank      EndpointType = "jina-rerank"
 	EndpointTypeImageGeneration EndpointType = "image-generation"
+	EndpointTypeEmbeddings      EndpointType = "embeddings"
+	EndpointTypeOpenAIVideo     EndpointType = "openai-video"
 	//EndpointTypeMidjourney     EndpointType = "midjourney-proxy"
 	//EndpointTypeSuno           EndpointType = "suno-proxy"
 	//EndpointTypeKling          EndpointType = "kling"

+ 3 - 0
constant/env.go

@@ -13,3 +13,6 @@ var NotifyLimitCount int
 var NotificationLimitDurationMinute int
 var GenerateDefaultToken bool
 var ErrorLogEnabled bool
+
+// temporary variable for sora patch, will be removed in future
+var TaskPricePatches []string

+ 23 - 7
controller/billing.go

@@ -1,10 +1,11 @@
 package controller
 
 import (
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/dto"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/setting/operation_setting"
 	"github.com/gin-gonic/gin"
-	"one-api/common"
-	"one-api/dto"
-	"one-api/model"
 )
 
 func GetSubscription(c *gin.Context) {
@@ -39,8 +40,18 @@ func GetSubscription(c *gin.Context) {
 	}
 	quota := remainQuota + usedQuota
 	amount := float64(quota)
-	if common.DisplayInCurrencyEnabled {
-		amount /= common.QuotaPerUnit
+	// OpenAI 兼容接口中的 *_USD 字段含义保持“额度单位”对应值:
+	// 我们将其解释为以“站点展示类型”为准:
+	// - USD: 直接除以 QuotaPerUnit
+	// - CNY: 先转 USD 再乘汇率
+	// - TOKENS: 直接使用 tokens 数量
+	switch operation_setting.GetQuotaDisplayType() {
+	case operation_setting.QuotaDisplayTypeCNY:
+		amount = amount / common.QuotaPerUnit * operation_setting.USDExchangeRate
+	case operation_setting.QuotaDisplayTypeTokens:
+		// amount 保持 tokens 数值
+	default:
+		amount = amount / common.QuotaPerUnit
 	}
 	if token != nil && token.UnlimitedQuota {
 		amount = 100000000
@@ -80,8 +91,13 @@ func GetUsage(c *gin.Context) {
 		return
 	}
 	amount := float64(quota)
-	if common.DisplayInCurrencyEnabled {
-		amount /= common.QuotaPerUnit
+	switch operation_setting.GetQuotaDisplayType() {
+	case operation_setting.QuotaDisplayTypeCNY:
+		amount = amount / common.QuotaPerUnit * operation_setting.USDExchangeRate
+	case operation_setting.QuotaDisplayTypeTokens:
+		// tokens 保持原值
+	default:
+		amount = amount / common.QuotaPerUnit
 	}
 	usage := OpenAIUsageResponse{
 		Object:     "list",

+ 15 - 6
controller/channel-billing.go

@@ -6,15 +6,16 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"one-api/common"
-	"one-api/constant"
-	"one-api/model"
-	"one-api/service"
-	"one-api/setting/operation_setting"
-	"one-api/types"
 	"strconv"
 	"time"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/constant"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/service"
+	"github.com/QuantumNous/new-api/setting/operation_setting"
+	"github.com/QuantumNous/new-api/types"
+
 	"github.com/shopspring/decimal"
 
 	"github.com/gin-gonic/gin"
@@ -127,6 +128,14 @@ func GetAuthHeader(token string) http.Header {
 	return h
 }
 
+// GetClaudeAuthHeader get claude auth header
+func GetClaudeAuthHeader(token string) http.Header {
+	h := http.Header{}
+	h.Add("x-api-key", token)
+	h.Add("anthropic-version", "2023-06-01")
+	return h
+}
+
 func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) {
 	req, err := http.NewRequest(method, url, nil)
 	if err != nil {

+ 238 - 141
controller/channel-test.go

@@ -10,24 +10,26 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"net/url"
-	"one-api/common"
-	"one-api/constant"
-	"one-api/dto"
-	"one-api/middleware"
-	"one-api/model"
-	"one-api/relay"
-	relaycommon "one-api/relay/common"
-	relayconstant "one-api/relay/constant"
-	"one-api/relay/helper"
-	"one-api/service"
-	"one-api/setting/operation_setting"
-	"one-api/types"
 	"strconv"
 	"strings"
 	"sync"
 	"time"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/constant"
+	"github.com/QuantumNous/new-api/dto"
+	"github.com/QuantumNous/new-api/middleware"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/relay"
+	relaycommon "github.com/QuantumNous/new-api/relay/common"
+	relayconstant "github.com/QuantumNous/new-api/relay/constant"
+	"github.com/QuantumNous/new-api/relay/helper"
+	"github.com/QuantumNous/new-api/service"
+	"github.com/QuantumNous/new-api/setting/operation_setting"
+	"github.com/QuantumNous/new-api/types"
+
 	"github.com/bytedance/gopkg/util/gopool"
+	"github.com/samber/lo"
 
 	"github.com/gin-gonic/gin"
 )
@@ -38,95 +40,70 @@ type testResult struct {
 	newAPIError *types.NewAPIError
 }
 
-func testChannel(channel *model.Channel, testModel string) testResult {
+func testChannel(channel *model.Channel, testModel string, endpointType string) testResult {
 	tik := time.Now()
-	if channel.Type == constant.ChannelTypeMidjourney {
+	var unsupportedTestChannelTypes = []int{
+		constant.ChannelTypeMidjourney,
+		constant.ChannelTypeMidjourneyPlus,
+		constant.ChannelTypeSunoAPI,
+		constant.ChannelTypeKling,
+		constant.ChannelTypeJimeng,
+		constant.ChannelTypeDoubaoVideo,
+		constant.ChannelTypeVidu,
+	}
+	if lo.Contains(unsupportedTestChannelTypes, channel.Type) {
+		channelTypeName := constant.GetChannelTypeName(channel.Type)
 		return testResult{
-			localErr:    errors.New("midjourney channel test is not supported"),
-			newAPIError: nil,
-		}
-	}
-	if channel.Type == constant.ChannelTypeMidjourneyPlus {
-		return testResult{
-			localErr:    errors.New("midjourney plus channel test is not supported"),
-			newAPIError: nil,
-		}
-	}
-	if channel.Type == constant.ChannelTypeSunoAPI {
-		return testResult{
-			localErr:    errors.New("suno channel test is not supported"),
-			newAPIError: nil,
-		}
-	}
-	if channel.Type == constant.ChannelTypeKling {
-		return testResult{
-			localErr:    errors.New("kling channel test is not supported"),
-			newAPIError: nil,
-		}
-	}
-	if channel.Type == constant.ChannelTypeJimeng {
-		return testResult{
-			localErr:    errors.New("jimeng channel test is not supported"),
-			newAPIError: nil,
-		}
-	}
-	if channel.Type == constant.ChannelTypeVidu {
-		return testResult{
-			localErr:    errors.New("vidu channel test is not supported"),
-			newAPIError: nil,
+			localErr: fmt.Errorf("%s channel test is not supported", channelTypeName),
 		}
 	}
 	w := httptest.NewRecorder()
 	c, _ := gin.CreateTestContext(w)
 
-	requestPath := "/v1/chat/completions"
-
-	// 先判断是否为 Embedding 模型
-	if strings.Contains(strings.ToLower(testModel), "embedding") ||
-		strings.HasPrefix(testModel, "m3e") || // m3e 系列模型
-		strings.Contains(testModel, "bge-") || // bge 系列模型
-		strings.Contains(testModel, "embed") ||
-		channel.Type == constant.ChannelTypeMokaAI { // 其他 embedding 模型
-		requestPath = "/v1/embeddings" // 修改请求路径
-	}
-
-	// VolcEngine 图像生成模型
-	if channel.Type == constant.ChannelTypeVolcEngine && strings.Contains(testModel, "seedream") {
-		requestPath = "/v1/images/generations"
-	}
-
-	c.Request = &http.Request{
-		Method: "POST",
-		URL:    &url.URL{Path: requestPath}, // 使用动态路径
-		Body:   nil,
-		Header: make(http.Header),
-	}
-
+	testModel = strings.TrimSpace(testModel)
 	if testModel == "" {
 		if channel.TestModel != nil && *channel.TestModel != "" {
-			testModel = *channel.TestModel
+			testModel = strings.TrimSpace(*channel.TestModel)
 		} else {
-			if len(channel.GetModels()) > 0 {
-				testModel = channel.GetModels()[0]
-			} else {
+			models := channel.GetModels()
+			if len(models) > 0 {
+				testModel = strings.TrimSpace(models[0])
+			}
+			if testModel == "" {
 				testModel = "gpt-4o-mini"
 			}
 		}
 	}
 
-	// 重新检查模型类型并更新请求路径
-	if strings.Contains(strings.ToLower(testModel), "embedding") ||
-		strings.HasPrefix(testModel, "m3e") ||
-		strings.Contains(testModel, "bge-") ||
-		strings.Contains(testModel, "embed") ||
-		channel.Type == constant.ChannelTypeMokaAI {
-		requestPath = "/v1/embeddings"
-		c.Request.URL.Path = requestPath
+	requestPath := "/v1/chat/completions"
+
+	// 如果指定了端点类型,使用指定的端点类型
+	if endpointType != "" {
+		if endpointInfo, ok := common.GetDefaultEndpointInfo(constant.EndpointType(endpointType)); ok {
+			requestPath = endpointInfo.Path
+		}
+	} else {
+		// 如果没有指定端点类型,使用原有的自动检测逻辑
+		// 先判断是否为 Embedding 模型
+		if strings.Contains(strings.ToLower(testModel), "embedding") ||
+			strings.HasPrefix(testModel, "m3e") || // m3e 系列模型
+			strings.Contains(testModel, "bge-") || // bge 系列模型
+			strings.Contains(testModel, "embed") ||
+			channel.Type == constant.ChannelTypeMokaAI { // 其他 embedding 模型
+			requestPath = "/v1/embeddings" // 修改请求路径
+		}
+
+		// VolcEngine 图像生成模型
+		if channel.Type == constant.ChannelTypeVolcEngine && strings.Contains(testModel, "seedream") {
+			requestPath = "/v1/images/generations"
+		}
 	}
 
-	if channel.Type == constant.ChannelTypeVolcEngine && strings.Contains(testModel, "seedream") {
-		requestPath = "/v1/images/generations"
-		c.Request.URL.Path = requestPath
+	c.Request = &http.Request{
+		Method: "POST",
+		URL:    &url.URL{Path: requestPath}, // 使用动态路径
+		Body:   nil,
+		Header: make(http.Header),
 	}
 
 	cache, err := model.GetUserCache(1)
@@ -153,17 +130,54 @@ func testChannel(channel *model.Channel, testModel string) testResult {
 			newAPIError: newAPIError,
 		}
 	}
-	request := buildTestRequest(testModel)
 
-	// Determine relay format based on request path
-	relayFormat := types.RelayFormatOpenAI
-	if c.Request.URL.Path == "/v1/embeddings" {
-		relayFormat = types.RelayFormatEmbedding
-	}
-	if c.Request.URL.Path == "/v1/images/generations" {
-		relayFormat = types.RelayFormatOpenAIImage
+	// Determine relay format based on endpoint type or request path
+	var relayFormat types.RelayFormat
+	if endpointType != "" {
+		// 根据指定的端点类型设置 relayFormat
+		switch constant.EndpointType(endpointType) {
+		case constant.EndpointTypeOpenAI:
+			relayFormat = types.RelayFormatOpenAI
+		case constant.EndpointTypeOpenAIResponse:
+			relayFormat = types.RelayFormatOpenAIResponses
+		case constant.EndpointTypeAnthropic:
+			relayFormat = types.RelayFormatClaude
+		case constant.EndpointTypeGemini:
+			relayFormat = types.RelayFormatGemini
+		case constant.EndpointTypeJinaRerank:
+			relayFormat = types.RelayFormatRerank
+		case constant.EndpointTypeImageGeneration:
+			relayFormat = types.RelayFormatOpenAIImage
+		case constant.EndpointTypeEmbeddings:
+			relayFormat = types.RelayFormatEmbedding
+		default:
+			relayFormat = types.RelayFormatOpenAI
+		}
+	} else {
+		// 根据请求路径自动检测
+		relayFormat = types.RelayFormatOpenAI
+		if c.Request.URL.Path == "/v1/embeddings" {
+			relayFormat = types.RelayFormatEmbedding
+		}
+		if c.Request.URL.Path == "/v1/images/generations" {
+			relayFormat = types.RelayFormatOpenAIImage
+		}
+		if c.Request.URL.Path == "/v1/messages" {
+			relayFormat = types.RelayFormatClaude
+		}
+		if strings.Contains(c.Request.URL.Path, "/v1beta/models") {
+			relayFormat = types.RelayFormatGemini
+		}
+		if c.Request.URL.Path == "/v1/rerank" || c.Request.URL.Path == "/rerank" {
+			relayFormat = types.RelayFormatRerank
+		}
+		if c.Request.URL.Path == "/v1/responses" {
+			relayFormat = types.RelayFormatOpenAIResponses
+		}
 	}
 
+	request := buildTestRequest(testModel, endpointType)
+
 	info, err := relaycommon.GenRelayInfo(c, relayFormat, request, nil)
 
 	if err != nil {
@@ -186,7 +200,8 @@ func testChannel(channel *model.Channel, testModel string) testResult {
 	}
 
 	testModel = info.UpstreamModelName
-	request.Model = testModel
+	// 更新请求中的模型名称
+	request.SetModelName(testModel)
 
 	apiType, _ := common.ChannelType2APIType(channel.Type)
 	adaptor := relay.GetAdaptor(apiType)
@@ -216,33 +231,62 @@ func testChannel(channel *model.Channel, testModel string) testResult {
 
 	var convertedRequest any
 	// 根据 RelayMode 选择正确的转换函数
-	if info.RelayMode == relayconstant.RelayModeEmbeddings {
-		// 创建一个 EmbeddingRequest
-		embeddingRequest := dto.EmbeddingRequest{
-			Input: request.Input,
-			Model: request.Model,
-		}
-		// 调用专门用于 Embedding 的转换函数
-		convertedRequest, err = adaptor.ConvertEmbeddingRequest(c, info, embeddingRequest)
-	} else if info.RelayMode == relayconstant.RelayModeImagesGenerations {
-		// 创建一个 ImageRequest
-		prompt := "cat"
-		if request.Prompt != nil {
-			if promptStr, ok := request.Prompt.(string); ok && promptStr != "" {
-				prompt = promptStr
+	switch info.RelayMode {
+	case relayconstant.RelayModeEmbeddings:
+		// Embedding 请求 - request 已经是正确的类型
+		if embeddingReq, ok := request.(*dto.EmbeddingRequest); ok {
+			convertedRequest, err = adaptor.ConvertEmbeddingRequest(c, info, *embeddingReq)
+		} else {
+			return testResult{
+				context:     c,
+				localErr:    errors.New("invalid embedding request type"),
+				newAPIError: types.NewError(errors.New("invalid embedding request type"), types.ErrorCodeConvertRequestFailed),
 			}
 		}
-		imageRequest := dto.ImageRequest{
-			Prompt: prompt,
-			Model:  request.Model,
-			N:      uint(request.N),
-			Size:   request.Size,
+	case relayconstant.RelayModeImagesGenerations:
+		// 图像生成请求 - request 已经是正确的类型
+		if imageReq, ok := request.(*dto.ImageRequest); ok {
+			convertedRequest, err = adaptor.ConvertImageRequest(c, info, *imageReq)
+		} else {
+			return testResult{
+				context:     c,
+				localErr:    errors.New("invalid image request type"),
+				newAPIError: types.NewError(errors.New("invalid image request type"), types.ErrorCodeConvertRequestFailed),
+			}
+		}
+	case relayconstant.RelayModeRerank:
+		// Rerank 请求 - request 已经是正确的类型
+		if rerankReq, ok := request.(*dto.RerankRequest); ok {
+			convertedRequest, err = adaptor.ConvertRerankRequest(c, info.RelayMode, *rerankReq)
+		} else {
+			return testResult{
+				context:     c,
+				localErr:    errors.New("invalid rerank request type"),
+				newAPIError: types.NewError(errors.New("invalid rerank request type"), types.ErrorCodeConvertRequestFailed),
+			}
+		}
+	case relayconstant.RelayModeResponses:
+		// Response 请求 - request 已经是正确的类型
+		if responseReq, ok := request.(*dto.OpenAIResponsesRequest); ok {
+			convertedRequest, err = adaptor.ConvertOpenAIResponsesRequest(c, info, *responseReq)
+		} else {
+			return testResult{
+				context:     c,
+				localErr:    errors.New("invalid response request type"),
+				newAPIError: types.NewError(errors.New("invalid response request type"), types.ErrorCodeConvertRequestFailed),
+			}
+		}
+	default:
+		// Chat/Completion 等其他请求类型
+		if generalReq, ok := request.(*dto.GeneralOpenAIRequest); ok {
+			convertedRequest, err = adaptor.ConvertOpenAIRequest(c, info, generalReq)
+		} else {
+			return testResult{
+				context:     c,
+				localErr:    errors.New("invalid general request type"),
+				newAPIError: types.NewError(errors.New("invalid general request type"), types.ErrorCodeConvertRequestFailed),
+			}
 		}
-		// 调用专门用于图像生成的转换函数
-		convertedRequest, err = adaptor.ConvertImageRequest(c, info, imageRequest)
-	} else {
-		// 对其他所有请求类型(如 Chat),保持原有逻辑
-		convertedRequest, err = adaptor.ConvertOpenAIRequest(c, info, request)
 	}
 
 	if err != nil {
@@ -345,22 +389,82 @@ func testChannel(channel *model.Channel, testModel string) testResult {
 	}
 }
 
-func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
-	testRequest := &dto.GeneralOpenAIRequest{
-		Model:  "", // this will be set later
-		Stream: false,
+func buildTestRequest(model string, endpointType string) dto.Request {
+	// 根据端点类型构建不同的测试请求
+	if endpointType != "" {
+		switch constant.EndpointType(endpointType) {
+		case constant.EndpointTypeEmbeddings:
+			// 返回 EmbeddingRequest
+			return &dto.EmbeddingRequest{
+				Model: model,
+				Input: []any{"hello world"},
+			}
+		case constant.EndpointTypeImageGeneration:
+			// 返回 ImageRequest
+			return &dto.ImageRequest{
+				Model:  model,
+				Prompt: "a cute cat",
+				N:      1,
+				Size:   "1024x1024",
+			}
+		case constant.EndpointTypeJinaRerank:
+			// 返回 RerankRequest
+			return &dto.RerankRequest{
+				Model:     model,
+				Query:     "What is Deep Learning?",
+				Documents: []any{"Deep Learning is a subset of machine learning.", "Machine learning is a field of artificial intelligence."},
+				TopN:      2,
+			}
+		case constant.EndpointTypeOpenAIResponse:
+			// 返回 OpenAIResponsesRequest
+			return &dto.OpenAIResponsesRequest{
+				Model: model,
+				Input: json.RawMessage("\"hi\""),
+			}
+		case constant.EndpointTypeAnthropic, constant.EndpointTypeGemini, constant.EndpointTypeOpenAI:
+			// 返回 GeneralOpenAIRequest
+			maxTokens := uint(10)
+			if constant.EndpointType(endpointType) == constant.EndpointTypeGemini {
+				maxTokens = 3000
+			}
+			return &dto.GeneralOpenAIRequest{
+				Model:  model,
+				Stream: false,
+				Messages: []dto.Message{
+					{
+						Role:    "user",
+						Content: "hi",
+					},
+				},
+				MaxTokens: maxTokens,
+			}
+		}
 	}
 
+	// 自动检测逻辑(保持原有行为)
 	// 先判断是否为 Embedding 模型
-	if strings.Contains(strings.ToLower(model), "embedding") || // 其他 embedding 模型
-		strings.HasPrefix(model, "m3e") || // m3e 系列模型
+	if strings.Contains(strings.ToLower(model), "embedding") ||
+		strings.HasPrefix(model, "m3e") ||
 		strings.Contains(model, "bge-") {
-		testRequest.Model = model
-		// Embedding 请求
-		testRequest.Input = []any{"hello world"} // 修改为any,因为dto/openai_request.go 的ParseInput方法无法处理[]string类型
-		return testRequest
+		// 返回 EmbeddingRequest
+		return &dto.EmbeddingRequest{
+			Model: model,
+			Input: []any{"hello world"},
+		}
 	}
-	// 并非Embedding 模型
+
+	// Chat/Completion 请求 - 返回 GeneralOpenAIRequest
+	testRequest := &dto.GeneralOpenAIRequest{
+		Model:  model,
+		Stream: false,
+		Messages: []dto.Message{
+			{
+				Role:    "user",
+				Content: "hi",
+			},
+		},
+	}
+
 	if strings.HasPrefix(model, "o") {
 		testRequest.MaxCompletionTokens = 10
 	} else if strings.Contains(model, "thinking") {
@@ -373,12 +477,6 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
 		testRequest.MaxTokens = 10
 	}
 
-	testMessage := dto.Message{
-		Role:    "user",
-		Content: "hi",
-	}
-	testRequest.Model = model
-	testRequest.Messages = append(testRequest.Messages, testMessage)
 	return testRequest
 }
 
@@ -402,8 +500,9 @@ func TestChannel(c *gin.Context) {
 	//	}
 	//}()
 	testModel := c.Query("model")
+	endpointType := c.Query("endpoint_type")
 	tik := time.Now()
-	result := testChannel(channel, testModel)
+	result := testChannel(channel, testModel, endpointType)
 	if result.localErr != nil {
 		c.JSON(http.StatusOK, gin.H{
 			"success": false,
@@ -429,7 +528,6 @@ func TestChannel(c *gin.Context) {
 		"message": "",
 		"time":    consumedTime,
 	})
-	return
 }
 
 var testAllChannelsLock sync.Mutex
@@ -463,7 +561,7 @@ func testAllChannels(notify bool) error {
 		for _, channel := range channels {
 			isChannelEnabled := channel.Status == common.ChannelStatusEnabled
 			tik := time.Now()
-			result := testChannel(channel, "")
+			result := testChannel(channel, "", "")
 			tok := time.Now()
 			milliseconds := tok.Sub(tik).Milliseconds()
 
@@ -477,7 +575,7 @@ func testAllChannels(notify bool) error {
 			// 当错误检查通过,才检查响应时间
 			if common.AutomaticDisableChannelEnabled && !shouldBanChannel {
 				if milliseconds > disableThreshold {
-					err := errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0))
+					err := fmt.Errorf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)
 					newAPIError = types.NewOpenAIError(err, types.ErrorCodeChannelResponseTimeExceeded, http.StatusRequestTimeout)
 					shouldBanChannel = true
 				}
@@ -514,7 +612,6 @@ func TestAllChannels(c *gin.Context) {
 		"success": true,
 		"message": "",
 	})
-	return
 }
 
 var autoTestChannelsOnce sync.Once
@@ -526,10 +623,10 @@ func AutomaticallyTestChannels() {
 				time.Sleep(10 * time.Minute)
 				continue
 			}
-			frequency := operation_setting.GetMonitorSetting().AutoTestChannelMinutes
-			common.SysLog(fmt.Sprintf("automatically test channels with interval %d minutes", frequency))
 			for {
+				frequency := operation_setting.GetMonitorSetting().AutoTestChannelMinutes
 				time.Sleep(time.Duration(frequency) * time.Minute)
+				common.SysLog(fmt.Sprintf("automatically test channels with interval %d minutes", frequency))
 				common.SysLog("automatically testing all channels")
 				_ = testAllChannels(false)
 				common.SysLog("automatically channel test finished")

+ 16 - 38
controller/channel.go

@@ -4,13 +4,15 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
-	"one-api/common"
-	"one-api/constant"
-	"one-api/dto"
-	"one-api/model"
 	"strconv"
 	"strings"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/constant"
+	"github.com/QuantumNous/new-api/dto"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/service"
+
 	"github.com/gin-gonic/gin"
 )
 
@@ -197,9 +199,10 @@ func FetchUpstreamModels(c *gin.Context) {
 	// 获取响应体 - 根据渠道类型决定是否添加 AuthHeader
 	var body []byte
 	key := strings.Split(channel.Key, "\n")[0]
-	if channel.Type == constant.ChannelTypeGemini {
-		body, err = GetResponseBody("GET", url, channel, GetAuthHeader(key)) // Use AuthHeader since Gemini now forces it
-	} else {
+	switch channel.Type {
+	case constant.ChannelTypeAnthropic:
+		body, err = GetResponseBody("GET", url, channel, GetClaudeAuthHeader(key))
+	default:
 		body, err = GetResponseBody("GET", url, channel, GetAuthHeader(key))
 	}
 	if err != nil {
@@ -383,18 +386,9 @@ func GetChannel(c *gin.Context) {
 	return
 }
 
-// GetChannelKey 验证2FA后获取渠道密钥
+// GetChannelKey 获取渠道密钥(需要通过安全验证中间件)
+// 此函数依赖 SecureVerificationRequired 中间件,确保用户已通过安全验证
 func GetChannelKey(c *gin.Context) {
-	type GetChannelKeyRequest struct {
-		Code string `json:"code" binding:"required"`
-	}
-
-	var req GetChannelKeyRequest
-	if err := c.ShouldBindJSON(&req); err != nil {
-		common.ApiError(c, fmt.Errorf("参数错误: %v", err))
-		return
-	}
-
 	userId := c.GetInt("id")
 	channelId, err := strconv.Atoi(c.Param("id"))
 	if err != nil {
@@ -402,24 +396,6 @@ func GetChannelKey(c *gin.Context) {
 		return
 	}
 
-	// 获取2FA记录并验证
-	twoFA, err := model.GetTwoFAByUserId(userId)
-	if err != nil {
-		common.ApiError(c, fmt.Errorf("获取2FA信息失败: %v", err))
-		return
-	}
-
-	if twoFA == nil || !twoFA.IsEnabled {
-		common.ApiError(c, fmt.Errorf("用户未启用2FA,无法查看密钥"))
-		return
-	}
-
-	// 统一的2FA验证逻辑
-	if !validateTwoFactorAuth(twoFA, req.Code) {
-		common.ApiError(c, fmt.Errorf("验证码或备用码错误,请重试"))
-		return
-	}
-
 	// 获取渠道信息(包含密钥)
 	channel, err := model.GetChannelById(channelId, true)
 	if err != nil {
@@ -435,10 +411,10 @@ func GetChannelKey(c *gin.Context) {
 	// 记录操作日志
 	model.RecordLog(userId, model.LogTypeSystem, fmt.Sprintf("查看渠道密钥信息 (渠道ID: %d)", channelId))
 
-	// 统一的成功响应格式
+	// 返回渠道密钥
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
-		"message": "验证成功",
+		"message": "获取成功",
 		"data": map[string]interface{}{
 			"key": channel.Key,
 		},
@@ -633,6 +609,7 @@ func AddChannel(c *gin.Context) {
 		common.ApiError(c, err)
 		return
 	}
+	service.ResetProxyClientCache()
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",
@@ -894,6 +871,7 @@ func UpdateChannel(c *gin.Context) {
 		return
 	}
 	model.InitChannelCache()
+	service.ResetProxyClientCache()
 	channel.Key = ""
 	clearChannelInfo(&channel.Channel)
 	c.JSON(http.StatusOK, gin.H{

+ 3 - 2
controller/console_migrate.go

@@ -5,8 +5,9 @@ package controller
 import (
 	"encoding/json"
 	"net/http"
-	"one-api/common"
-	"one-api/model"
+
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
 
 	"github.com/gin-gonic/gin"
 )

+ 3 - 2
controller/github.go

@@ -6,11 +6,12 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
-	"one-api/common"
-	"one-api/model"
 	"strconv"
 	"time"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
+
 	"github.com/gin-contrib/sessions"
 	"github.com/gin-gonic/gin"
 )

+ 4 - 3
controller/group.go

@@ -2,9 +2,10 @@ package controller
 
 import (
 	"net/http"
-	"one-api/model"
-	"one-api/setting"
-	"one-api/setting/ratio_setting"
+
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/setting"
+	"github.com/QuantumNous/new-api/setting/ratio_setting"
 
 	"github.com/gin-gonic/gin"
 )

+ 3 - 2
controller/linuxdo.go

@@ -7,12 +7,13 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
-	"one-api/common"
-	"one-api/model"
 	"strconv"
 	"strings"
 	"time"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
+
 	"github.com/gin-contrib/sessions"
 	"github.com/gin-gonic/gin"
 )

+ 3 - 2
controller/log.go

@@ -2,10 +2,11 @@ package controller
 
 import (
 	"net/http"
-	"one-api/common"
-	"one-api/model"
 	"strconv"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
+
 	"github.com/gin-gonic/gin"
 )
 

+ 8 - 7
controller/midjourney.go

@@ -7,15 +7,16 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"one-api/common"
-	"one-api/dto"
-	"one-api/logger"
-	"one-api/model"
-	"one-api/service"
-	"one-api/setting"
-	"one-api/setting/system_setting"
 	"time"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/dto"
+	"github.com/QuantumNous/new-api/logger"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/service"
+	"github.com/QuantumNous/new-api/setting"
+	"github.com/QuantumNous/new-api/setting/system_setting"
+
 	"github.com/gin-gonic/gin"
 )
 

+ 55 - 20
controller/misc.go

@@ -4,16 +4,17 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
-	"one-api/common"
-	"one-api/constant"
-	"one-api/middleware"
-	"one-api/model"
-	"one-api/setting"
-	"one-api/setting/console_setting"
-	"one-api/setting/operation_setting"
-	"one-api/setting/system_setting"
 	"strings"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/constant"
+	"github.com/QuantumNous/new-api/middleware"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/setting"
+	"github.com/QuantumNous/new-api/setting/console_setting"
+	"github.com/QuantumNous/new-api/setting/operation_setting"
+	"github.com/QuantumNous/new-api/setting/system_setting"
+
 	"github.com/gin-gonic/gin"
 )
 
@@ -42,6 +43,9 @@ func GetStatus(c *gin.Context) {
 	common.OptionMapRWMutex.RLock()
 	defer common.OptionMapRWMutex.RUnlock()
 
+	passkeySetting := system_setting.GetPasskeySettings()
+	legalSetting := system_setting.GetLegalSettings()
+
 	data := gin.H{
 		"version":                     common.Version,
 		"start_time":                  common.StartTime,
@@ -64,18 +68,22 @@ func GetStatus(c *gin.Context) {
 		"top_up_link":                 common.TopUpLink,
 		"docs_link":                   operation_setting.GetGeneralSetting().DocsLink,
 		"quota_per_unit":              common.QuotaPerUnit,
-		"display_in_currency":         common.DisplayInCurrencyEnabled,
-		"enable_batch_update":         common.BatchUpdateEnabled,
-		"enable_drawing":              common.DrawingEnabled,
-		"enable_task":                 common.TaskEnabled,
-		"enable_data_export":          common.DataExportEnabled,
-		"data_export_default_time":    common.DataExportDefaultTime,
-		"default_collapse_sidebar":    common.DefaultCollapseSidebar,
-		"mj_notify_enabled":           setting.MjNotifyEnabled,
-		"chats":                       setting.Chats,
-		"demo_site_enabled":           operation_setting.DemoSiteEnabled,
-		"self_use_mode_enabled":       operation_setting.SelfUseModeEnabled,
-		"default_use_auto_group":      setting.DefaultUseAutoGroup,
+		// 兼容旧前端:保留 display_in_currency,同时提供新的 quota_display_type
+		"display_in_currency":           operation_setting.IsCurrencyDisplay(),
+		"quota_display_type":            operation_setting.GetQuotaDisplayType(),
+		"custom_currency_symbol":        operation_setting.GetGeneralSetting().CustomCurrencySymbol,
+		"custom_currency_exchange_rate": operation_setting.GetGeneralSetting().CustomCurrencyExchangeRate,
+		"enable_batch_update":           common.BatchUpdateEnabled,
+		"enable_drawing":                common.DrawingEnabled,
+		"enable_task":                   common.TaskEnabled,
+		"enable_data_export":            common.DataExportEnabled,
+		"data_export_default_time":      common.DataExportDefaultTime,
+		"default_collapse_sidebar":      common.DefaultCollapseSidebar,
+		"mj_notify_enabled":             setting.MjNotifyEnabled,
+		"chats":                         setting.Chats,
+		"demo_site_enabled":             operation_setting.DemoSiteEnabled,
+		"self_use_mode_enabled":         operation_setting.SelfUseModeEnabled,
+		"default_use_auto_group":        setting.DefaultUseAutoGroup,
 
 		"usd_exchange_rate": operation_setting.USDExchangeRate,
 		"price":             operation_setting.Price,
@@ -94,7 +102,16 @@ func GetStatus(c *gin.Context) {
 		"oidc_enabled":                system_setting.GetOIDCSettings().Enabled,
 		"oidc_client_id":              system_setting.GetOIDCSettings().ClientId,
 		"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
+		"passkey_login":               passkeySetting.Enabled,
+		"passkey_display_name":        passkeySetting.RPDisplayName,
+		"passkey_rp_id":               passkeySetting.RPID,
+		"passkey_origins":             passkeySetting.Origins,
+		"passkey_allow_insecure":      passkeySetting.AllowInsecureOrigin,
+		"passkey_user_verification":   passkeySetting.UserVerification,
+		"passkey_attachment":          passkeySetting.AttachmentPreference,
 		"setup":                       constant.Setup,
+		"user_agreement_enabled":      legalSetting.UserAgreement != "",
+		"privacy_policy_enabled":      legalSetting.PrivacyPolicy != "",
 	}
 
 	// 根据启用状态注入可选内容
@@ -138,6 +155,24 @@ func GetAbout(c *gin.Context) {
 	return
 }
 
+func GetUserAgreement(c *gin.Context) {
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data":    system_setting.GetLegalSettings().UserAgreement,
+	})
+	return
+}
+
+func GetPrivacyPolicy(c *gin.Context) {
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data":    system_setting.GetLegalSettings().PrivacyPolicy,
+	})
+	return
+}
+
 func GetMidjourney(c *gin.Context) {
 	common.OptionMapRWMutex.RLock()
 	defer common.OptionMapRWMutex.RUnlock()

+ 2 - 1
controller/missing_models.go

@@ -2,7 +2,8 @@ package controller
 
 import (
 	"net/http"
-	"one-api/model"
+
+	"github.com/QuantumNous/new-api/model"
 
 	"github.com/gin-gonic/gin"
 )

+ 14 - 13
controller/model.go

@@ -2,21 +2,22 @@ package controller
 
 import (
 	"fmt"
-	"github.com/gin-gonic/gin"
-	"github.com/samber/lo"
 	"net/http"
-	"one-api/common"
-	"one-api/constant"
-	"one-api/dto"
-	"one-api/model"
-	"one-api/relay"
-	"one-api/relay/channel/ai360"
-	"one-api/relay/channel/lingyiwanwu"
-	"one-api/relay/channel/minimax"
-	"one-api/relay/channel/moonshot"
-	relaycommon "one-api/relay/common"
-	"one-api/setting"
 	"time"
+
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/constant"
+	"github.com/QuantumNous/new-api/dto"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/relay"
+	"github.com/QuantumNous/new-api/relay/channel/ai360"
+	"github.com/QuantumNous/new-api/relay/channel/lingyiwanwu"
+	"github.com/QuantumNous/new-api/relay/channel/minimax"
+	"github.com/QuantumNous/new-api/relay/channel/moonshot"
+	relaycommon "github.com/QuantumNous/new-api/relay/common"
+	"github.com/QuantumNous/new-api/setting"
+	"github.com/gin-gonic/gin"
+	"github.com/samber/lo"
 )
 
 // https://platform.openai.com/docs/api-reference/models/list

+ 3 - 3
controller/model_meta.go

@@ -6,9 +6,9 @@ import (
 	"strconv"
 	"strings"
 
-	"one-api/common"
-	"one-api/constant"
-	"one-api/model"
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/constant"
+	"github.com/QuantumNous/new-api/model"
 
 	"github.com/gin-gonic/gin"
 )

+ 2 - 2
controller/model_sync.go

@@ -13,8 +13,8 @@ import (
 	"sync"
 	"time"
 
-	"one-api/common"
-	"one-api/model"
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
 
 	"github.com/gin-gonic/gin"
 	"gorm.io/gorm"

+ 4 - 3
controller/oidc.go

@@ -6,13 +6,14 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
-	"one-api/common"
-	"one-api/model"
-	"one-api/setting/system_setting"
 	"strconv"
 	"strings"
 	"time"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/setting/system_setting"
+
 	"github.com/gin-contrib/sessions"
 	"github.com/gin-gonic/gin"
 )

+ 7 - 6
controller/option.go

@@ -4,14 +4,15 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
-	"one-api/common"
-	"one-api/model"
-	"one-api/setting"
-	"one-api/setting/console_setting"
-	"one-api/setting/ratio_setting"
-	"one-api/setting/system_setting"
 	"strings"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/setting"
+	"github.com/QuantumNous/new-api/setting/console_setting"
+	"github.com/QuantumNous/new-api/setting/ratio_setting"
+	"github.com/QuantumNous/new-api/setting/system_setting"
+
 	"github.com/gin-gonic/gin"
 )
 

+ 497 - 0
controller/passkey.go

@@ -0,0 +1,497 @@
+package controller
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"strconv"
+	"time"
+
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
+	passkeysvc "github.com/QuantumNous/new-api/service/passkey"
+	"github.com/QuantumNous/new-api/setting/system_setting"
+
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+	"github.com/go-webauthn/webauthn/protocol"
+	webauthnlib "github.com/go-webauthn/webauthn/webauthn"
+)
+
+func PasskeyRegisterBegin(c *gin.Context) {
+	if !system_setting.GetPasskeySettings().Enabled {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": "管理员未启用 Passkey 登录",
+		})
+		return
+	}
+
+	user, err := getSessionUser(c)
+	if err != nil {
+		c.JSON(http.StatusUnauthorized, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+
+	credential, err := model.GetPasskeyByUserID(user.Id)
+	if err != nil && !errors.Is(err, model.ErrPasskeyNotFound) {
+		common.ApiError(c, err)
+		return
+	}
+	if errors.Is(err, model.ErrPasskeyNotFound) {
+		credential = nil
+	}
+
+	wa, err := passkeysvc.BuildWebAuthn(c.Request)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	waUser := passkeysvc.NewWebAuthnUser(user, credential)
+	var options []webauthnlib.RegistrationOption
+	if credential != nil {
+		descriptor := credential.ToWebAuthnCredential().Descriptor()
+		options = append(options, webauthnlib.WithExclusions([]protocol.CredentialDescriptor{descriptor}))
+	}
+
+	creation, sessionData, err := wa.BeginRegistration(waUser, options...)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	if err := passkeysvc.SaveSessionData(c, passkeysvc.RegistrationSessionKey, sessionData); err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data": gin.H{
+			"options": creation,
+		},
+	})
+}
+
+func PasskeyRegisterFinish(c *gin.Context) {
+	if !system_setting.GetPasskeySettings().Enabled {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": "管理员未启用 Passkey 登录",
+		})
+		return
+	}
+
+	user, err := getSessionUser(c)
+	if err != nil {
+		c.JSON(http.StatusUnauthorized, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+
+	wa, err := passkeysvc.BuildWebAuthn(c.Request)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	credentialRecord, err := model.GetPasskeyByUserID(user.Id)
+	if err != nil && !errors.Is(err, model.ErrPasskeyNotFound) {
+		common.ApiError(c, err)
+		return
+	}
+	if errors.Is(err, model.ErrPasskeyNotFound) {
+		credentialRecord = nil
+	}
+
+	sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.RegistrationSessionKey)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	waUser := passkeysvc.NewWebAuthnUser(user, credentialRecord)
+	credential, err := wa.FinishRegistration(waUser, *sessionData, c.Request)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	passkeyCredential := model.NewPasskeyCredentialFromWebAuthn(user.Id, credential)
+	if passkeyCredential == nil {
+		common.ApiErrorMsg(c, "无法创建 Passkey 凭证")
+		return
+	}
+
+	if err := model.UpsertPasskeyCredential(passkeyCredential); err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "Passkey 注册成功",
+	})
+}
+
+func PasskeyDelete(c *gin.Context) {
+	user, err := getSessionUser(c)
+	if err != nil {
+		c.JSON(http.StatusUnauthorized, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+
+	if err := model.DeletePasskeyByUserID(user.Id); err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "Passkey 已解绑",
+	})
+}
+
+func PasskeyStatus(c *gin.Context) {
+	user, err := getSessionUser(c)
+	if err != nil {
+		c.JSON(http.StatusUnauthorized, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+
+	credential, err := model.GetPasskeyByUserID(user.Id)
+	if errors.Is(err, model.ErrPasskeyNotFound) {
+		c.JSON(http.StatusOK, gin.H{
+			"success": true,
+			"message": "",
+			"data": gin.H{
+				"enabled": false,
+			},
+		})
+		return
+	}
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	data := gin.H{
+		"enabled":      true,
+		"last_used_at": credential.LastUsedAt,
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data":    data,
+	})
+}
+
+func PasskeyLoginBegin(c *gin.Context) {
+	if !system_setting.GetPasskeySettings().Enabled {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": "管理员未启用 Passkey 登录",
+		})
+		return
+	}
+
+	wa, err := passkeysvc.BuildWebAuthn(c.Request)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	assertion, sessionData, err := wa.BeginDiscoverableLogin()
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	if err := passkeysvc.SaveSessionData(c, passkeysvc.LoginSessionKey, sessionData); err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data": gin.H{
+			"options": assertion,
+		},
+	})
+}
+
+func PasskeyLoginFinish(c *gin.Context) {
+	if !system_setting.GetPasskeySettings().Enabled {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": "管理员未启用 Passkey 登录",
+		})
+		return
+	}
+
+	wa, err := passkeysvc.BuildWebAuthn(c.Request)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.LoginSessionKey)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	handler := func(rawID, userHandle []byte) (webauthnlib.User, error) {
+		// 首先通过凭证ID查找用户
+		credential, err := model.GetPasskeyByCredentialID(rawID)
+		if err != nil {
+			return nil, fmt.Errorf("未找到 Passkey 凭证: %w", err)
+		}
+
+		// 通过凭证获取用户
+		user := &model.User{Id: credential.UserID}
+		if err := user.FillUserById(); err != nil {
+			return nil, fmt.Errorf("用户信息获取失败: %w", err)
+		}
+
+		if user.Status != common.UserStatusEnabled {
+			return nil, errors.New("该用户已被禁用")
+		}
+
+		if len(userHandle) > 0 {
+			userID, parseErr := strconv.Atoi(string(userHandle))
+			if parseErr != nil {
+				// 记录异常但继续验证,因为某些客户端可能使用非数字格式
+				common.SysLog(fmt.Sprintf("PasskeyLogin: userHandle parse error for credential, length: %d", len(userHandle)))
+			} else if userID != user.Id {
+				return nil, errors.New("用户句柄与凭证不匹配")
+			}
+		}
+
+		return passkeysvc.NewWebAuthnUser(user, credential), nil
+	}
+
+	waUser, credential, err := wa.FinishPasskeyLogin(handler, *sessionData, c.Request)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	userWrapper, ok := waUser.(*passkeysvc.WebAuthnUser)
+	if !ok {
+		common.ApiErrorMsg(c, "Passkey 登录状态异常")
+		return
+	}
+
+	modelUser := userWrapper.ModelUser()
+	if modelUser == nil {
+		common.ApiErrorMsg(c, "Passkey 登录状态异常")
+		return
+	}
+
+	if modelUser.Status != common.UserStatusEnabled {
+		common.ApiErrorMsg(c, "该用户已被禁用")
+		return
+	}
+
+	// 更新凭证信息
+	updatedCredential := model.NewPasskeyCredentialFromWebAuthn(modelUser.Id, credential)
+	if updatedCredential == nil {
+		common.ApiErrorMsg(c, "Passkey 凭证更新失败")
+		return
+	}
+	now := time.Now()
+	updatedCredential.LastUsedAt = &now
+	if err := model.UpsertPasskeyCredential(updatedCredential); err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	setupLogin(modelUser, c)
+	return
+}
+
+func AdminResetPasskey(c *gin.Context) {
+	id, err := strconv.Atoi(c.Param("id"))
+	if err != nil {
+		common.ApiErrorMsg(c, "无效的用户 ID")
+		return
+	}
+
+	user := &model.User{Id: id}
+	if err := user.FillUserById(); err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	if _, err := model.GetPasskeyByUserID(user.Id); err != nil {
+		if errors.Is(err, model.ErrPasskeyNotFound) {
+			c.JSON(http.StatusOK, gin.H{
+				"success": false,
+				"message": "该用户尚未绑定 Passkey",
+			})
+			return
+		}
+		common.ApiError(c, err)
+		return
+	}
+
+	if err := model.DeletePasskeyByUserID(user.Id); err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "Passkey 已重置",
+	})
+}
+
+func PasskeyVerifyBegin(c *gin.Context) {
+	if !system_setting.GetPasskeySettings().Enabled {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": "管理员未启用 Passkey 登录",
+		})
+		return
+	}
+
+	user, err := getSessionUser(c)
+	if err != nil {
+		c.JSON(http.StatusUnauthorized, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+
+	credential, err := model.GetPasskeyByUserID(user.Id)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": "该用户尚未绑定 Passkey",
+		})
+		return
+	}
+
+	wa, err := passkeysvc.BuildWebAuthn(c.Request)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	waUser := passkeysvc.NewWebAuthnUser(user, credential)
+	assertion, sessionData, err := wa.BeginLogin(waUser)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	if err := passkeysvc.SaveSessionData(c, passkeysvc.VerifySessionKey, sessionData); err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data": gin.H{
+			"options": assertion,
+		},
+	})
+}
+
+func PasskeyVerifyFinish(c *gin.Context) {
+	if !system_setting.GetPasskeySettings().Enabled {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": "管理员未启用 Passkey 登录",
+		})
+		return
+	}
+
+	user, err := getSessionUser(c)
+	if err != nil {
+		c.JSON(http.StatusUnauthorized, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+
+	wa, err := passkeysvc.BuildWebAuthn(c.Request)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	credential, err := model.GetPasskeyByUserID(user.Id)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": "该用户尚未绑定 Passkey",
+		})
+		return
+	}
+
+	sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.VerifySessionKey)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	waUser := passkeysvc.NewWebAuthnUser(user, credential)
+	_, err = wa.FinishLogin(waUser, *sessionData, c.Request)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	// 更新凭证的最后使用时间
+	now := time.Now()
+	credential.LastUsedAt = &now
+	if err := model.UpsertPasskeyCredential(credential); err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "Passkey 验证成功",
+	})
+}
+
+func getSessionUser(c *gin.Context) (*model.User, error) {
+	session := sessions.Default(c)
+	idRaw := session.Get("id")
+	if idRaw == nil {
+		return nil, errors.New("未登录")
+	}
+	id, ok := idRaw.(int)
+	if !ok {
+		return nil, errors.New("无效的会话信息")
+	}
+	user := &model.User{Id: id}
+	if err := user.FillUserById(); err != nil {
+		return nil, err
+	}
+	if user.Status != common.UserStatusEnabled {
+		return nil, errors.New("该用户已被禁用")
+	}
+	return user, nil
+}

+ 6 - 5
controller/playground.go

@@ -3,13 +3,14 @@ package controller
 import (
 	"errors"
 	"fmt"
-	"one-api/common"
-	"one-api/constant"
-	"one-api/middleware"
-	"one-api/model"
-	"one-api/types"
 	"time"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/constant"
+	"github.com/QuantumNous/new-api/middleware"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/types"
+
 	"github.com/gin-gonic/gin"
 )
 

+ 2 - 2
controller/prefill_group.go

@@ -3,8 +3,8 @@ package controller
 import (
 	"strconv"
 
-	"one-api/common"
-	"one-api/model"
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
 
 	"github.com/gin-gonic/gin"
 )

+ 3 - 3
controller/pricing.go

@@ -1,9 +1,9 @@
 package controller
 
 import (
-	"one-api/model"
-	"one-api/setting"
-	"one-api/setting/ratio_setting"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/setting"
+	"github.com/QuantumNous/new-api/setting/ratio_setting"
 
 	"github.com/gin-gonic/gin"
 )

+ 2 - 1
controller/ratio_config.go

@@ -2,7 +2,8 @@ package controller
 
 import (
 	"net/http"
-	"one-api/setting/ratio_setting"
+
+	"github.com/QuantumNous/new-api/setting/ratio_setting"
 
 	"github.com/gin-gonic/gin"
 )

+ 5 - 4
controller/ratio_sync.go

@@ -7,14 +7,15 @@ import (
 	"io"
 	"net"
 	"net/http"
-	"one-api/logger"
 	"strings"
 	"sync"
 	"time"
 
-	"one-api/dto"
-	"one-api/model"
-	"one-api/setting/ratio_setting"
+	"github.com/QuantumNous/new-api/logger"
+
+	"github.com/QuantumNous/new-api/dto"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/setting/ratio_setting"
 
 	"github.com/gin-gonic/gin"
 )

+ 3 - 2
controller/redemption.go

@@ -3,11 +3,12 @@ package controller
 import (
 	"errors"
 	"net/http"
-	"one-api/common"
-	"one-api/model"
 	"strconv"
 	"unicode/utf8"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
+
 	"github.com/gin-gonic/gin"
 )
 

+ 25 - 17
controller/relay.go

@@ -6,21 +6,22 @@ import (
 	"io"
 	"log"
 	"net/http"
-	"one-api/common"
-	"one-api/constant"
-	"one-api/dto"
-	"one-api/logger"
-	"one-api/middleware"
-	"one-api/model"
-	"one-api/relay"
-	relaycommon "one-api/relay/common"
-	relayconstant "one-api/relay/constant"
-	"one-api/relay/helper"
-	"one-api/service"
-	"one-api/setting"
-	"one-api/types"
 	"strings"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/constant"
+	"github.com/QuantumNous/new-api/dto"
+	"github.com/QuantumNous/new-api/logger"
+	"github.com/QuantumNous/new-api/middleware"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/relay"
+	relaycommon "github.com/QuantumNous/new-api/relay/common"
+	relayconstant "github.com/QuantumNous/new-api/relay/constant"
+	"github.com/QuantumNous/new-api/relay/helper"
+	"github.com/QuantumNous/new-api/service"
+	"github.com/QuantumNous/new-api/setting"
+	"github.com/QuantumNous/new-api/types"
+
 	"github.com/bytedance/gopkg/util/gopool"
 
 	"github.com/gin-gonic/gin"
@@ -139,9 +140,13 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
 
 	// common.SetContextKey(c, constant.ContextKeyTokenCountMeta, meta)
 
-	newAPIError = service.PreConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
-	if newAPIError != nil {
-		return
+	if priceData.FreeModel {
+		logger.LogInfo(c, fmt.Sprintf("模型 %s 免费,跳过预扣费", relayInfo.OriginModelName))
+	} else {
+		newAPIError = service.PreConsumeQuota(c, priceData.QuotaToPreConsume, relayInfo)
+		if newAPIError != nil {
+			return
+		}
 	}
 
 	defer func() {
@@ -224,7 +229,7 @@ func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*m
 		return nil, types.NewError(fmt.Errorf("获取分组 %s 下模型 %s 的可用渠道失败(retry): %s", selectGroup, originalModel, err.Error()), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry())
 	}
 	if channel == nil {
-		return nil, types.NewError(fmt.Errorf("分组 %s 下模型 %s 的可用渠道不存在(数据库一致性已被破坏,retry)", selectGroup, originalModel), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry())
+		return nil, types.NewError(fmt.Errorf("分组 %s 下模型 %s 的可用渠道不存在(retry)", selectGroup, originalModel), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry())
 	}
 	newAPIError := middleware.SetupContextForSelectedChannel(c, channel, originalModel)
 	if newAPIError != nil {
@@ -294,6 +299,9 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t
 		userGroup := c.GetString("group")
 		channelId := c.GetInt("channel_id")
 		other := make(map[string]interface{})
+		if c.Request != nil && c.Request.URL != nil {
+			other["request_path"] = c.Request.URL.Path
+		}
 		other["error_type"] = err.GetErrorType()
 		other["error_code"] = err.GetErrorCode()
 		other["status_code"] = err.StatusCode

+ 314 - 0
controller/secure_verification.go

@@ -0,0 +1,314 @@
+package controller
+
+import (
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
+	passkeysvc "github.com/QuantumNous/new-api/service/passkey"
+	"github.com/QuantumNous/new-api/setting/system_setting"
+
+	"github.com/gin-contrib/sessions"
+	"github.com/gin-gonic/gin"
+)
+
+const (
+	// SecureVerificationSessionKey 安全验证的 session key
+	SecureVerificationSessionKey = "secure_verified_at"
+	// SecureVerificationTimeout 验证有效期(秒)
+	SecureVerificationTimeout = 300 // 5分钟
+)
+
+type UniversalVerifyRequest struct {
+	Method string `json:"method"` // "2fa" 或 "passkey"
+	Code   string `json:"code,omitempty"`
+}
+
+type VerificationStatusResponse struct {
+	Verified  bool  `json:"verified"`
+	ExpiresAt int64 `json:"expires_at,omitempty"`
+}
+
+// UniversalVerify 通用验证接口
+// 支持 2FA 和 Passkey 验证,验证成功后在 session 中记录时间戳
+func UniversalVerify(c *gin.Context) {
+	userId := c.GetInt("id")
+	if userId == 0 {
+		c.JSON(http.StatusUnauthorized, gin.H{
+			"success": false,
+			"message": "未登录",
+		})
+		return
+	}
+
+	var req UniversalVerifyRequest
+	if err := c.ShouldBindJSON(&req); err != nil {
+		common.ApiError(c, fmt.Errorf("参数错误: %v", err))
+		return
+	}
+
+	// 获取用户信息
+	user := &model.User{Id: userId}
+	if err := user.FillUserById(); err != nil {
+		common.ApiError(c, fmt.Errorf("获取用户信息失败: %v", err))
+		return
+	}
+
+	if user.Status != common.UserStatusEnabled {
+		common.ApiError(c, fmt.Errorf("该用户已被禁用"))
+		return
+	}
+
+	// 检查用户的验证方式
+	twoFA, _ := model.GetTwoFAByUserId(userId)
+	has2FA := twoFA != nil && twoFA.IsEnabled
+
+	passkey, passkeyErr := model.GetPasskeyByUserID(userId)
+	hasPasskey := passkeyErr == nil && passkey != nil
+
+	if !has2FA && !hasPasskey {
+		common.ApiError(c, fmt.Errorf("用户未启用2FA或Passkey"))
+		return
+	}
+
+	// 根据验证方式进行验证
+	var verified bool
+	var verifyMethod string
+
+	switch req.Method {
+	case "2fa":
+		if !has2FA {
+			common.ApiError(c, fmt.Errorf("用户未启用2FA"))
+			return
+		}
+		if req.Code == "" {
+			common.ApiError(c, fmt.Errorf("验证码不能为空"))
+			return
+		}
+		verified = validateTwoFactorAuth(twoFA, req.Code)
+		verifyMethod = "2FA"
+
+	case "passkey":
+		if !hasPasskey {
+			common.ApiError(c, fmt.Errorf("用户未启用Passkey"))
+			return
+		}
+		// Passkey 验证需要先调用 PasskeyVerifyBegin 和 PasskeyVerifyFinish
+		// 这里只是验证 Passkey 验证流程是否已经完成
+		// 实际上,前端应该先调用这两个接口,然后再调用本接口
+		verified = true // Passkey 验证逻辑已在 PasskeyVerifyFinish 中完成
+		verifyMethod = "Passkey"
+
+	default:
+		common.ApiError(c, fmt.Errorf("不支持的验证方式: %s", req.Method))
+		return
+	}
+
+	if !verified {
+		common.ApiError(c, fmt.Errorf("验证失败,请检查验证码"))
+		return
+	}
+
+	// 验证成功,在 session 中记录时间戳
+	session := sessions.Default(c)
+	now := time.Now().Unix()
+	session.Set(SecureVerificationSessionKey, now)
+	if err := session.Save(); err != nil {
+		common.ApiError(c, fmt.Errorf("保存验证状态失败: %v", err))
+		return
+	}
+
+	// 记录日志
+	model.RecordLog(userId, model.LogTypeSystem, fmt.Sprintf("通用安全验证成功 (验证方式: %s)", verifyMethod))
+
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "验证成功",
+		"data": gin.H{
+			"verified":   true,
+			"expires_at": now + SecureVerificationTimeout,
+		},
+	})
+}
+
+// GetVerificationStatus 获取验证状态
+func GetVerificationStatus(c *gin.Context) {
+	userId := c.GetInt("id")
+	if userId == 0 {
+		c.JSON(http.StatusUnauthorized, gin.H{
+			"success": false,
+			"message": "未登录",
+		})
+		return
+	}
+
+	session := sessions.Default(c)
+	verifiedAtRaw := session.Get(SecureVerificationSessionKey)
+
+	if verifiedAtRaw == nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": true,
+			"message": "",
+			"data": VerificationStatusResponse{
+				Verified: false,
+			},
+		})
+		return
+	}
+
+	verifiedAt, ok := verifiedAtRaw.(int64)
+	if !ok {
+		c.JSON(http.StatusOK, gin.H{
+			"success": true,
+			"message": "",
+			"data": VerificationStatusResponse{
+				Verified: false,
+			},
+		})
+		return
+	}
+
+	elapsed := time.Now().Unix() - verifiedAt
+	if elapsed >= SecureVerificationTimeout {
+		// 验证已过期
+		session.Delete(SecureVerificationSessionKey)
+		_ = session.Save()
+		c.JSON(http.StatusOK, gin.H{
+			"success": true,
+			"message": "",
+			"data": VerificationStatusResponse{
+				Verified: false,
+			},
+		})
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data": VerificationStatusResponse{
+			Verified:  true,
+			ExpiresAt: verifiedAt + SecureVerificationTimeout,
+		},
+	})
+}
+
+// CheckSecureVerification 检查是否已通过安全验证
+// 返回 true 表示验证有效,false 表示需要重新验证
+func CheckSecureVerification(c *gin.Context) bool {
+	session := sessions.Default(c)
+	verifiedAtRaw := session.Get(SecureVerificationSessionKey)
+
+	if verifiedAtRaw == nil {
+		return false
+	}
+
+	verifiedAt, ok := verifiedAtRaw.(int64)
+	if !ok {
+		return false
+	}
+
+	elapsed := time.Now().Unix() - verifiedAt
+	if elapsed >= SecureVerificationTimeout {
+		// 验证已过期,清除 session
+		session.Delete(SecureVerificationSessionKey)
+		_ = session.Save()
+		return false
+	}
+
+	return true
+}
+
+// PasskeyVerifyAndSetSession Passkey 验证完成后设置 session
+// 这是一个辅助函数,供 PasskeyVerifyFinish 调用
+func PasskeyVerifyAndSetSession(c *gin.Context) {
+	session := sessions.Default(c)
+	now := time.Now().Unix()
+	session.Set(SecureVerificationSessionKey, now)
+	_ = session.Save()
+}
+
+// PasskeyVerifyForSecure 用于安全验证的 Passkey 验证流程
+// 整合了 begin 和 finish 流程
+func PasskeyVerifyForSecure(c *gin.Context) {
+	if !system_setting.GetPasskeySettings().Enabled {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": "管理员未启用 Passkey 登录",
+		})
+		return
+	}
+
+	userId := c.GetInt("id")
+	if userId == 0 {
+		c.JSON(http.StatusUnauthorized, gin.H{
+			"success": false,
+			"message": "未登录",
+		})
+		return
+	}
+
+	user := &model.User{Id: userId}
+	if err := user.FillUserById(); err != nil {
+		common.ApiError(c, fmt.Errorf("获取用户信息失败: %v", err))
+		return
+	}
+
+	if user.Status != common.UserStatusEnabled {
+		common.ApiError(c, fmt.Errorf("该用户已被禁用"))
+		return
+	}
+
+	credential, err := model.GetPasskeyByUserID(userId)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": "该用户尚未绑定 Passkey",
+		})
+		return
+	}
+
+	wa, err := passkeysvc.BuildWebAuthn(c.Request)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	waUser := passkeysvc.NewWebAuthnUser(user, credential)
+	sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.VerifySessionKey)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	_, err = wa.FinishLogin(waUser, *sessionData, c.Request)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	// 更新凭证的最后使用时间
+	now := time.Now()
+	credential.LastUsedAt = &now
+	if err := model.UpsertPasskeyCredential(credential); err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	// 验证成功,设置 session
+	PasskeyVerifyAndSetSession(c)
+
+	// 记录日志
+	model.RecordLog(userId, model.LogTypeSystem, "Passkey 安全验证成功")
+
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "Passkey 验证成功",
+		"data": gin.H{
+			"verified":   true,
+			"expires_at": time.Now().Unix() + SecureVerificationTimeout,
+		},
+	})
+}

+ 7 - 6
controller/setup.go

@@ -1,12 +1,13 @@
 package controller
 
 import (
-	"github.com/gin-gonic/gin"
-	"one-api/common"
-	"one-api/constant"
-	"one-api/model"
-	"one-api/setting/operation_setting"
 	"time"
+
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/constant"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/setting/operation_setting"
+	"github.com/gin-gonic/gin"
 )
 
 type Setup struct {
@@ -178,4 +179,4 @@ func boolToString(b bool) string {
 		return "true"
 	}
 	return "false"
-}
+}

+ 7 - 6
controller/task.go

@@ -7,16 +7,17 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"one-api/common"
-	"one-api/constant"
-	"one-api/dto"
-	"one-api/logger"
-	"one-api/model"
-	"one-api/relay"
 	"sort"
 	"strconv"
 	"time"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/constant"
+	"github.com/QuantumNous/new-api/dto"
+	"github.com/QuantumNous/new-api/logger"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/relay"
+
 	"github.com/gin-gonic/gin"
 	"github.com/samber/lo"
 )

+ 136 - 15
controller/task_video.go

@@ -5,15 +5,17 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"one-api/common"
-	"one-api/constant"
-	"one-api/dto"
-	"one-api/logger"
-	"one-api/model"
-	"one-api/relay"
-	"one-api/relay/channel"
-	relaycommon "one-api/relay/common"
 	"time"
+
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/constant"
+	"github.com/QuantumNous/new-api/dto"
+	"github.com/QuantumNous/new-api/logger"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/relay"
+	"github.com/QuantumNous/new-api/relay/channel"
+	relaycommon "github.com/QuantumNous/new-api/relay/common"
+	"github.com/QuantumNous/new-api/setting/ratio_setting"
 )
 
 func UpdateVideoTaskAll(ctx context.Context, platform constant.TaskPlatform, taskChannelM map[int][]string, taskM map[string]*model.Task) error {
@@ -46,6 +48,11 @@ func updateVideoTaskAll(ctx context.Context, platform constant.TaskPlatform, cha
 	if adaptor == nil {
 		return fmt.Errorf("video adaptor not found")
 	}
+	info := &relaycommon.RelayInfo{}
+	info.ChannelMeta = &relaycommon.ChannelMeta{
+		ChannelBaseUrl: cacheGetChannel.GetBaseURL(),
+	}
+	adaptor.Init(info)
 	for _, taskId := range taskIds {
 		if err := updateVideoSingleTask(ctx, adaptor, cacheGetChannel, taskId, taskM); err != nil {
 			logger.LogError(ctx, fmt.Sprintf("Failed to update video task %s: %s", taskId, err.Error()))
@@ -81,26 +88,39 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
 		return fmt.Errorf("readAll failed for task %s: %w", taskId, err)
 	}
 
+	logger.LogDebug(ctx, fmt.Sprintf("UpdateVideoSingleTask response: %s", string(responseBody)))
+
 	taskResult := &relaycommon.TaskInfo{}
 	// try parse as New API response format
 	var responseItems dto.TaskResponse[model.Task]
-	if err = json.Unmarshal(responseBody, &responseItems); err == nil && responseItems.IsSuccess() {
+	if err = common.Unmarshal(responseBody, &responseItems); err == nil && responseItems.IsSuccess() {
+		logger.LogDebug(ctx, fmt.Sprintf("UpdateVideoSingleTask parsed as new api response format: %+v", responseItems))
 		t := responseItems.Data
 		taskResult.TaskID = t.TaskID
 		taskResult.Status = string(t.Status)
 		taskResult.Url = t.FailReason
 		taskResult.Progress = t.Progress
 		taskResult.Reason = t.FailReason
+		task.Data = t.Data
 	} else if taskResult, err = adaptor.ParseTaskResult(responseBody); err != nil {
 		return fmt.Errorf("parseTaskResult failed for task %s: %w", taskId, err)
 	} else {
 		task.Data = redactVideoResponseBody(responseBody)
 	}
 
+	logger.LogDebug(ctx, fmt.Sprintf("UpdateVideoSingleTask taskResult: %+v", taskResult))
+
 	now := time.Now().Unix()
 	if taskResult.Status == "" {
-		return fmt.Errorf("task %s status is empty", taskId)
+		//return fmt.Errorf("task %s status is empty", taskId)
+		taskResult = relaycommon.FailTaskInfo("upstream returned empty status")
 	}
+
+	// 记录原本的状态,防止重复退款
+	shouldRefund := false
+	quota := task.Quota
+	preStatus := task.Status
+
 	task.Status = model.TaskStatus(taskResult.Status)
 	switch taskResult.Status {
 	case model.TaskStatusSubmitted:
@@ -120,7 +140,98 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
 		if !(len(taskResult.Url) > 5 && taskResult.Url[:5] == "data:") {
 			task.FailReason = taskResult.Url
 		}
+
+		// 如果返回了 total_tokens 并且配置了模型倍率(非固定价格),则重新计费
+		if taskResult.TotalTokens > 0 {
+			// 获取模型名称
+			var taskData map[string]interface{}
+			if err := json.Unmarshal(task.Data, &taskData); err == nil {
+				if modelName, ok := taskData["model"].(string); ok && modelName != "" {
+					// 获取模型价格和倍率
+					modelRatio, hasRatioSetting, _ := ratio_setting.GetModelRatio(modelName)
+					// 只有配置了倍率(非固定价格)时才按 token 重新计费
+					if hasRatioSetting && modelRatio > 0 {
+						// 获取用户和组的倍率信息
+						group := task.Group
+						if group == "" {
+							user, err := model.GetUserById(task.UserId, false)
+							if err == nil {
+								group = user.Group
+							}
+						}
+						if group != "" {
+							groupRatio := ratio_setting.GetGroupRatio(group)
+							userGroupRatio, hasUserGroupRatio := ratio_setting.GetGroupGroupRatio(group, group)
+
+							var finalGroupRatio float64
+							if hasUserGroupRatio {
+								finalGroupRatio = userGroupRatio
+							} else {
+								finalGroupRatio = groupRatio
+							}
+
+							// 计算实际应扣费额度: totalTokens * modelRatio * groupRatio
+							actualQuota := int(float64(taskResult.TotalTokens) * modelRatio * finalGroupRatio)
+
+							// 计算差额
+							preConsumedQuota := task.Quota
+							quotaDelta := actualQuota - preConsumedQuota
+
+							if quotaDelta > 0 {
+								// 需要补扣费
+								logger.LogInfo(ctx, fmt.Sprintf("视频任务 %s 预扣费后补扣费:%s(实际消耗:%s,预扣费:%s,tokens:%d)",
+									task.TaskID,
+									logger.LogQuota(quotaDelta),
+									logger.LogQuota(actualQuota),
+									logger.LogQuota(preConsumedQuota),
+									taskResult.TotalTokens,
+								))
+								if err := model.DecreaseUserQuota(task.UserId, quotaDelta); err != nil {
+									logger.LogError(ctx, fmt.Sprintf("补扣费失败: %s", err.Error()))
+								} else {
+									model.UpdateUserUsedQuotaAndRequestCount(task.UserId, quotaDelta)
+									model.UpdateChannelUsedQuota(task.ChannelId, quotaDelta)
+									task.Quota = actualQuota // 更新任务记录的实际扣费额度
+
+									// 记录消费日志
+									logContent := fmt.Sprintf("视频任务成功补扣费,模型倍率 %.2f,分组倍率 %.2f,tokens %d,预扣费 %s,实际扣费 %s,补扣费 %s",
+										modelRatio, finalGroupRatio, taskResult.TotalTokens,
+										logger.LogQuota(preConsumedQuota), logger.LogQuota(actualQuota), logger.LogQuota(quotaDelta))
+									model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
+								}
+							} else if quotaDelta < 0 {
+								// 需要退还多扣的费用
+								refundQuota := -quotaDelta
+								logger.LogInfo(ctx, fmt.Sprintf("视频任务 %s 预扣费后返还:%s(实际消耗:%s,预扣费:%s,tokens:%d)",
+									task.TaskID,
+									logger.LogQuota(refundQuota),
+									logger.LogQuota(actualQuota),
+									logger.LogQuota(preConsumedQuota),
+									taskResult.TotalTokens,
+								))
+								if err := model.IncreaseUserQuota(task.UserId, refundQuota, false); err != nil {
+									logger.LogError(ctx, fmt.Sprintf("退还预扣费失败: %s", err.Error()))
+								} else {
+									task.Quota = actualQuota // 更新任务记录的实际扣费额度
+
+									// 记录退款日志
+									logContent := fmt.Sprintf("视频任务成功退还多扣费用,模型倍率 %.2f,分组倍率 %.2f,tokens %d,预扣费 %s,实际扣费 %s,退还 %s",
+										modelRatio, finalGroupRatio, taskResult.TotalTokens,
+										logger.LogQuota(preConsumedQuota), logger.LogQuota(actualQuota), logger.LogQuota(refundQuota))
+									model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
+								}
+							} else {
+								// quotaDelta == 0, 预扣费刚好准确
+								logger.LogInfo(ctx, fmt.Sprintf("视频任务 %s 预扣费准确(%s,tokens:%d)",
+									task.TaskID, logger.LogQuota(actualQuota), taskResult.TotalTokens))
+							}
+						}
+					}
+				}
+			}
+		}
 	case model.TaskStatusFailure:
+		logger.LogJson(ctx, fmt.Sprintf("Task %s failed", taskId), task)
 		task.Status = model.TaskStatusFailure
 		task.Progress = "100%"
 		if task.FinishTime == 0 {
@@ -128,13 +239,13 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
 		}
 		task.FailReason = taskResult.Reason
 		logger.LogInfo(ctx, fmt.Sprintf("Task %s failed: %s", task.TaskID, task.FailReason))
-		quota := task.Quota
+		taskResult.Progress = "100%"
 		if quota != 0 {
-			if err := model.IncreaseUserQuota(task.UserId, quota, false); err != nil {
-				logger.LogError(ctx, "Failed to increase user quota: "+err.Error())
+			if preStatus != model.TaskStatusFailure {
+				shouldRefund = true
+			} else {
+				logger.LogWarn(ctx, fmt.Sprintf("Task %s already in failure status, skip refund", task.TaskID))
 			}
-			logContent := fmt.Sprintf("Video async task failed %s, refund %s", task.TaskID, logger.LogQuota(quota))
-			model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
 		}
 	default:
 		return fmt.Errorf("unknown task status %s for task %s", taskResult.Status, taskId)
@@ -144,6 +255,16 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha
 	}
 	if err := task.Update(); err != nil {
 		common.SysLog("UpdateVideoTask task error: " + err.Error())
+		shouldRefund = false
+	}
+
+	if shouldRefund {
+		// 任务失败且之前状态不是失败才退还额度,防止重复退还
+		if err := model.IncreaseUserQuota(task.UserId, quota, false); err != nil {
+			logger.LogWarn(ctx, "Failed to increase user quota: "+err.Error())
+		}
+		logContent := fmt.Sprintf("Video async task failed %s, refund %s", task.TaskID, logger.LogQuota(quota))
+		model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
 	}
 
 	return nil

+ 4 - 3
controller/telegram.go

@@ -6,10 +6,11 @@ import (
 	"encoding/hex"
 	"io"
 	"net/http"
-	"one-api/common"
-	"one-api/model"
 	"sort"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
+
 	"github.com/gin-contrib/sessions"
 	"github.com/gin-gonic/gin"
 )
@@ -65,7 +66,7 @@ func TelegramBind(c *gin.Context) {
 		return
 	}
 
-	c.Redirect(302, "/setting")
+	c.Redirect(302, "/console/personal")
 }
 
 func TelegramLogin(c *gin.Context) {

+ 3 - 2
controller/token.go

@@ -2,11 +2,12 @@ package controller
 
 import (
 	"net/http"
-	"one-api/common"
-	"one-api/model"
 	"strconv"
 	"strings"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
+
 	"github.com/gin-gonic/gin"
 )
 

+ 94 - 18
controller/topup.go

@@ -4,17 +4,18 @@ import (
 	"fmt"
 	"log"
 	"net/url"
-	"one-api/common"
-	"one-api/logger"
-	"one-api/model"
-	"one-api/service"
-	"one-api/setting"
-	"one-api/setting/operation_setting"
-	"one-api/setting/system_setting"
 	"strconv"
 	"sync"
 	"time"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/logger"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/service"
+	"github.com/QuantumNous/new-api/setting"
+	"github.com/QuantumNous/new-api/setting/operation_setting"
+	"github.com/QuantumNous/new-api/setting/system_setting"
+
 	"github.com/Calcium-Ion/go-epay/epay"
 	"github.com/gin-gonic/gin"
 	"github.com/samber/lo"
@@ -88,8 +89,9 @@ func GetEpayClient() *epay.Client {
 
 func getPayMoney(amount int64, group string) float64 {
 	dAmount := decimal.NewFromInt(amount)
-
-	if !common.DisplayInCurrencyEnabled {
+	// 充值金额以“展示类型”为准:
+	// - USD/CNY: 前端传 amount 为金额单位;TOKENS: 前端传 tokens,需要换成 USD 金额
+	if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
 		dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
 		dAmount = dAmount.Div(dQuotaPerUnit)
 	}
@@ -117,7 +119,7 @@ func getPayMoney(amount int64, group string) float64 {
 
 func getMinTopup() int64 {
 	minTopup := operation_setting.MinTopUp
-	if !common.DisplayInCurrencyEnabled {
+	if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
 		dMinTopup := decimal.NewFromInt(int64(minTopup))
 		dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
 		minTopup = int(dMinTopup.Mul(dQuotaPerUnit).IntPart())
@@ -178,18 +180,19 @@ func RequestEpay(c *gin.Context) {
 		return
 	}
 	amount := req.Amount
-	if !common.DisplayInCurrencyEnabled {
+	if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
 		dAmount := decimal.NewFromInt(int64(amount))
 		dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
 		amount = dAmount.Div(dQuotaPerUnit).IntPart()
 	}
 	topUp := &model.TopUp{
-		UserId:     id,
-		Amount:     amount,
-		Money:      payMoney,
-		TradeNo:    tradeNo,
-		CreateTime: time.Now().Unix(),
-		Status:     "pending",
+		UserId:        id,
+		Amount:        amount,
+		Money:         payMoney,
+		TradeNo:       tradeNo,
+		PaymentMethod: req.PaymentMethod,
+		CreateTime:    time.Now().Unix(),
+		Status:        "pending",
 	}
 	err = topUp.Insert()
 	if err != nil {
@@ -237,8 +240,8 @@ func EpayNotify(c *gin.Context) {
 		_, err := c.Writer.Write([]byte("fail"))
 		if err != nil {
 			log.Println("易支付回调写入失败")
-			return
 		}
+		return
 	}
 	verifyInfo, err := client.Verify(params)
 	if err == nil && verifyInfo.VerifyStatus {
@@ -314,3 +317,76 @@ func RequestAmount(c *gin.Context) {
 	}
 	c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)})
 }
+
+func GetUserTopUps(c *gin.Context) {
+	userId := c.GetInt("id")
+	pageInfo := common.GetPageQuery(c)
+	keyword := c.Query("keyword")
+
+	var (
+		topups []*model.TopUp
+		total  int64
+		err    error
+	)
+	if keyword != "" {
+		topups, total, err = model.SearchUserTopUps(userId, keyword, pageInfo)
+	} else {
+		topups, total, err = model.GetUserTopUps(userId, pageInfo)
+	}
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	pageInfo.SetTotal(int(total))
+	pageInfo.SetItems(topups)
+	common.ApiSuccess(c, pageInfo)
+}
+
+// GetAllTopUps 管理员获取全平台充值记录
+func GetAllTopUps(c *gin.Context) {
+	pageInfo := common.GetPageQuery(c)
+	keyword := c.Query("keyword")
+
+	var (
+		topups []*model.TopUp
+		total  int64
+		err    error
+	)
+	if keyword != "" {
+		topups, total, err = model.SearchAllTopUps(keyword, pageInfo)
+	} else {
+		topups, total, err = model.GetAllTopUps(pageInfo)
+	}
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+
+	pageInfo.SetTotal(int(total))
+	pageInfo.SetItems(topups)
+	common.ApiSuccess(c, pageInfo)
+}
+
+type AdminCompleteTopupRequest struct {
+	TradeNo string `json:"trade_no"`
+}
+
+// AdminCompleteTopUp 管理员补单接口
+func AdminCompleteTopUp(c *gin.Context) {
+	var req AdminCompleteTopupRequest
+	if err := c.ShouldBindJSON(&req); err != nil || req.TradeNo == "" {
+		common.ApiErrorMsg(c, "参数错误")
+		return
+	}
+
+	// 订单级互斥,防止并发补单
+	LockOrder(req.TradeNo)
+	defer UnlockOrder(req.TradeNo)
+
+	if err := model.ManualCompleteTopUp(req.TradeNo); err != nil {
+		common.ApiError(c, err)
+		return
+	}
+	common.ApiSuccess(c, nil)
+}

+ 17 - 14
controller/topup_stripe.go

@@ -5,15 +5,16 @@ import (
 	"io"
 	"log"
 	"net/http"
-	"one-api/common"
-	"one-api/model"
-	"one-api/setting"
-	"one-api/setting/operation_setting"
-	"one-api/setting/system_setting"
 	"strconv"
 	"strings"
 	"time"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/setting"
+	"github.com/QuantumNous/new-api/setting/operation_setting"
+	"github.com/QuantumNous/new-api/setting/system_setting"
+
 	"github.com/gin-gonic/gin"
 	"github.com/stripe/stripe-go/v81"
 	"github.com/stripe/stripe-go/v81/checkout/session"
@@ -83,12 +84,13 @@ func (*StripeAdaptor) RequestPay(c *gin.Context, req *StripePayRequest) {
 	}
 
 	topUp := &model.TopUp{
-		UserId:     id,
-		Amount:     req.Amount,
-		Money:      chargedMoney,
-		TradeNo:    referenceId,
-		CreateTime: time.Now().Unix(),
-		Status:     common.TopUpStatusPending,
+		UserId:        id,
+		Amount:        req.Amount,
+		Money:         chargedMoney,
+		TradeNo:       referenceId,
+		PaymentMethod: PaymentMethodStripe,
+		CreateTime:    time.Now().Unix(),
+		Status:        common.TopUpStatusPending,
 	}
 	err = topUp.Insert()
 	if err != nil {
@@ -225,7 +227,8 @@ func genStripeLink(referenceId string, customerId string, email string, amount i
 				Quantity: stripe.Int64(amount),
 			},
 		},
-		Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
+		Mode:                stripe.String(string(stripe.CheckoutSessionModePayment)),
+		AllowPromotionCodes: stripe.Bool(setting.StripePromotionCodesEnabled),
 	}
 
 	if "" == customerId {
@@ -257,7 +260,7 @@ func GetChargedAmount(count float64, user model.User) float64 {
 
 func getStripePayMoney(amount float64, group string) float64 {
 	originalAmount := amount
-	if !common.DisplayInCurrencyEnabled {
+	if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
 		amount = amount / common.QuotaPerUnit
 	}
 	// Using float64 for monetary calculations is acceptable here due to the small amounts involved
@@ -278,7 +281,7 @@ func getStripePayMoney(amount float64, group string) float64 {
 
 func getStripeMinTopup() int64 {
 	minTopup := setting.StripeMinTopUp
-	if !common.DisplayInCurrencyEnabled {
+	if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
 		minTopup = minTopup * int(common.QuotaPerUnit)
 	}
 	return int64(minTopup)

+ 3 - 2
controller/twofa.go

@@ -4,10 +4,11 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
-	"one-api/common"
-	"one-api/model"
 	"strconv"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
+
 	"github.com/gin-contrib/sessions"
 	"github.com/gin-gonic/gin"
 )

+ 2 - 1
controller/uptime_kuma.go

@@ -5,11 +5,12 @@ import (
 	"encoding/json"
 	"errors"
 	"net/http"
-	"one-api/setting/console_setting"
 	"strconv"
 	"strings"
 	"time"
 
+	"github.com/QuantumNous/new-api/setting/console_setting"
+
 	"github.com/gin-gonic/gin"
 	"golang.org/x/sync/errgroup"
 )

+ 3 - 2
controller/usedata.go

@@ -2,10 +2,11 @@ package controller
 
 import (
 	"net/http"
-	"one-api/common"
-	"one-api/model"
 	"strconv"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
+
 	"github.com/gin-gonic/gin"
 )
 

+ 61 - 7
controller/user.go

@@ -5,16 +5,17 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
-	"one-api/common"
-	"one-api/dto"
-	"one-api/logger"
-	"one-api/model"
-	"one-api/setting"
 	"strconv"
 	"strings"
 	"sync"
 
-	"one-api/constant"
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/dto"
+	"github.com/QuantumNous/new-api/logger"
+	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/setting"
+
+	"github.com/QuantumNous/new-api/constant"
 
 	"github.com/gin-contrib/sessions"
 	"github.com/gin-gonic/gin"
@@ -450,6 +451,10 @@ func GetSelf(c *gin.Context) {
 		"role":              user.Role,
 		"status":            user.Status,
 		"email":             user.Email,
+		"github_id":         user.GitHubId,
+		"oidc_id":           user.OidcId,
+		"wechat_id":         user.WeChatId,
+		"telegram_id":       user.TelegramId,
 		"group":             user.Group,
 		"quota":             user.Quota,
 		"used_quota":        user.UsedQuota,
@@ -1098,6 +1103,9 @@ type UpdateUserSettingRequest struct {
 	WebhookSecret              string  `json:"webhook_secret,omitempty"`
 	NotificationEmail          string  `json:"notification_email,omitempty"`
 	BarkUrl                    string  `json:"bark_url,omitempty"`
+	GotifyUrl                  string  `json:"gotify_url,omitempty"`
+	GotifyToken                string  `json:"gotify_token,omitempty"`
+	GotifyPriority             int     `json:"gotify_priority,omitempty"`
 	AcceptUnsetModelRatioModel bool    `json:"accept_unset_model_ratio_model"`
 	RecordIpLog                bool    `json:"record_ip_log"`
 }
@@ -1113,7 +1121,7 @@ func UpdateUserSetting(c *gin.Context) {
 	}
 
 	// 验证预警类型
-	if req.QuotaWarningType != dto.NotifyTypeEmail && req.QuotaWarningType != dto.NotifyTypeWebhook && req.QuotaWarningType != dto.NotifyTypeBark {
+	if req.QuotaWarningType != dto.NotifyTypeEmail && req.QuotaWarningType != dto.NotifyTypeWebhook && req.QuotaWarningType != dto.NotifyTypeBark && req.QuotaWarningType != dto.NotifyTypeGotify {
 		c.JSON(http.StatusOK, gin.H{
 			"success": false,
 			"message": "无效的预警类型",
@@ -1188,6 +1196,40 @@ func UpdateUserSetting(c *gin.Context) {
 		}
 	}
 
+	// 如果是Gotify类型,验证Gotify URL和Token
+	if req.QuotaWarningType == dto.NotifyTypeGotify {
+		if req.GotifyUrl == "" {
+			c.JSON(http.StatusOK, gin.H{
+				"success": false,
+				"message": "Gotify服务器地址不能为空",
+			})
+			return
+		}
+		if req.GotifyToken == "" {
+			c.JSON(http.StatusOK, gin.H{
+				"success": false,
+				"message": "Gotify令牌不能为空",
+			})
+			return
+		}
+		// 验证URL格式
+		if _, err := url.ParseRequestURI(req.GotifyUrl); err != nil {
+			c.JSON(http.StatusOK, gin.H{
+				"success": false,
+				"message": "无效的Gotify服务器地址",
+			})
+			return
+		}
+		// 检查是否是HTTP或HTTPS
+		if !strings.HasPrefix(req.GotifyUrl, "https://") && !strings.HasPrefix(req.GotifyUrl, "http://") {
+			c.JSON(http.StatusOK, gin.H{
+				"success": false,
+				"message": "Gotify服务器地址必须以http://或https://开头",
+			})
+			return
+		}
+	}
+
 	userId := c.GetInt("id")
 	user, err := model.GetUserById(userId, true)
 	if err != nil {
@@ -1221,6 +1263,18 @@ func UpdateUserSetting(c *gin.Context) {
 		settings.BarkUrl = req.BarkUrl
 	}
 
+	// 如果是Gotify类型,添加Gotify配置到设置中
+	if req.QuotaWarningType == dto.NotifyTypeGotify {
+		settings.GotifyUrl = req.GotifyUrl
+		settings.GotifyToken = req.GotifyToken
+		// Gotify优先级范围0-10,超出范围则使用默认值5
+		if req.GotifyPriority < 0 || req.GotifyPriority > 10 {
+			settings.GotifyPriority = 5
+		} else {
+			settings.GotifyPriority = req.GotifyPriority
+		}
+	}
+
 	// 更新用户设置
 	user.SetSetting(settings)
 	if err := user.Update(false); err != nil {

+ 2 - 2
controller/vendor_meta.go

@@ -3,8 +3,8 @@ package controller
 import (
 	"strconv"
 
-	"one-api/common"
-	"one-api/model"
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
 
 	"github.com/gin-gonic/gin"
 )

+ 130 - 0
controller/video_proxy.go

@@ -0,0 +1,130 @@
+package controller
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"time"
+
+	"github.com/QuantumNous/new-api/logger"
+	"github.com/QuantumNous/new-api/model"
+
+	"github.com/gin-gonic/gin"
+)
+
+func VideoProxy(c *gin.Context) {
+	taskID := c.Param("task_id")
+	if taskID == "" {
+		c.JSON(http.StatusBadRequest, gin.H{
+			"error": gin.H{
+				"message": "task_id is required",
+				"type":    "invalid_request_error",
+			},
+		})
+		return
+	}
+
+	task, exists, err := model.GetByOnlyTaskId(taskID)
+	if err != nil {
+		logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to query task %s: %s", taskID, err.Error()))
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"error": gin.H{
+				"message": "Failed to query task",
+				"type":    "server_error",
+			},
+		})
+		return
+	}
+	if !exists || task == nil {
+		logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to get task %s: %s", taskID, err.Error()))
+		c.JSON(http.StatusNotFound, gin.H{
+			"error": gin.H{
+				"message": "Task not found",
+				"type":    "invalid_request_error",
+			},
+		})
+		return
+	}
+
+	if task.Status != model.TaskStatusSuccess {
+		c.JSON(http.StatusBadRequest, gin.H{
+			"error": gin.H{
+				"message": fmt.Sprintf("Task is not completed yet, current status: %s", task.Status),
+				"type":    "invalid_request_error",
+			},
+		})
+		return
+	}
+
+	channel, err := model.CacheGetChannel(task.ChannelId)
+	if err != nil {
+		logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to get channel %d: %s", task.ChannelId, err.Error()))
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"error": gin.H{
+				"message": "Failed to retrieve channel information",
+				"type":    "server_error",
+			},
+		})
+		return
+	}
+	baseURL := channel.GetBaseURL()
+	if baseURL == "" {
+		baseURL = "https://api.openai.com"
+	}
+	videoURL := fmt.Sprintf("%s/v1/videos/%s/content", baseURL, task.TaskID)
+
+	client := &http.Client{
+		Timeout: 60 * time.Second,
+	}
+
+	req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, videoURL, nil)
+	if err != nil {
+		logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to create request for %s: %s", videoURL, err.Error()))
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"error": gin.H{
+				"message": "Failed to create proxy request",
+				"type":    "server_error",
+			},
+		})
+		return
+	}
+
+	req.Header.Set("Authorization", "Bearer "+channel.Key)
+
+	resp, err := client.Do(req)
+	if err != nil {
+		logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to fetch video from %s: %s", videoURL, err.Error()))
+		c.JSON(http.StatusBadGateway, gin.H{
+			"error": gin.H{
+				"message": "Failed to fetch video content",
+				"type":    "server_error",
+			},
+		})
+		return
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		logger.LogError(c.Request.Context(), fmt.Sprintf("Upstream returned status %d for %s", resp.StatusCode, videoURL))
+		c.JSON(http.StatusBadGateway, gin.H{
+			"error": gin.H{
+				"message": fmt.Sprintf("Upstream service returned status %d", resp.StatusCode),
+				"type":    "server_error",
+			},
+		})
+		return
+	}
+
+	for key, values := range resp.Header {
+		for _, value := range values {
+			c.Writer.Header().Add(key, value)
+		}
+	}
+
+	c.Writer.Header().Set("Cache-Control", "public, max-age=86400") // Cache for 24 hours
+	c.Writer.WriteHeader(resp.StatusCode)
+	_, err = io.Copy(c.Writer, resp.Body)
+	if err != nil {
+		logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to stream video content: %s", err.Error()))
+	}
+}

+ 3 - 2
controller/wechat.go

@@ -5,11 +5,12 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
-	"one-api/common"
-	"one-api/model"
 	"strconv"
 	"time"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/model"
+
 	"github.com/gin-contrib/sessions"
 	"github.com/gin-gonic/gin"
 )

+ 51 - 19
docker-compose.yml

@@ -1,4 +1,18 @@
-version: '3.4'
+# New-API Docker Compose Configuration
+# 
+# Quick Start:
+#   1. docker-compose up -d
+#   2. Access at http://localhost:3000
+#
+# Using MySQL instead of PostgreSQL:
+#   1. Comment out the postgres service and SQL_DSN line 15
+#   2. Uncomment the mysql service and SQL_DSN line 16
+#   3. Uncomment mysql in depends_on (line 28)
+#   4. Uncomment mysql_data in volumes section (line 64)
+#
+# ⚠️  IMPORTANT: Change all default passwords before deploying to production!
+
+version: '3.4' # For compatibility with older Docker versions
 
 services:
   new-api:
@@ -12,21 +26,25 @@ services:
       - ./data:/data
       - ./logs:/app/logs
     environment:
-      - SQL_DSN=root:123456@tcp(mysql:3306)/new-api  # Point to the mysql service
+      - SQL_DSN=postgresql://root:123456@postgres:5432/new-api # ⚠️ IMPORTANT: Change the password in production!
+#      - SQL_DSN=root:123456@tcp(mysql:3306)/new-api  # Point to the mysql service, uncomment if using MySQL
       - REDIS_CONN_STRING=redis://redis
       - TZ=Asia/Shanghai
-      - ERROR_LOG_ENABLED=true # 是否启用错误日志记录
-    #      - STREAMING_TIMEOUT=300  # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值
-    #      - SESSION_SECRET=random_string  # 多机部署时设置,必须修改这个随机字符串!!!!!!!
-    #      - NODE_TYPE=slave  # Uncomment for slave node in multi-node deployment
-    #      - SYNC_FREQUENCY=60  # Uncomment if regular database syncing is needed
-    #      - FRONTEND_BASE_URL=https://openai.justsong.cn  # Uncomment for multi-node deployment with front-end URL
+      - ERROR_LOG_ENABLED=true # 是否启用错误日志记录 (Whether to enable error log recording)
+      - BATCH_UPDATE_ENABLED=true  # 是否启用批量更新 (Whether to enable batch update)
+#      - STREAMING_TIMEOUT=300  # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值 (Streaming timeout in seconds, default is 120s. Increase if experiencing empty completions)
+#      - SESSION_SECRET=random_string  # 多机部署时设置,必须修改这个随机字符串!! (multi-node deployment, set this to a random string!!!!!!!)
+#      - SYNC_FREQUENCY=60  # Uncomment if regular database syncing is needed
+#      - GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX  # Google Analytics 的测量 ID (Google Analytics Measurement ID)
+#      - UMAMI_WEBSITE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx  # Umami 网站 ID (Umami Website ID)
+#      - UMAMI_SCRIPT_URL=https://analytics.umami.is/script.js  # Umami 脚本 URL,默认为官方地址 (Umami Script URL, defaults to official URL)
 
     depends_on:
       - redis
-      - mysql
+      - postgres
+#      - mysql  # Uncomment if using MySQL
     healthcheck:
-      test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk -F: '{print $$2}'"]
+      test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' || exit 1"]
       interval: 30s
       timeout: 10s
       retries: 3
@@ -36,17 +54,31 @@ services:
     container_name: redis
     restart: always
 
-  mysql:
-    image: mysql:8.2
-    container_name: mysql
+  postgres:
+    image: postgres:15
+    container_name: postgres
     restart: always
     environment:
-      MYSQL_ROOT_PASSWORD: 123456  # Ensure this matches the password in SQL_DSN
-      MYSQL_DATABASE: new-api
+      POSTGRES_USER: root
+      POSTGRES_PASSWORD: 123456  # ⚠️ IMPORTANT: Change this password in production!
+      POSTGRES_DB: new-api
     volumes:
-      - mysql_data:/var/lib/mysql
-    # ports:
-    #   - "3306:3306"  # If you want to access MySQL from outside Docker, uncomment
+      - pg_data:/var/lib/postgresql/data
+#    ports:
+#      - "5432:5432"  # Uncomment if you need to access PostgreSQL from outside Docker
+
+#  mysql:
+#    image: mysql:8.2
+#    container_name: mysql
+#    restart: always
+#    environment:
+#      MYSQL_ROOT_PASSWORD: 123456  # ⚠️ IMPORTANT: Change this password in production!
+#      MYSQL_DATABASE: new-api
+#    volumes:
+#      - mysql_data:/var/lib/mysql
+#    ports:
+#      - "3306:3306"  # Uncomment if you need to access MySQL from outside Docker
 
 volumes:
-  mysql_data:
+  pg_data:
+#  mysql_data:

+ 107 - 0
docs/translation-glossary.fr.md

@@ -0,0 +1,107 @@
+# Glossaire Français (French Glossary)
+
+Ce document fournit des traductions standards françaises pour la terminologie clé du projet afin d'assurer la cohérence et la précision des traductions.
+
+This document provides standard French translations for key project terminology to ensure consistency and accuracy in translations.
+
+## Concepts de Base (Core Concepts)
+
+- L'utilisation d'émojis dans les traductions est autorisée s'ils sont présents dans l'original
+- L'utilisation de termes purement techniques est autorisée s'ils sont présents dans l'original
+- L'utilisation de termes techniques en anglais est autorisée s'ils sont largement utilisés dans l'environnement technique francophone (par exemple, API)
+
+| Chinois | Français | Anglais | Description |
+|---------|----------|---------|-------------|
+| 倍率 | Ratio | Ratio/Multiplier | Multiplicateur utilisé pour le calcul des prix. **Important :** Dans le contexte des calculs de prix, toujours utiliser "Ratio" plutôt que "Multiplicateur" pour assurer la cohérence terminologique |
+| 令牌 | Jeton | Token | Identifiants d'accès API ou unités de texte traitées par les modèles |
+| 渠道 | Canal | Channel | Canal d'accès aux fournisseurs d'API |
+| 分组 | Groupe | Group | Classification des utilisateurs ou des jetons |
+| 额度 | Quota | Quota | Quota de services disponible pour l'utilisateur |
+
+## Modèles (Model Related)
+
+| Chinois | Français | Anglais | Description |
+|---------|----------|---------|-------------|
+| 提示 | Invite | Prompt | Contenu d'entrée du modèle |
+| 补全 | Complétion | Completion | Contenu de sortie du modèle. **Important :** Ne pas utiliser "Achèvement" ou "Finalisation" - uniquement "Complétion" pour correspondre à la terminologie technique |
+| 输入 | Entrée | Input/Prompt | Contenu envoyé au modèle |
+| 输出 | Sortie | Output/Completion | Contenu retourné par le modèle |
+| 模型倍率 | Ratio du modèle | Model Ratio | Ratio de tarification pour différents modèles |
+| 补全倍率 | Ratio de complétion | Completion Ratio | Ratio de tarification supplémentaire pour la sortie |
+| 固定价格 | Prix fixe | Price per call | Prix par appel |
+| 按量计费 | Paiement à l'utilisation | Pay-as-you-go | Tarification basée sur l'utilisation |
+| 按次计费 | Paiement par appel | Pay-per-view | Prix fixe par appel |
+
+## Gestion des Utilisateurs (User Management)
+
+| Chinois | Français | Anglais | Description |
+|---------|----------|---------|-------------|
+| 超级管理员 | Super-administrateur | Root User | Administrateur avec les privilèges les plus élevés |
+| 管理员 | Administrateur | Admin User | Administrateur système |
+| 普通用户 | Utilisateur normal | Normal User | Utilisateur avec privilèges standards |
+
+## Recharge et Échange (Recharge & Redemption)
+
+| Chinois | Français | Anglais | Description |
+|---------|----------|---------|-------------|
+| 充值 | Recharge | Top Up | Ajout de quota au compte |
+| 兑换码 | Code d'échange | Redemption Code | Code qui peut être échangé contre du quota |
+
+## Gestion des Canaux (Channel Management)
+
+| Chinois | Français | Anglais | Description |
+|---------|----------|---------|-------------|
+| 渠道 | Canal | Channel | Canal du fournisseur d'API |
+| API密钥 | Clé API | API Key | Clé d'accès API. **Important :** Utiliser "Clé API" au lieu de "Jeton API" pour plus de précision et conformément à la terminologie technique francophone établie. Le terme "Clé" reflète mieux la fonctionnalité d'accès aux ressources, tandis que "Jeton" est plus souvent associé aux unités de texte dans le contexte du traitement des modèles linguistiques. |
+| 优先级 | Priorité | Priority | Priorité de sélection du canal |
+| 权重 | Poids | Weight | Poids d'équilibrage de charge |
+| 代理 | Proxy | Proxy | Adresse du serveur proxy |
+| 模型重定向 | Redirection de modèle | Model Mapping | Remplacement du nom du modèle dans le corps de la requête |
+| 供应商 | Fournisseur | Provider/Vendor | Fournisseur de services ou d'API |
+
+## Sécurité (Security Related)
+
+| Chinois | Français | Anglais | Description |
+|---------|----------|---------|-------------|
+| 两步验证 | Authentification à deux facteurs | Two-Factor Authentication | Méthode de vérification de sécurité supplémentaire pour les comptes |
+| 2FA | 2FA | Two-Factor Authentication | Abréviation de l'authentification à deux facteurs |
+
+## Recommandations de Traduction (Translation Guidelines)
+
+### Variantes Contextuelles de Traduction
+
+**Invite/Entrée (Prompt/Input)**
+
+- **Invite** : Lors de l'interaction avec les LLM, dans l'interface utilisateur, lors de la description de l'interaction avec le modèle
+- **Entrée** : Dans la tarification, la documentation technique, la description du processus de traitement des données
+- **Règle** : S'il s'agit de l'expérience utilisateur et de l'interaction avec l'IA → "Invite", s'il s'agit du processus technique ou des calculs → "Entrée"
+
+**Jeton (Token)**
+
+- Jeton d'accès API (API Token)
+- Unité de texte traitée par le modèle (Text Token)
+- Jeton d'accès système (Access Token)
+
+**Quota (Quota)**
+
+- Quota de services disponible pour l'utilisateur
+- Parfois traduit comme "Crédit"
+
+### Particularités de la Langue Française
+
+- **Formes plurielles** : Nécessite une implémentation correcte des formes plurielles (_one, _other)
+- **Accords grammaticaux** : Attention aux accords grammaticaux dans les termes techniques
+- **Genre grammatical** : Accord du genre des termes techniques (par exemple, "modèle" - masculin, "canal" - masculin)
+
+### Termes Standardisés
+
+- **Complétion (Completion)** : Contenu de sortie du modèle
+- **Ratio (Ratio)** : Multiplicateur pour le calcul des prix
+- **Code d'échange (Redemption Code)** : Utilisé au lieu de "Code d'échange" pour plus de précision
+- **Fournisseur (Provider/Vendor)** : Organisation ou service fournissant des API ou des modèles d'IA
+
+---
+
+**Note pour les contributeurs :** Si vous trouvez des incohérences dans les traductions de terminologie ou si vous avez de meilleures suggestions de traduction pour le français, n'hésitez pas à créer une Issue ou une Pull Request.
+
+**Contribution Note for French:** If you find any inconsistencies in terminology translations or have better translation suggestions for French, please feel free to submit an Issue or Pull Request.

+ 86 - 0
docs/translation-glossary.md

@@ -0,0 +1,86 @@
+# 翻译术语表 (Translation Glossary)
+
+本文档为翻译贡献者提供项目中关键术语的标准翻译参考,以确保翻译的一致性和准确性。
+
+This document provides standard translation references for key terminology in the project to ensure consistency and accuracy for translation contributors.
+
+## 核心概念 (Core Concepts)
+
+| 中文 | English | 说明 | Description |
+|------|---------|------|-------------|
+| 倍率 | Ratio | 用于计算价格的乘数因子 | Multiplier factor used for price calculation |
+| 令牌 | Token | API访问凭证,也指模型处理的文本单元 | API access credentials or text units processed by models |
+| 渠道 | Channel | API服务提供商的接入通道 | Access channel for API service providers |
+| 分组 | Group | 用户或令牌的分类,影响价格倍率 | Classification of users or tokens, affecting price ratios |
+| 额度 | Quota | 用户可用的服务额度 | Available service quota for users |
+
+## 模型相关 (Model Related)
+
+| 中文 | English | 说明 | Description |
+|------|---------|------|-------------|
+| 提示 | Prompt | 模型输入内容 | Model input content |
+| 补全 | Completion | 模型输出内容 | Model output content |
+| 输入 | Input/Prompt | 发送给模型的内容 | Content sent to the model |
+| 输出 | Output/Completion | 模型返回的内容 | Content returned by the model |
+| 模型倍率 | Model Ratio | 不同模型的计费倍率 | Billing ratio for different models |
+| 补全倍率 | Completion Ratio | 输出内容的额外计费倍率 | Additional billing ratio for output content |
+| 固定价格 | Price per call | 按次计费的价格 | Fixed price per call |
+| 按量计费 | Pay-as-you-go | 根据使用量计费 | Billing based on usage |
+| 按次计费 | Pay-per-view | 每次调用固定价格 | Fixed price per invocation |
+
+## 用户管理 (User Management)
+
+| 中文 | English | 说明 | Description |
+|------|---------|------|-------------|
+| 超级管理员 | Root User | 最高权限管理员 | Administrator with highest privileges |
+| 管理员 | Admin User | 系统管理员 | System administrator |
+| 普通用户 | Normal User | 普通权限用户 | Regular user with standard privileges |
+
+## 充值与兑换 (Recharge & Redemption)
+
+| 中文 | English | 说明 | Description |
+|------|---------|------|-------------|
+| 充值 | Top Up | 为账户增加额度 | Add quota to account |
+| 兑换码 | Redemption Code | 可兑换额度的代码 | Code that can be redeemed for quota |
+
+## 渠道管理 (Channel Management)
+
+| 中文 | English | 说明 | Description |
+|------|---------|------|-------------|
+| 渠道 | Channel | API服务提供通道 | API service provider channel |
+| 密钥 | Key | API访问密钥 | API access key |
+| 优先级 | Priority | 渠道选择优先级 | Channel selection priority |
+| 权重 | Weight | 负载均衡权重 | Load balancing weight |
+| 代理 | Proxy | 代理服务器地址 | Proxy server address |
+| 模型重定向 | Model Mapping | 请求体中模型名称替换 | Model name replacement in request body |
+
+## 安全相关 (Security Related)
+
+| 中文 | English | 说明 | Description |
+|------|---------|------|-------------|
+| 两步验证 | Two-Factor Authentication | 为账户提供额外安全保护的验证方式 | Additional security verification method for accounts |
+| 2FA | Two-Factor Authentication | 两步验证的缩写 | Abbreviation for Two-Factor Authentication |
+
+## 计费相关 (Billing Related)
+
+| 中文 | English | 说明 | Description |
+|------|---------|------|-------------|
+| 倍率 | Ratio | 价格计算的乘数因子 | Multiplier factor used for price calculation |
+| 倍率 | Multiplier | 价格计算的乘数因子(同义词) | Multiplier factor used for price calculation (synonym) |
+
+## 翻译注意事项 (Translation Guidelines)
+
+- **提示 (Prompt)** = 模型输入内容 / Model input content
+- **补全 (Completion)** = 模型输出内容 / Model output content
+- **倍率 (Ratio)** = 价格计算的乘数因子 / Multiplier factor for price calculation
+- **额度 (Quota)** = 可用的用户服务额度,有时也翻译为 Credit / Available service quota for users, sometimes also translated as Credit
+- **Token** = 根据上下文可能指 / Depending on context, may refer to:
+  - API访问令牌 (API Token)
+  - 模型处理的文本单元 (Text Token)
+  - 系统访问令牌 (Access Token)
+
+---
+
+**贡献说明**: 如发现术语翻译不一致或有更好的翻译建议,欢迎提交 Issue 或 Pull Request。
+
+**Contribution Note**: If you find any inconsistencies in terminology translations or have better translation suggestions, please feel free to submit an Issue or Pull Request.

+ 107 - 0
docs/translation-glossary.ru.md

@@ -0,0 +1,107 @@
+# Русский глоссарий (Russian Glossary)
+
+Данный раздел предоставляет стандартные переводы ключевой терминологии проекта на русский язык для обеспечения согласованности и точности переводов.
+
+This section provides standard Russian translations for key project terminology to ensure consistency and accuracy in translations.
+
+## Основные концепции (Core Concepts)
+
+- Допускается использовать символы Emoji в переводе, если они были в оригинале.
+- Допускается использование сугубо технических терминов, если они были в оригинале.
+- Допускается использование технических терминов на английском языке, если они широко используются в русскоязычной технической среде (например, API).
+
+| Китайский | Русский | Английский | Описание |
+|-----------|--------|-----------|----------|
+| 倍率 | Коэффициент | Ratio/Multiplier | Множитель для расчета цены. **Важно:** В контексте расчетов цен всегда использовать "Коэффициент", а не "Множитель" для обеспечения консистентности терминологии |
+| 令牌 | Токен | Token | Учетные данные API или текстовые единицы |
+| 渠道 | Канал | Channel | Канал доступа к поставщику API |
+| 分组 | Группа | Group | Классификация пользователей или токенов |
+| 额度 | Квота | Quota | Доступная квота услуг для пользователя |
+
+## Модели (Model Related)
+
+| Китайский | Русский | Английский | Описание |
+|-----------|--------|-----------|----------|
+| 提示 | Промпт/Ввод | Prompt | Содержимое ввода в модель |
+| 补全 | Вывод | Completion | Содержимое вывода модели. **Важно:** Не использовать "Дополнение" или "Завершение" - только "Вывод" для соответствия технической терминологии |
+| 输入 | Ввод | Input/Prompt | Содержимое, отправляемое в модель |
+| 输出 | Вывод | Output/Completion | Содержимое, возвращаемое моделью |
+| 模型倍率 | Коэффициент модели | Model Ratio | Коэффициент тарификации для разных моделей |
+| 补全倍率 | Коэффициент вывода | Completion Ratio | Дополнительный коэффициент тарификации для вывода |
+| 固定价格 | Цена за запрос | Price per call | Цена за один вызов |
+| 按量计费 | Оплата по объему | Pay-as-you-go | Тарификация на основе использования |
+| 按次计费 | Оплата за запрос | Pay-per-view | Фиксированная цена за вызов |
+
+## Управление пользователями (User Management)
+
+| Китайский | Русский | Английский | Описание |
+|-----------|--------|-----------|----------|
+| 超级管理员 | Суперадминистратор | Root User | Администратор с наивысшими привилегиями |
+| 管理员 | Администратор | Admin User | Системный администратор |
+| 普通用户 | Обычный пользователь | Normal User | Пользователь со стандартными привилегиями |
+
+## Пополнение и обмен (Recharge & Redemption)
+
+| Китайский | Русский | Английский | Описание |
+|-----------|--------|-----------|----------|
+| 充值 | Пополнение | Top Up | Добавление квоты на аккаунт |
+| 兑换码 | Код купона | Redemption Code | Код, который можно обменять на квоту |
+
+## Управление каналами (Channel Management)
+
+| Китайский | Русский | Английский | Описание |
+|-----------|--------|-----------|----------|
+| 渠道 | Канал | Channel | Канал поставщика API |
+| API密钥 | API ключ | API Key | Ключ доступа к API. **Важно:** Использовать "API ключ" вместо "API токен" для большей точности и соответствия общепринятой русскоязычной технической терминологии. Термин "ключ" более точно отражает функционал доступа к ресурсам, в то время как "токен" чаще ассоциируется с текстовыми единицами в контексте обработки языковых моделей. |
+| 优先级 | Приоритет | Priority | Приоритет выбора канала |
+| 权重 | Вес | Weight | Вес балансировки нагрузки |
+| 代理 | Прокси | Proxy | Адрес прокси-сервера |
+| 模型重定向 | Перенаправление модели | Model Mapping | Замена имени модели в теле запроса |
+| 供应商 | Поставщик | Provider/Vendor | Поставщик услуг или API |
+
+## Безопасность (Security Related)
+
+| Китайский | Русский | Английский | Описание |
+|-----------|--------|-----------|----------|
+| 两步验证 | Двухфакторная аутентификация | Two-Factor Authentication | Дополнительный метод проверки безопасности для аккаунтов |
+| 2FA | 2FA | Two-Factor Authentication | Аббревиатура двухфакторной аутентификации |
+
+## Рекомендации по переводу (Translation Guidelines)
+
+### Контекстуальные варианты перевода
+
+**Промпт/Ввод (Prompt/Input)**
+
+- **Промпт**: При общении с LLM, в пользовательском интерфейсе, при описании взаимодействия с моделью
+- **Ввод**: При тарификации, технической документации, описании процесса обработки данных
+- **Правило**: Если речь о пользовательском опыте и взаимодействии с AI → "Промпт", если о техническом процессе или расчетах → "Ввод"
+
+**Token**
+
+- API токен доступа (API Token)
+- Текстовая единица, обрабатываемая моделью (Text Token)
+- Токен доступа к системе (Access Token)
+
+**Квота (Quota)**
+
+- Доступная квота услуг пользователя
+- Иногда переводится как "Кредит"
+
+### Особенности русского языка
+
+- **Множественные формы**: Требуется правильная реализация множественных форм (_one,_few, _many,_other)
+- **Падежные окончания**: Внимательное отношение к падежным окончаниям в технических терминах
+- **Грамматический род**: Согласование рода технических терминов (например, "модель" - женский род, "канал" - мужской род)
+
+### Стандартизированные термины
+
+- **Вывод (Completion)**: Содержимое вывода модели
+- **Коэффициент (Ratio)**: Множитель для расчета цены
+- **Код купона (Redemption Code)**: Используется вместо "Код обмена" для большей точности
+- **Поставщик (Provider/Vendor)**: Организация или сервис, предоставляющий API или AI-модели
+
+---
+
+**Примечание для участников:** При обнаружении несогласованности в переводах терминологии или наличии лучших предложений по переводу, не стесняйтесь создавать Issue или Pull Request.
+
+**Contribution Note for Russian:** If you find any inconsistencies in terminology translations or have better translation suggestions for Russian, please feel free to submit an Issue or Pull Request.

+ 11 - 6
dto/audio.go

@@ -1,17 +1,22 @@
 package dto
 
 import (
-	"one-api/types"
+	"encoding/json"
+
+	"github.com/QuantumNous/new-api/types"
 
 	"github.com/gin-gonic/gin"
 )
 
 type AudioRequest struct {
-	Model          string  `json:"model"`
-	Input          string  `json:"input"`
-	Voice          string  `json:"voice"`
-	Speed          float64 `json:"speed,omitempty"`
-	ResponseFormat string  `json:"response_format,omitempty"`
+	Model          string          `json:"model"`
+	Input          string          `json:"input"`
+	Voice          string          `json:"voice"`
+	Instructions   string          `json:"instructions,omitempty"`
+	ResponseFormat string          `json:"response_format,omitempty"`
+	Speed          float64         `json:"speed,omitempty"`
+	StreamFormat   string          `json:"stream_format,omitempty"`
+	Metadata       json.RawMessage `json:"metadata,omitempty"`
 }
 
 func (r *AudioRequest) GetTokenCountMeta() *types.TokenCountMeta {

+ 19 - 0
dto/channel_settings.go

@@ -16,7 +16,26 @@ const (
 	VertexKeyTypeAPIKey VertexKeyType = "api_key"
 )
 
+type AwsKeyType string
+
+const (
+	AwsKeyTypeAKSK   AwsKeyType = "ak_sk" // 默认
+	AwsKeyTypeApiKey AwsKeyType = "api_key"
+)
+
 type ChannelOtherSettings struct {
 	AzureResponsesVersion string        `json:"azure_responses_version,omitempty"`
 	VertexKeyType         VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key"
+	OpenRouterEnterprise  *bool         `json:"openrouter_enterprise,omitempty"`
+	AllowServiceTier      bool          `json:"allow_service_tier,omitempty"`      // 是否允许 service_tier 透传(默认过滤以避免额外计费)
+	DisableStore          bool          `json:"disable_store,omitempty"`           // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用)
+	AllowSafetyIdentifier bool          `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私)
+	AwsKeyType            AwsKeyType    `json:"aws_key_type,omitempty"`
+}
+
+func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool {
+	if s == nil || s.OpenRouterEnterprise == nil {
+		return false
+	}
+	return *s.OpenRouterEnterprise
 }

+ 17 - 8
dto/claude.go

@@ -3,10 +3,11 @@ package dto
 import (
 	"encoding/json"
 	"fmt"
-	"one-api/common"
-	"one-api/types"
 	"strings"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/types"
+
 	"github.com/gin-gonic/gin"
 )
 
@@ -23,7 +24,7 @@ type ClaudeMediaMessage struct {
 	StopReason   *string              `json:"stop_reason,omitempty"`
 	PartialJson  *string              `json:"partial_json,omitempty"`
 	Role         string               `json:"role,omitempty"`
-	Thinking     string               `json:"thinking,omitempty"`
+	Thinking     *string              `json:"thinking,omitempty"`
 	Signature    string               `json:"signature,omitempty"`
 	Delta        string               `json:"delta,omitempty"`
 	CacheControl json.RawMessage      `json:"cache_control,omitempty"`
@@ -147,6 +148,10 @@ func (c *ClaudeMessage) SetStringContent(content string) {
 	c.Content = content
 }
 
+func (c *ClaudeMessage) SetContent(content any) {
+	c.Content = content
+}
+
 func (c *ClaudeMessage) ParseContent() ([]ClaudeMediaMessage, error) {
 	return common.Any2Type[[]ClaudeMediaMessage](c.Content)
 }
@@ -195,11 +200,15 @@ type ClaudeRequest struct {
 	Temperature       *float64        `json:"temperature,omitempty"`
 	TopP              float64         `json:"top_p,omitempty"`
 	TopK              int             `json:"top_k,omitempty"`
-	//ClaudeMetadata    `json:"metadata,omitempty"`
-	Stream     bool      `json:"stream,omitempty"`
-	Tools      any       `json:"tools,omitempty"`
-	ToolChoice any       `json:"tool_choice,omitempty"`
-	Thinking   *Thinking `json:"thinking,omitempty"`
+	Stream            bool            `json:"stream,omitempty"`
+	Tools             any             `json:"tools,omitempty"`
+	ContextManagement json.RawMessage `json:"context_management,omitempty"`
+	ToolChoice        any             `json:"tool_choice,omitempty"`
+	Thinking          *Thinking       `json:"thinking,omitempty"`
+	McpServers        json.RawMessage `json:"mcp_servers,omitempty"`
+	Metadata          json.RawMessage `json:"metadata,omitempty"`
+	// 服务层级字段,用于指定 API 服务等级。允许透传可能导致实际计费高于预期,默认应过滤
+	ServiceTier string `json:"service_tier,omitempty"`
 }
 
 func (c *ClaudeRequest) GetTokenCountMeta() *types.TokenCountMeta {

+ 2 - 1
dto/embedding.go

@@ -1,9 +1,10 @@
 package dto
 
 import (
-	"one-api/types"
 	"strings"
 
+	"github.com/QuantumNous/new-api/types"
+
 	"github.com/gin-gonic/gin"
 )
 

+ 1 - 1
dto/error.go

@@ -1,6 +1,6 @@
 package dto
 
-import "one-api/types"
+import "github.com/QuantumNous/new-api/types"
 
 type OpenAIError struct {
 	Message string `json:"message"`

+ 14 - 7
dto/gemini.go

@@ -2,14 +2,17 @@ package dto
 
 import (
 	"encoding/json"
-	"github.com/gin-gonic/gin"
-	"one-api/common"
-	"one-api/logger"
-	"one-api/types"
 	"strings"
+
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/logger"
+	"github.com/QuantumNous/new-api/types"
+
+	"github.com/gin-gonic/gin"
 )
 
 type GeminiChatRequest struct {
+	Requests           []GeminiChatRequest        `json:"requests,omitempty"` // For batch requests
 	Contents           []GeminiChatContent        `json:"contents"`
 	SafetySettings     []GeminiChatSafetySettings `json:"safetySettings,omitempty"`
 	GenerationConfig   GeminiChatGenerationConfig `json:"generationConfig,omitempty"`
@@ -251,6 +254,7 @@ type GeminiChatTool struct {
 	GoogleSearchRetrieval any `json:"googleSearchRetrieval,omitempty"`
 	CodeExecution         any `json:"codeExecution,omitempty"`
 	FunctionDeclarations  any `json:"functionDeclarations,omitempty"`
+	URLContext            any `json:"urlContext,omitempty"`
 }
 
 type GeminiChatGenerationConfig struct {
@@ -272,6 +276,7 @@ type GeminiChatGenerationConfig struct {
 	ResponseModalities []string              `json:"responseModalities,omitempty"`
 	ThinkingConfig     *GeminiThinkingConfig `json:"thinkingConfig,omitempty"`
 	SpeechConfig       json.RawMessage       `json:"speechConfig,omitempty"` // RawMessage to allow flexible speech config
+	ImageConfig        json.RawMessage       `json:"imageConfig,omitempty"`  // RawMessage to allow flexible image config
 }
 
 type MediaResolution string
@@ -290,12 +295,13 @@ type GeminiChatSafetyRating struct {
 
 type GeminiChatPromptFeedback struct {
 	SafetyRatings []GeminiChatSafetyRating `json:"safetyRatings"`
+	BlockReason   *string                  `json:"blockReason,omitempty"`
 }
 
 type GeminiChatResponse struct {
-	Candidates     []GeminiChatCandidate    `json:"candidates"`
-	PromptFeedback GeminiChatPromptFeedback `json:"promptFeedback"`
-	UsageMetadata  GeminiUsageMetadata      `json:"usageMetadata"`
+	Candidates     []GeminiChatCandidate     `json:"candidates"`
+	PromptFeedback *GeminiChatPromptFeedback `json:"promptFeedback,omitempty"`
+	UsageMetadata  GeminiUsageMetadata       `json:"usageMetadata"`
 }
 
 type GeminiUsageMetadata struct {
@@ -325,6 +331,7 @@ type GeminiImageParameters struct {
 	SampleCount      int    `json:"sampleCount,omitempty"`
 	AspectRatio      string `json:"aspectRatio,omitempty"`
 	PersonGeneration string `json:"personGeneration,omitempty"`
+	ImageSize        string `json:"imageSize,omitempty"`
 }
 
 type GeminiImageResponse struct {

+ 10 - 8
dto/openai_image.go

@@ -2,11 +2,12 @@ package dto
 
 import (
 	"encoding/json"
-	"one-api/common"
-	"one-api/types"
 	"reflect"
 	"strings"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/types"
+
 	"github.com/gin-gonic/gin"
 )
 
@@ -74,14 +75,15 @@ func (r ImageRequest) MarshalJSON() ([]byte, error) {
 		return nil, err
 	}
 
+	// 不能合并ExtraFields!!!!!!!!
 	// 合并 ExtraFields
-	for k, v := range r.Extra {
-		if _, exists := baseMap[k]; !exists {
-			baseMap[k] = v
-		}
-	}
+	//for k, v := range r.Extra {
+	//	if _, exists := baseMap[k]; !exists {
+	//		baseMap[k] = v
+	//	}
+	//}
 
-	return json.Marshal(baseMap)
+	return common.Marshal(baseMap)
 }
 
 func GetJSONFieldNames(t reflect.Type) map[string]struct{} {

+ 35 - 15
dto/openai_request.go

@@ -3,10 +3,11 @@ package dto
 import (
 	"encoding/json"
 	"fmt"
-	"one-api/common"
-	"one-api/types"
 	"strings"
 
+	"github.com/QuantumNous/new-api/common"
+	"github.com/QuantumNous/new-api/types"
+
 	"github.com/gin-gonic/gin"
 )
 
@@ -57,6 +58,18 @@ type GeneralOpenAIRequest struct {
 	Dimensions          int               `json:"dimensions,omitempty"`
 	Modalities          json.RawMessage   `json:"modalities,omitempty"`
 	Audio               json.RawMessage   `json:"audio,omitempty"`
+	// 安全标识符,用于帮助 OpenAI 检测可能违反使用政策的应用程序用户
+	// 注意:此字段会向 OpenAI 发送用户标识信息,默认过滤以保护用户隐私
+	SafetyIdentifier string `json:"safety_identifier,omitempty"`
+	// Whether or not to store the output of this chat completion request for use in our model distillation or evals products.
+	// 是否存储此次请求数据供 OpenAI 用于评估和优化产品
+	// 注意:默认过滤此字段以保护用户隐私,但过滤后可能导致 Codex 无法正常使用
+	Store json.RawMessage `json:"store,omitempty"`
+	// Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the user field
+	PromptCacheKey string          `json:"prompt_cache_key,omitempty"`
+	LogitBias      json.RawMessage `json:"logit_bias,omitempty"`
+	Metadata       json.RawMessage `json:"metadata,omitempty"`
+	Prediction     json.RawMessage `json:"prediction,omitempty"`
 	// gemini
 	ExtraBody json.RawMessage `json:"extra_body,omitempty"`
 	//xai
@@ -75,6 +88,12 @@ type GeneralOpenAIRequest struct {
 	WebSearch json.RawMessage `json:"web_search,omitempty"`
 	// doubao,zhipu_v4
 	THINKING json.RawMessage `json:"thinking,omitempty"`
+	// pplx Params
+	SearchDomainFilter     json.RawMessage `json:"search_domain_filter,omitempty"`
+	SearchRecencyFilter    string          `json:"search_recency_filter,omitempty"`
+	ReturnImages           bool            `json:"return_images,omitempty"`
+	ReturnRelatedQuestions bool            `json:"return_related_questions,omitempty"`
+	SearchMode             string          `json:"search_mode,omitempty"`
 }
 
 func (r *GeneralOpenAIRequest) GetTokenCountMeta() *types.TokenCountMeta {
@@ -775,19 +794,20 @@ type OpenAIResponsesRequest struct {
 	ParallelToolCalls  json.RawMessage `json:"parallel_tool_calls,omitempty"`
 	PreviousResponseID string          `json:"previous_response_id,omitempty"`
 	Reasoning          *Reasoning      `json:"reasoning,omitempty"`
-	ServiceTier        string          `json:"service_tier,omitempty"`
-	Store              json.RawMessage `json:"store,omitempty"`
-	PromptCacheKey     json.RawMessage `json:"prompt_cache_key,omitempty"`
-	Stream             bool            `json:"stream,omitempty"`
-	Temperature        float64         `json:"temperature,omitempty"`
-	Text               json.RawMessage `json:"text,omitempty"`
-	ToolChoice         json.RawMessage `json:"tool_choice,omitempty"`
-	Tools              json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map
-	TopP               float64         `json:"top_p,omitempty"`
-	Truncation         string          `json:"truncation,omitempty"`
-	User               string          `json:"user,omitempty"`
-	MaxToolCalls       uint            `json:"max_tool_calls,omitempty"`
-	Prompt             json.RawMessage `json:"prompt,omitempty"`
+	// 服务层级字段,用于指定 API 服务等级。允许透传可能导致实际计费高于预期,默认应过滤
+	ServiceTier    string          `json:"service_tier,omitempty"`
+	Store          json.RawMessage `json:"store,omitempty"`
+	PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"`
+	Stream         bool            `json:"stream,omitempty"`
+	Temperature    float64         `json:"temperature,omitempty"`
+	Text           json.RawMessage `json:"text,omitempty"`
+	ToolChoice     json.RawMessage `json:"tool_choice,omitempty"`
+	Tools          json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map
+	TopP           float64         `json:"top_p,omitempty"`
+	Truncation     string          `json:"truncation,omitempty"`
+	User           string          `json:"user,omitempty"`
+	MaxToolCalls   uint            `json:"max_tool_calls,omitempty"`
+	Prompt         json.RawMessage `json:"prompt,omitempty"`
 }
 
 func (r *OpenAIResponsesRequest) GetTokenCountMeta() *types.TokenCountMeta {

+ 12 - 1
dto/openai_response.go

@@ -3,7 +3,8 @@ package dto
 import (
 	"encoding/json"
 	"fmt"
-	"one-api/types"
+
+	"github.com/QuantumNous/new-api/types"
 )
 
 const (
@@ -233,6 +234,16 @@ type Usage struct {
 	Cost any `json:"cost,omitempty"`
 }
 
+type OpenAIVideoResponse struct {
+	Id        string `json:"id" example:"file-abc123"`
+	Object    string `json:"object" example:"file"`
+	Bytes     int64  `json:"bytes" example:"120000"`
+	CreatedAt int64  `json:"created_at" example:"1677610602"`
+	ExpiresAt int64  `json:"expires_at" example:"1677614202"`
+	Filename  string `json:"filename" example:"mydata.jsonl"`
+	Purpose   string `json:"purpose" example:"fine-tune"`
+}
+
 type InputTokenDetails struct {
 	CachedTokens         int `json:"cached_tokens"`
 	CachedCreationTokens int `json:"-"`

+ 52 - 0
dto/openai_video.go

@@ -0,0 +1,52 @@
+package dto
+
+import (
+	"strconv"
+	"strings"
+)
+
+const (
+	VideoStatusUnknown    = "unknown"
+	VideoStatusQueued     = "queued"
+	VideoStatusInProgress = "in_progress"
+	VideoStatusCompleted  = "completed"
+	VideoStatusFailed     = "failed"
+)
+
+type OpenAIVideo struct {
+	ID                 string            `json:"id"`
+	TaskID             string            `json:"task_id,omitempty"` //兼容旧接口 待废弃
+	Object             string            `json:"object"`
+	Model              string            `json:"model"`
+	Status             string            `json:"status"` // Should use VideoStatus constants: VideoStatusQueued, VideoStatusInProgress, VideoStatusCompleted, VideoStatusFailed
+	Progress           int               `json:"progress"`
+	CreatedAt          int64             `json:"created_at"`
+	CompletedAt        int64             `json:"completed_at,omitempty"`
+	ExpiresAt          int64             `json:"expires_at,omitempty"`
+	Seconds            string            `json:"seconds,omitempty"`
+	Size               string            `json:"size,omitempty"`
+	RemixedFromVideoID string            `json:"remixed_from_video_id,omitempty"`
+	Error              *OpenAIVideoError `json:"error,omitempty"`
+	Metadata           map[string]any    `json:"meta_data,omitempty"`
+}
+
+func (m *OpenAIVideo) SetProgressStr(progress string) {
+	progress = strings.TrimSuffix(progress, "%")
+	m.Progress, _ = strconv.Atoi(progress)
+}
+func (m *OpenAIVideo) SetMetadata(k string, v any) {
+	if m.Metadata == nil {
+		m.Metadata = make(map[string]any)
+	}
+	m.Metadata[k] = v
+}
+func NewOpenAIVideo() *OpenAIVideo {
+	return &OpenAIVideo{
+		Object: "video",
+	}
+}
+
+type OpenAIVideoError struct {
+	Message string `json:"message"`
+	Code    string `json:"code"`
+}

+ 1 - 1
dto/pricing.go

@@ -1,6 +1,6 @@
 package dto
 
-import "one-api/constant"
+import "github.com/QuantumNous/new-api/constant"
 
 // 这里不好动就不动了,本来想独立出来的(
 type OpenAIModels struct {

+ 1 - 1
dto/realtime.go

@@ -1,6 +1,6 @@
 package dto
 
-import "one-api/types"
+import "github.com/QuantumNous/new-api/types"
 
 const (
 	RealtimeEventTypeError              = "error"

+ 1 - 1
dto/request_common.go

@@ -1,8 +1,8 @@
 package dto
 
 import (
+	"github.com/QuantumNous/new-api/types"
 	"github.com/gin-gonic/gin"
-	"one-api/types"
 )
 
 type Request interface {

+ 3 - 2
dto/rerank.go

@@ -2,9 +2,10 @@ package dto
 
 import (
 	"fmt"
-	"github.com/gin-gonic/gin"
-	"one-api/types"
 	"strings"
+
+	"github.com/QuantumNous/new-api/types"
+	"github.com/gin-gonic/gin"
 )
 
 type RerankRequest struct {

+ 4 - 0
dto/user_settings.go

@@ -7,6 +7,9 @@ type UserSetting struct {
 	WebhookSecret         string  `json:"webhook_secret,omitempty"`                 // WebhookSecret webhook密钥
 	NotificationEmail     string  `json:"notification_email,omitempty"`             // NotificationEmail 通知邮箱地址
 	BarkUrl               string  `json:"bark_url,omitempty"`                       // BarkUrl Bark推送URL
+	GotifyUrl             string  `json:"gotify_url,omitempty"`                     // GotifyUrl Gotify服务器地址
+	GotifyToken           string  `json:"gotify_token,omitempty"`                   // GotifyToken Gotify应用令牌
+	GotifyPriority        int     `json:"gotify_priority"`                          // GotifyPriority Gotify消息优先级
 	AcceptUnsetRatioModel bool    `json:"accept_unset_model_ratio_model,omitempty"` // AcceptUnsetRatioModel 是否接受未设置价格的模型
 	RecordIpLog           bool    `json:"record_ip_log,omitempty"`                  // 是否记录请求和错误日志IP
 	SidebarModules        string  `json:"sidebar_modules,omitempty"`                // SidebarModules 左侧边栏模块配置
@@ -16,4 +19,5 @@ var (
 	NotifyTypeEmail   = "email"   // Email 邮件
 	NotifyTypeWebhook = "webhook" // Webhook
 	NotifyTypeBark    = "bark"    // Bark 推送
+	NotifyTypeGotify  = "gotify"  // Gotify 推送
 )

+ 73 - 0
electron/README.md

@@ -0,0 +1,73 @@
+# New API Electron Desktop App
+
+This directory contains the Electron wrapper for New API, providing a native desktop application with system tray support for Windows, macOS, and Linux.
+
+## Prerequisites
+
+### 1. Go Binary (Required)
+The Electron app requires the compiled Go binary to function. You have two options:
+
+**Option A: Use existing binary (without Go installed)**
+```bash
+# If you have a pre-built binary (e.g., new-api-macos)
+cp ../new-api-macos ../new-api
+```
+
+**Option B: Build from source (requires Go)**
+TODO
+
+### 3. Electron Dependencies
+```bash
+cd electron
+npm install
+```
+
+## Development
+
+Run the app in development mode:
+```bash
+npm start
+```
+
+This will:
+- Start the Go backend on port 3000
+- Open an Electron window with DevTools enabled
+- Create a system tray icon (menu bar on macOS)
+- Store database in `../data/new-api.db`
+
+## Building for Production
+
+### Quick Build
+```bash
+# Ensure Go binary exists in parent directory
+ls ../new-api  # Should exist
+
+# Build for current platform
+npm run build
+
+# Platform-specific builds
+npm run build:mac    # Creates .dmg and .zip
+npm run build:win    # Creates .exe installer
+npm run build:linux  # Creates .AppImage and .deb
+```
+
+### Build Output
+- Built applications are in `electron/dist/`
+- macOS: `.dmg` (installer) and `.zip` (portable)
+- Windows: `.exe` (installer) and portable exe
+- Linux: `.AppImage` and `.deb`
+
+## Configuration
+
+### Port
+Default port is 3000. To change, edit `main.js`:
+```javascript
+const PORT = 3000; // Change to desired port
+```
+
+### Database Location
+- **Development**: `../data/new-api.db` (project directory)
+- **Production**:
+  - macOS: `~/Library/Application Support/New API/data/`
+  - Windows: `%APPDATA%/New API/data/`
+  - Linux: `~/.config/New API/data/`

+ 41 - 0
electron/build.sh

@@ -0,0 +1,41 @@
+#!/bin/bash
+
+set -e
+
+echo "Building New API Electron App..."
+
+echo "Step 1: Building frontend..."
+cd ../web
+DISABLE_ESLINT_PLUGIN='true' bun run build
+cd ../electron
+
+echo "Step 2: Building Go backend..."
+cd ..
+
+if [[ "$OSTYPE" == "darwin"* ]]; then
+    echo "Building for macOS..."
+    CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api
+    cd electron
+    npm install
+    npm run build:mac
+elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
+    echo "Building for Linux..."
+    CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api
+    cd electron
+    npm install
+    npm run build:linux
+elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
+    echo "Building for Windows..."
+    CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api.exe
+    cd electron
+    npm install
+    npm run build:win
+else
+    echo "Unknown OS, building for current platform..."
+    CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api
+    cd electron
+    npm install
+    npm run build
+fi
+
+echo "Build complete! Check electron/dist/ for output."

+ 60 - 0
electron/create-tray-icon.js

@@ -0,0 +1,60 @@
+// Create a simple tray icon for macOS
+// Run: node create-tray-icon.js
+
+const fs = require('fs');
+const { createCanvas } = require('canvas');
+
+function createTrayIcon() {
+  // For macOS, we'll use a Template image (black and white)
+  // Size should be 22x22 for Retina displays (@2x would be 44x44)
+  const canvas = createCanvas(22, 22);
+  const ctx = canvas.getContext('2d');
+
+  // Clear canvas
+  ctx.clearRect(0, 0, 22, 22);
+
+  // Draw a simple "API" icon
+  ctx.fillStyle = '#000000';
+  ctx.font = 'bold 10px system-ui';
+  ctx.textAlign = 'center';
+  ctx.textBaseline = 'middle';
+  ctx.fillText('API', 11, 11);
+
+  // Save as PNG
+  const buffer = canvas.toBuffer('image/png');
+  fs.writeFileSync('tray-icon.png', buffer);
+
+  // For Template images on macOS (will adapt to menu bar theme)
+  fs.writeFileSync('tray-iconTemplate.png', buffer);
+  fs.writeFileSync('[email protected]', buffer);
+
+  console.log('Tray icon created successfully!');
+}
+
+// Check if canvas is installed
+try {
+  createTrayIcon();
+} catch (err) {
+  console.log('Canvas module not installed.');
+  console.log('For now, creating a placeholder. Install canvas with: npm install canvas');
+
+  // Create a minimal 1x1 transparent PNG as placeholder
+  const minimalPNG = Buffer.from([
+    0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
+    0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
+    0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
+    0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xDB, 0x56,
+    0xCA, 0x00, 0x00, 0x00, 0x03, 0x50, 0x4C, 0x54,
+    0x45, 0x00, 0x00, 0x00, 0xA7, 0x7A, 0x3D, 0xDA,
+    0x00, 0x00, 0x00, 0x01, 0x74, 0x52, 0x4E, 0x53,
+    0x00, 0x40, 0xE6, 0xD8, 0x66, 0x00, 0x00, 0x00,
+    0x0A, 0x49, 0x44, 0x41, 0x54, 0x08, 0x1D, 0x62,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+    0x00, 0x01, 0x0A, 0x2D, 0xCB, 0x59, 0x00, 0x00,
+    0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42,
+    0x60, 0x82
+  ]);
+
+  fs.writeFileSync('tray-icon.png', minimalPNG);
+  console.log('Created placeholder tray icon.');
+}

+ 18 - 0
electron/entitlements.mac.plist

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
+    <true/>
+    <key>com.apple.security.cs.allow-jit</key>
+    <true/>
+    <key>com.apple.security.cs.disable-library-validation</key>
+    <true/>
+    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
+    <true/>
+    <key>com.apple.security.network.client</key>
+    <true/>
+    <key>com.apple.security.network.server</key>
+    <true/>
+</dict>
+</plist>

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov