diff --git a/.dockerignore b/.dockerignore index 0670cd7d1..781a7b550 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,4 +5,5 @@ .gitignore Makefile docs -.eslintcache \ No newline at end of file +.eslintcache +.gocache \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report_en.md b/.github/ISSUE_TEMPLATE/bug_report_en.md new file mode 100644 index 000000000..5c2506180 --- /dev/null +++ b/.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. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request_en.md b/.github/ISSUE_TEMPLATE/feature_request_en.md new file mode 100644 index 000000000..cdfc43f0d --- /dev/null +++ b/.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** + diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md index 4f6e41ac2..7403f6c00 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -13,7 +13,3 @@ ### PR 描述 **请在下方详细描述您的 PR,包括目的、实现细节等。** - -### **重要提示** - -**所有 PR 都必须提交到 `alpha` 分支。请确保您的 PR 目标分支是 `alpha`。** diff --git a/.github/workflows/docker-image-alpha.yml b/.github/workflows/docker-image-alpha.yml index c02bd4097..2a7d43ad5 100644 --- a/.github/workflows/docker-image-alpha.yml +++ b/.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 diff --git a/.github/workflows/docker-image-arm64.yml b/.github/workflows/docker-image-arm64.yml index 8e4656aa7..78517af0e 100644 --- a/.github/workflows/docker-image-arm64.yml +++ b/.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 }} \ No newline at end of file + 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 diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml new file mode 100644 index 000000000..20113e00f --- /dev/null +++ b/.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 }} \ No newline at end of file diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml deleted file mode 100644 index 3e3ddc53b..000000000 --- a/.github/workflows/linux-release.yml +++ /dev/null @@ -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 }} \ No newline at end of file diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml deleted file mode 100644 index 8eaf2d67a..000000000 --- a/.github/workflows/macos-release.yml +++ /dev/null @@ -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 }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..11fd6c7d1 --- /dev/null +++ b/.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 }} + + diff --git a/.github/workflows/sync-to-gitee.yml b/.github/workflows/sync-to-gitee.yml new file mode 100644 index 000000000..4f515a188 --- /dev/null +++ b/.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<> $GITHUB_OUTPUT + + # 下载 release 的所有附件 + gh release download "$TAG_NAME" --dir ./release_assets || echo "No assets to download" + + # 列出下载的文件 + ls -la ./release_assets/ || echo "No assets directory" + + - name: Create Gitee Release + id: create_release + uses: nICEnnnnnnnLee/action-gitee-release@v2.0.0 + with: + gitee_action: create_release + gitee_owner: ${{ env.GITEE_OWNER }} + gitee_repo: ${{ env.GITEE_REPO }} + gitee_token: ${{ secrets.GITEE_TOKEN }} + gitee_tag_name: ${{ github.event.inputs.tag_name }} + gitee_release_name: ${{ steps.release_info.outputs.release_name }} + gitee_release_body: ${{ steps.release_info.outputs.release_body }} + gitee_target_commitish: ${{ steps.release_info.outputs.target_commitish }} + + - name: Upload Assets to Gitee + if: hashFiles('release_assets/*') != '' + uses: nICEnnnnnnnLee/action-gitee-release@v2.0.0 + with: + gitee_action: upload_asset + gitee_owner: ${{ env.GITEE_OWNER }} + gitee_repo: ${{ env.GITEE_REPO }} + gitee_token: ${{ secrets.GITEE_TOKEN }} + gitee_release_id: ${{ steps.create_release.outputs.release-id }} + gitee_upload_retry_times: 3 + gitee_files: | + release_assets/* + + - name: Cleanup + if: always() + run: | + rm -rf release_assets/ + + - name: Summary + if: success() + run: | + echo "✅ Successfully synced release ${{ github.event.inputs.tag_name }} to Gitee!" + echo "🔗 Gitee Release URL: https://gitee.com/${{ env.GITEE_OWNER }}/${{ env.GITEE_REPO }}/releases/tag/${{ github.event.inputs.tag_name }}" + diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml deleted file mode 100644 index 30e864f34..000000000 --- a/.github/workflows/windows-release.yml +++ /dev/null @@ -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 }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1382829fd..0cf4ce09a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,11 @@ logs web/dist .env one-api +new-api .DS_Store tiktoken_cache -.eslintcache \ No newline at end of file +.eslintcache +.gocache + +electron/node_modules +electron/dist \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 08cc86f72..89f1bc75a 100644 --- a/Dockerfile +++ b/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"] diff --git a/README.en.md b/README.en.md index 69fd32f8b..54f4274a5 100644 --- a/README.en.md +++ b/README.en.md @@ -1,6 +1,10 @@

- 中文 | English + 中文 | English | Français | 日本語

+ +> [!NOTE] +> **MT (Machine Translation)**: This document is machine translated. For the most accurate information, please refer to the [Chinese version](./README.md). +
![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 diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 000000000..b308660cf --- /dev/null +++ b/README.fr.md @@ -0,0 +1,225 @@ +

+ 中文 | English | Français | 日本語 +

+ +> [!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). + +
+ +![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 + +Calcium-Ion%2Fnew-api | Trendshift + +

+ + licence + + + version + + + docker + + + docker + + + GoReportCard + +

+
+ +## 📝 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. + +

🤝 Partenaires de confiance

+

 

+

Sans ordre particulier

+

+ Cherry Studio + Université de Pékin + UCloud + Alibaba Cloud + IO.NET +

+

 

+ +## 📚 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) \ No newline at end of file diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 000000000..315518c91 --- /dev/null +++ b/README.ja.md @@ -0,0 +1,226 @@ +

+ 中文 | English | Français | 日本語 +

+ +> [!NOTE] +> **MT(機械翻訳)**: この文書は機械翻訳されています。最も正確な情報については、[中国語版](./README.md)を参照してください。 + +
+ +![new-api](/web/public/logo.png) + +# New API + +🍥次世代大規模モデルゲートウェイとAI資産管理システム + +Calcium-Ion%2Fnew-api | Trendshift + +

+ + license + + + release + + + docker + + + docker + + + GoReportCard + +

+
+ +## 📝 プロジェクト説明 + +> [!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 サービスを提供しないでください。 + +

🤝 信頼できるパートナー

+

 

+

順不同

+

+ Cherry Studio + 北京大学 + UCloud 優刻得 + Alibaba Cloud + IO.NET +

+

 

+ +## 📚 ドキュメント + +詳細なドキュメントは公式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) + diff --git a/README.md b/README.md index d68b3e135..0bc9980e9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- 中文 | English + 中文 | English | Français | 日本語

@@ -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的项目: diff --git a/common/api_type.go b/common/api_type.go index 5ac46c863..8dbf4a900 100644 --- a/common/api_type.go +++ b/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 diff --git a/common/constants.go b/common/constants.go index e6d59d101..2ef2b7df2 100644 --- a/common/constants.go +++ b/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 diff --git a/common/crypto.go b/common/crypto.go index c353188aa..3ca06bd2d 100644 --- a/common/crypto.go +++ b/common/crypto.go @@ -4,6 +4,7 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/hex" + "golang.org/x/crypto/bcrypt" ) diff --git a/common/database.go b/common/database.go index 38a54d5e6..71dbd94d5 100644 --- a/common/database.go +++ b/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" \ No newline at end of file +var SQLitePath = "one-api.db?_busy_timeout=30000" diff --git a/common/email.go b/common/email.go index 18e6dbf73..e27d8bcd1 100644 --- a/common/email.go +++ b/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 } diff --git a/common/embed-file-system.go b/common/embed-file-system.go index 3ea02cf81..d7bd2d5bd 100644 --- a/common/embed-file-system.go +++ b/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 diff --git a/common/endpoint_defaults.go b/common/endpoint_defaults.go index ffc263507..c04c5f6d8 100644 --- a/common/endpoint_defaults.go +++ b/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 返回指定端点类型的默认信息以及是否存在 diff --git a/common/endpoint_type.go b/common/endpoint_type.go index a0ca73ead..65e80ce2f 100644 --- a/common/endpoint_type.go +++ b/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} diff --git a/common/gin.go b/common/gin.go index 2cb358444..4bc9f1ba7 100644 --- a/common/gin.go +++ b/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 +} diff --git a/common/gopool.go b/common/gopool.go index bf5df3119..d410380b8 100644 --- a/common/gopool.go +++ b/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 diff --git a/common/init.go b/common/init.go index c4626f9ae..51b1f2138 100644 --- a/common/init.go +++ b/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 ] [--log-dir ] [--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 ] [--log-dir ] [--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 + } } diff --git a/common/json.go b/common/json.go index 13e23a460..a65da462e 100644 --- a/common/json.go +++ b/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) } diff --git a/common/limiter/limiter.go b/common/limiter/limiter.go index ef5d1935a..6be61bc95 100644 --- a/common/limiter/limiter.go +++ b/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 diff --git a/common/pprof.go b/common/pprof.go index 4bec30f15..745926536 100644 --- a/common/pprof.go +++ b/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文件 diff --git a/common/verification.go b/common/verification.go index d8ccd6eaf..41fd3c943 100644 --- a/common/verification.go +++ b/common/verification.go @@ -1,10 +1,11 @@ package common import ( - "github.com/google/uuid" "strings" "sync" "time" + + "github.com/google/uuid" ) type verificationValue struct { diff --git a/constant/api_type.go b/constant/api_type.go index f62d91d53..156ccc83c 100644 --- a/constant/api_type.go +++ b/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 ) diff --git a/constant/channel.go b/constant/channel.go index 2e1cc5b07..426477e13 100644 --- a/constant/channel.go +++ b/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" } diff --git a/constant/endpoint_type.go b/constant/endpoint_type.go index ef096b759..29c0d31e3 100644 --- a/constant/endpoint_type.go +++ b/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" diff --git a/constant/env.go b/constant/env.go index 8bc2f1316..09d4a2f36 100644 --- a/constant/env.go +++ b/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 diff --git a/controller/billing.go b/controller/billing.go index 1fb83633e..1c92a2507 100644 --- a/controller/billing.go +++ b/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", diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 1082b9e73..751ee3600 100644 --- a/controller/channel-billing.go +++ b/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 { diff --git a/controller/channel-test.go b/controller/channel-test.go index 9ea6eed75..9942369df 100644 --- a/controller/channel-test.go +++ b/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,61 +40,63 @@ 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 { - return testResult{ - localErr: errors.New("midjourney channel test is not supported"), - newAPIError: nil, - } + var unsupportedTestChannelTypes = []int{ + constant.ChannelTypeMidjourney, + constant.ChannelTypeMidjourneyPlus, + constant.ChannelTypeSunoAPI, + constant.ChannelTypeKling, + constant.ChannelTypeJimeng, + constant.ChannelTypeDoubaoVideo, + constant.ChannelTypeVidu, } - if channel.Type == constant.ChannelTypeMidjourneyPlus { + if lo.Contains(unsupportedTestChannelTypes, channel.Type) { + channelTypeName := constant.GetChannelTypeName(channel.Type) 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" // 修改请求路径 + testModel = strings.TrimSpace(testModel) + if testModel == "" { + if channel.TestModel != nil && *channel.TestModel != "" { + testModel = strings.TrimSpace(*channel.TestModel) + } else { + models := channel.GetModels() + if len(models) > 0 { + testModel = strings.TrimSpace(models[0]) + } + if testModel == "" { + testModel = "gpt-4o-mini" + } + } } - // VolcEngine 图像生成模型 - if channel.Type == constant.ChannelTypeVolcEngine && strings.Contains(testModel, "seedream") { - requestPath = "/v1/images/generations" + 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" + } } c.Request = &http.Request{ @@ -102,33 +106,6 @@ func testChannel(channel *model.Channel, testModel string) testResult { Header: make(http.Header), } - if testModel == "" { - if channel.TestModel != nil && *channel.TestModel != "" { - testModel = *channel.TestModel - } else { - if len(channel.GetModels()) > 0 { - testModel = channel.GetModels()[0] - } else { - 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 - } - - if channel.Type == constant.ChannelTypeVolcEngine && strings.Contains(testModel, "seedream") { - requestPath = "/v1/images/generations" - c.Request.URL.Path = requestPath - } - cache, err := model.GetUserCache(1) if err != nil { return testResult{ @@ -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") diff --git a/controller/channel.go b/controller/channel.go index 480d5b4f3..eba990955 100644 --- a/controller/channel.go +++ b/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{ diff --git a/controller/console_migrate.go b/controller/console_migrate.go index f0812c3d6..011ab09d4 100644 --- a/controller/console_migrate.go +++ b/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" ) diff --git a/controller/github.go b/controller/github.go index 881d6dc16..7097356db 100644 --- a/controller/github.go +++ b/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" ) diff --git a/controller/group.go b/controller/group.go index 2565b6eae..3267818e0 100644 --- a/controller/group.go +++ b/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" ) diff --git a/controller/linuxdo.go b/controller/linuxdo.go index 9fa156157..051563b5c 100644 --- a/controller/linuxdo.go +++ b/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" ) diff --git a/controller/log.go b/controller/log.go index 042fa7253..da9bca468 100644 --- a/controller/log.go +++ b/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" ) diff --git a/controller/midjourney.go b/controller/midjourney.go index 3a7304419..c480c12bb 100644 --- a/controller/midjourney.go +++ b/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" ) diff --git a/controller/misc.go b/controller/misc.go index 875142ffb..83b43fb57 100644 --- a/controller/misc.go +++ b/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() diff --git a/controller/missing_models.go b/controller/missing_models.go index 425f9b25f..eddd8699d 100644 --- a/controller/missing_models.go +++ b/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" ) diff --git a/controller/model.go b/controller/model.go index f0571b995..3ab890c4b 100644 --- a/controller/model.go +++ b/controller/model.go @@ -2,21 +2,22 @@ package controller import ( "fmt" + "net/http" + "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" - "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" ) // https://platform.openai.com/docs/api-reference/models/list diff --git a/controller/model_meta.go b/controller/model_meta.go index 31ea64f35..fd3626442 100644 --- a/controller/model_meta.go +++ b/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" ) diff --git a/controller/model_sync.go b/controller/model_sync.go index 74034b51a..e321ee0c5 100644 --- a/controller/model_sync.go +++ b/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" diff --git a/controller/oidc.go b/controller/oidc.go index 8e254d38f..ac49f84e1 100644 --- a/controller/oidc.go +++ b/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" ) diff --git a/controller/option.go b/controller/option.go index 7d1c676f5..56f65f5ff 100644 --- a/controller/option.go +++ b/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" ) diff --git a/controller/passkey.go b/controller/passkey.go new file mode 100644 index 000000000..a2cc53699 --- /dev/null +++ b/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 +} diff --git a/controller/playground.go b/controller/playground.go index 8a1cb2b67..f6e0953fc 100644 --- a/controller/playground.go +++ b/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" ) diff --git a/controller/prefill_group.go b/controller/prefill_group.go index d912d6098..3c990daa0 100644 --- a/controller/prefill_group.go +++ b/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" ) diff --git a/controller/pricing.go b/controller/pricing.go index 4b7cc86d5..e1edd1b66 100644 --- a/controller/pricing.go +++ b/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" ) diff --git a/controller/ratio_config.go b/controller/ratio_config.go index 0cb4aa73b..b9b9d479a 100644 --- a/controller/ratio_config.go +++ b/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" ) diff --git a/controller/ratio_sync.go b/controller/ratio_sync.go index 7a481c476..b8224b816 100644 --- a/controller/ratio_sync.go +++ b/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" ) diff --git a/controller/redemption.go b/controller/redemption.go index 1e305e3d8..945cefa35 100644 --- a/controller/redemption.go +++ b/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" ) diff --git a/controller/relay.go b/controller/relay.go index 23d725153..f52fb4815 100644 --- a/controller/relay.go +++ b/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 diff --git a/controller/secure_verification.go b/controller/secure_verification.go new file mode 100644 index 000000000..f30c259e6 --- /dev/null +++ b/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, + }, + }) +} diff --git a/controller/setup.go b/controller/setup.go index 3ae255e94..2f6a0c9be 100644 --- a/controller/setup.go +++ b/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" -} \ No newline at end of file +} diff --git a/controller/task.go b/controller/task.go index 1082d7a11..c14d7e21d 100644 --- a/controller/task.go +++ b/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" ) diff --git a/controller/task_video.go b/controller/task_video.go index 73d5c39b1..c39593507 100644 --- a/controller/task_video.go +++ b/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 diff --git a/controller/telegram.go b/controller/telegram.go index 8d07fc940..f16cdd66c 100644 --- a/controller/telegram.go +++ b/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) { diff --git a/controller/token.go b/controller/token.go index 8ed8b9570..04e31f8c1 100644 --- a/controller/token.go +++ b/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" ) diff --git a/controller/topup.go b/controller/topup.go index d70443524..f34d4b892 100644 --- a/controller/topup.go +++ b/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) +} diff --git a/controller/topup_stripe.go b/controller/topup_stripe.go index ccde91dbe..75dbe28b4 100644 --- a/controller/topup_stripe.go +++ b/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) diff --git a/controller/twofa.go b/controller/twofa.go index 1859a1284..556c07e9e 100644 --- a/controller/twofa.go +++ b/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" ) diff --git a/controller/uptime_kuma.go b/controller/uptime_kuma.go index 41b9695c3..2beceb426 100644 --- a/controller/uptime_kuma.go +++ b/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" ) diff --git a/controller/usedata.go b/controller/usedata.go index 4adee50f7..816988a2b 100644 --- a/controller/usedata.go +++ b/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" ) diff --git a/controller/user.go b/controller/user.go index 982329cec..4418548a8 100644 --- a/controller/user.go +++ b/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 { diff --git a/controller/vendor_meta.go b/controller/vendor_meta.go index 21d5a21db..243ed1862 100644 --- a/controller/vendor_meta.go +++ b/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" ) diff --git a/controller/video_proxy.go b/controller/video_proxy.go new file mode 100644 index 000000000..17e466ae8 --- /dev/null +++ b/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())) + } +} diff --git a/controller/wechat.go b/controller/wechat.go index 9a4bdfedf..07f2fb32e 100644 --- a/controller/wechat.go +++ b/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" ) diff --git a/docker-compose.yml b/docker-compose.yml index d98fd706e..a9d00967c 100644 --- a/docker-compose.yml +++ b/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: diff --git a/docs/translation-glossary.fr.md b/docs/translation-glossary.fr.md new file mode 100644 index 000000000..d73d0dad4 --- /dev/null +++ b/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. \ No newline at end of file diff --git a/docs/translation-glossary.md b/docs/translation-glossary.md new file mode 100644 index 000000000..c5f68ad15 --- /dev/null +++ b/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. diff --git a/docs/translation-glossary.ru.md b/docs/translation-glossary.ru.md new file mode 100644 index 000000000..60a9bd280 --- /dev/null +++ b/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. diff --git a/dto/audio.go b/dto/audio.go index 9d71f6f76..ea51516f8 100644 --- a/dto/audio.go +++ b/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 { diff --git a/dto/channel_settings.go b/dto/channel_settings.go index 8791f516e..e88f2235e 100644 --- a/dto/channel_settings.go +++ b/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 } diff --git a/dto/claude.go b/dto/claude.go index 963e588bf..c6e5c3ecd 100644 --- a/dto/claude.go +++ b/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 { diff --git a/dto/embedding.go b/dto/embedding.go index b473b7228..d0730f9f5 100644 --- a/dto/embedding.go +++ b/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" ) diff --git a/dto/error.go b/dto/error.go index d7f6824dc..79547671b 100644 --- a/dto/error.go +++ b/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"` diff --git a/dto/gemini.go b/dto/gemini.go index bc05c6aab..d9d153374 100644 --- a/dto/gemini.go +++ b/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 { diff --git a/dto/openai_image.go b/dto/openai_image.go index 5aece25f2..9750c8093 100644 --- a/dto/openai_image.go +++ b/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{} { diff --git a/dto/openai_request.go b/dto/openai_request.go index 191fa638f..0b8b5dc54 100644 --- a/dto/openai_request.go +++ b/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 { diff --git a/dto/openai_response.go b/dto/openai_response.go index 6353c15ff..0c345bacb 100644 --- a/dto/openai_response.go +++ b/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:"-"` diff --git a/dto/openai_video.go b/dto/openai_video.go new file mode 100644 index 000000000..051769b98 --- /dev/null +++ b/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"` +} diff --git a/dto/pricing.go b/dto/pricing.go index bc024de30..1ed8dcd31 100644 --- a/dto/pricing.go +++ b/dto/pricing.go @@ -1,6 +1,6 @@ package dto -import "one-api/constant" +import "github.com/QuantumNous/new-api/constant" // 这里不好动就不动了,本来想独立出来的( type OpenAIModels struct { diff --git a/dto/realtime.go b/dto/realtime.go index 32a690568..0fbfb86f0 100644 --- a/dto/realtime.go +++ b/dto/realtime.go @@ -1,6 +1,6 @@ package dto -import "one-api/types" +import "github.com/QuantumNous/new-api/types" const ( RealtimeEventTypeError = "error" diff --git a/dto/request_common.go b/dto/request_common.go index da3ac3c52..e6e40c3a1 100644 --- a/dto/request_common.go +++ b/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 { diff --git a/dto/rerank.go b/dto/rerank.go index 46f4bce6f..607d68a70 100644 --- a/dto/rerank.go +++ b/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 { diff --git a/dto/user_settings.go b/dto/user_settings.go index 89dd926ef..16ce7b985 100644 --- a/dto/user_settings.go +++ b/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 推送 ) diff --git a/electron/README.md b/electron/README.md new file mode 100644 index 000000000..88463b8ae --- /dev/null +++ b/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/` diff --git a/electron/build.sh b/electron/build.sh new file mode 100755 index 000000000..cef714328 --- /dev/null +++ b/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." \ No newline at end of file diff --git a/electron/create-tray-icon.js b/electron/create-tray-icon.js new file mode 100644 index 000000000..517393b2e --- /dev/null +++ b/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('tray-iconTemplate@2x.png', 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.'); +} \ No newline at end of file diff --git a/electron/entitlements.mac.plist b/electron/entitlements.mac.plist new file mode 100644 index 000000000..a00aebcd0 --- /dev/null +++ b/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-jit + + com.apple.security.cs.disable-library-validation + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + + \ No newline at end of file diff --git a/electron/icon.png b/electron/icon.png new file mode 100644 index 000000000..c63ac2d77 Binary files /dev/null and b/electron/icon.png differ diff --git a/electron/main.js b/electron/main.js new file mode 100644 index 000000000..210a45658 --- /dev/null +++ b/electron/main.js @@ -0,0 +1,590 @@ +const { app, BrowserWindow, dialog, Tray, Menu, shell } = require('electron'); +const { spawn } = require('child_process'); +const path = require('path'); +const http = require('http'); +const fs = require('fs'); + +let mainWindow; +let serverProcess; +let tray = null; +let serverErrorLogs = []; +const PORT = 3000; +const DEV_FRONTEND_PORT = 5173; // Vite dev server port + +// 保存日志到文件并打开 +function saveAndOpenErrorLog() { + try { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const logFileName = `new-api-crash-${timestamp}.log`; + const logDir = app.getPath('logs'); + const logFilePath = path.join(logDir, logFileName); + + // 确保日志目录存在 + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + + // 写入日志 + const logContent = `New API 崩溃日志 +生成时间: ${new Date().toLocaleString('zh-CN')} +平台: ${process.platform} +架构: ${process.arch} +应用版本: ${app.getVersion()} + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +完整错误日志: + +${serverErrorLogs.join('\n')} + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +日志文件位置: ${logFilePath} +`; + + fs.writeFileSync(logFilePath, logContent, 'utf8'); + + // 打开日志文件 + shell.openPath(logFilePath).then((error) => { + if (error) { + console.error('Failed to open log file:', error); + // 如果打开文件失败,至少显示文件位置 + shell.showItemInFolder(logFilePath); + } + }); + + return logFilePath; + } catch (err) { + console.error('Failed to save error log:', err); + return null; + } +} + +// 分析错误日志,识别常见错误并提供解决方案 +function analyzeError(errorLogs) { + const allLogs = errorLogs.join('\n'); + + // 检测端口占用错误 + if (allLogs.includes('failed to start HTTP server') || + allLogs.includes('bind: address already in use') || + allLogs.includes('listen tcp') && allLogs.includes('bind: address already in use')) { + return { + type: '端口被占用', + title: '端口 ' + PORT + ' 被占用', + message: '无法启动服务器,端口已被其他程序占用', + solution: `可能的解决方案:\n\n1. 关闭占用端口 ${PORT} 的其他程序\n2. 检查是否已经运行了另一个 New API 实例\n3. 使用以下命令查找占用端口的进程:\n Mac/Linux: lsof -i :${PORT}\n Windows: netstat -ano | findstr :${PORT}\n4. 重启电脑以释放端口` + }; + } + + // 检测数据库错误 + if (allLogs.includes('database is locked') || + allLogs.includes('unable to open database')) { + return { + type: '数据文件被占用', + title: '无法访问数据文件', + message: '应用的数据文件正被其他程序占用', + solution: '可能的解决方案:\n\n1. 检查是否已经打开了另一个 New API 窗口\n - 查看任务栏/Dock 中是否有其他 New API 图标\n - 查看系统托盘(Windows)或菜单栏(Mac)中是否有 New API 图标\n\n2. 如果刚刚关闭过应用,请等待 10 秒后再试\n\n3. 重启电脑以释放被占用的文件\n\n4. 如果问题持续,可以尝试:\n - 退出所有 New API 实例\n - 删除数据目录中的临时文件(.db-shm 和 .db-wal)\n - 重新启动应用' + }; + } + + // 检测权限错误 + if (allLogs.includes('permission denied') || + allLogs.includes('access denied')) { + return { + type: '权限错误', + title: '权限不足', + message: '程序没有足够的权限执行操作', + solution: '可能的解决方案:\n\n1. 以管理员/root权限运行程序\n2. 检查数据目录的读写权限\n3. 检查可执行文件的权限\n4. 在 Mac 上,检查安全性与隐私设置' + }; + } + + // 检测网络错误 + if (allLogs.includes('network is unreachable') || + allLogs.includes('no such host') || + allLogs.includes('connection refused')) { + return { + type: '网络错误', + title: '网络连接失败', + message: '无法建立网络连接', + solution: '可能的解决方案:\n\n1. 检查网络连接是否正常\n2. 检查防火墙设置\n3. 检查代理配置\n4. 确认目标服务器地址正确' + }; + } + + // 检测配置文件错误 + if (allLogs.includes('invalid configuration') || + allLogs.includes('failed to parse config') || + allLogs.includes('yaml') || allLogs.includes('json') && allLogs.includes('parse')) { + return { + type: '配置错误', + title: '配置文件错误', + message: '配置文件格式不正确或包含无效配置', + solution: '可能的解决方案:\n\n1. 检查配置文件格式是否正确\n2. 恢复默认配置\n3. 删除配置文件让程序重新生成\n4. 查看文档了解正确的配置格式' + }; + } + + // 检测内存不足 + if (allLogs.includes('out of memory') || + allLogs.includes('cannot allocate memory')) { + return { + type: '内存不足', + title: '系统内存不足', + message: '程序运行时内存不足', + solution: '可能的解决方案:\n\n1. 关闭其他占用内存的程序\n2. 增加系统可用内存\n3. 重启电脑释放内存\n4. 检查是否存在内存泄漏' + }; + } + + // 检测文件不存在错误 + if (allLogs.includes('no such file or directory') || + allLogs.includes('cannot find the file')) { + return { + type: '文件缺失', + title: '找不到必需的文件', + message: '缺少程序运行所需的文件', + solution: '可能的解决方案:\n\n1. 重新安装应用程序\n2. 检查安装目录是否完整\n3. 确保所有依赖文件都存在\n4. 检查文件路径是否正确' + }; + } + + return null; +} + +function getBinaryPath() { + const isDev = process.env.NODE_ENV === 'development'; + const platform = process.platform; + + if (isDev) { + const binaryName = platform === 'win32' ? 'new-api.exe' : 'new-api'; + return path.join(__dirname, '..', binaryName); + } + + let binaryName; + switch (platform) { + case 'win32': + binaryName = 'new-api.exe'; + break; + case 'darwin': + binaryName = 'new-api'; + break; + case 'linux': + binaryName = 'new-api'; + break; + default: + binaryName = 'new-api'; + } + + return path.join(process.resourcesPath, 'bin', binaryName); +} + +// Check if a server is available with retry logic +function checkServerAvailability(port, maxRetries = 30, retryDelay = 1000) { + return new Promise((resolve, reject) => { + let currentAttempt = 0; + + const tryConnect = () => { + currentAttempt++; + + if (currentAttempt % 5 === 1 && currentAttempt > 1) { + console.log(`Attempting to connect to port ${port}... (attempt ${currentAttempt}/${maxRetries})`); + } + + const req = http.get({ + hostname: '127.0.0.1', // Use IPv4 explicitly instead of 'localhost' to avoid IPv6 issues + port: port, + timeout: 10000 + }, (res) => { + // Server responded, connection successful + req.destroy(); + console.log(`✓ Successfully connected to port ${port} (status: ${res.statusCode})`); + resolve(); + }); + + req.on('error', (err) => { + if (currentAttempt >= maxRetries) { + reject(new Error(`Failed to connect to port ${port} after ${maxRetries} attempts: ${err.message}`)); + } else { + setTimeout(tryConnect, retryDelay); + } + }); + + req.on('timeout', () => { + req.destroy(); + if (currentAttempt >= maxRetries) { + reject(new Error(`Connection timeout on port ${port} after ${maxRetries} attempts`)); + } else { + setTimeout(tryConnect, retryDelay); + } + }); + }; + + tryConnect(); + }); +} + +function startServer() { + return new Promise((resolve, reject) => { + const isDev = process.env.NODE_ENV === 'development'; + + const userDataPath = app.getPath('userData'); + const dataDir = path.join(userDataPath, 'data'); + + // 设置环境变量供 preload.js 使用 + process.env.ELECTRON_DATA_DIR = dataDir; + + if (isDev) { + // 开发模式:假设开发者手动启动了 Go 后端和前端开发服务器 + // 只需要等待前端开发服务器就绪 + console.log('Development mode: skipping server startup'); + console.log('Please make sure you have started:'); + console.log(' 1. Go backend: go run main.go (port 3000)'); + console.log(' 2. Frontend dev server: cd web && bun dev (port 5173)'); + console.log(''); + console.log('Checking if servers are running...'); + + // First check if both servers are accessible + checkServerAvailability(DEV_FRONTEND_PORT) + .then(() => { + console.log('✓ Frontend dev server is accessible on port 5173'); + resolve(); + }) + .catch((err) => { + console.error(`✗ Cannot connect to frontend dev server on port ${DEV_FRONTEND_PORT}`); + console.error('Please make sure the frontend dev server is running:'); + console.error(' cd web && bun dev'); + reject(err); + }); + return; + } + + // 生产模式:启动二进制服务器 + const env = { ...process.env, PORT: PORT.toString() }; + + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } + + env.SQLITE_PATH = path.join(dataDir, 'new-api.db'); + + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('📁 您的数据存储位置:'); + console.log(' ' + dataDir); + console.log(' 💡 备份提示:复制此目录即可备份所有数据'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + const binaryPath = getBinaryPath(); + const workingDir = process.resourcesPath; + + console.log('Starting server from:', binaryPath); + + serverProcess = spawn(binaryPath, [], { + env, + cwd: workingDir + }); + + serverProcess.stdout.on('data', (data) => { + console.log(`Server: ${data}`); + }); + + serverProcess.stderr.on('data', (data) => { + const errorMsg = data.toString(); + console.error(`Server Error: ${errorMsg}`); + serverErrorLogs.push(errorMsg); + // 只保留最近的100条错误日志 + if (serverErrorLogs.length > 100) { + serverErrorLogs.shift(); + } + }); + + serverProcess.on('error', (err) => { + console.error('Failed to start server:', err); + reject(err); + }); + + serverProcess.on('close', (code) => { + console.log(`Server process exited with code ${code}`); + + // 如果退出代码不是0,说明服务器异常退出 + if (code !== 0 && code !== null) { + const errorDetails = serverErrorLogs.length > 0 + ? serverErrorLogs.slice(-20).join('\n') + : '没有捕获到错误日志'; + + // 分析错误类型 + const knownError = analyzeError(serverErrorLogs); + + let dialogOptions; + if (knownError) { + // 识别到已知错误,显示友好的错误信息和解决方案 + dialogOptions = { + type: 'error', + title: knownError.title, + message: knownError.message, + detail: `${knownError.solution}\n\n━━━━━━━━━━━━━━━━━━━━━━\n\n退出代码: ${code}\n\n错误类型: ${knownError.type}\n\n最近的错误日志:\n${errorDetails}`, + buttons: ['退出应用', '查看完整日志'], + defaultId: 0, + cancelId: 0 + }; + } else { + // 未识别的错误,显示通用错误信息 + dialogOptions = { + type: 'error', + title: '服务器崩溃', + message: '服务器进程异常退出', + detail: `退出代码: ${code}\n\n最近的错误信息:\n${errorDetails}`, + buttons: ['退出应用', '查看完整日志'], + defaultId: 0, + cancelId: 0 + }; + } + + dialog.showMessageBox(dialogOptions).then((result) => { + if (result.response === 1) { + // 用户选择查看详情,保存并打开日志文件 + const logPath = saveAndOpenErrorLog(); + + // 显示确认对话框 + const confirmMessage = logPath + ? `日志已保存到:\n${logPath}\n\n日志文件已在默认文本编辑器中打开。\n\n点击"退出"关闭应用程序。` + : '日志保存失败,但已在控制台输出。\n\n点击"退出"关闭应用程序。'; + + dialog.showMessageBox({ + type: 'info', + title: '日志已保存', + message: confirmMessage, + buttons: ['退出'], + defaultId: 0 + }).then(() => { + app.isQuitting = true; + app.quit(); + }); + + // 同时在控制台输出 + console.log('=== 完整错误日志 ==='); + console.log(serverErrorLogs.join('\n')); + } else { + // 用户选择直接退出 + app.isQuitting = true; + app.quit(); + } + }); + } else { + // 正常退出(code为0或null),直接关闭窗口 + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.close(); + } + } + }); + + checkServerAvailability(PORT) + .then(() => { + console.log('✓ Backend server is accessible on port 3000'); + resolve(); + }) + .catch((err) => { + console.error('✗ Failed to connect to backend server'); + reject(err); + }); + }); +} + +function createWindow() { + const isDev = process.env.NODE_ENV === 'development'; + const loadPort = isDev ? DEV_FRONTEND_PORT : PORT; + + mainWindow = new BrowserWindow({ + width: 1080, + height: 720, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + nodeIntegration: false, + contextIsolation: true + }, + title: 'New API', + icon: path.join(__dirname, 'icon.png') + }); + + mainWindow.loadURL(`http://127.0.0.1:${loadPort}`); + + console.log(`Loading from: http://127.0.0.1:${loadPort}`); + + if (isDev) { + mainWindow.webContents.openDevTools(); + } + + // Close to tray instead of quitting + mainWindow.on('close', (event) => { + if (!app.isQuitting) { + event.preventDefault(); + mainWindow.hide(); + if (process.platform === 'darwin') { + app.dock.hide(); + } + } + }); + + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + +function createTray() { + // Use template icon for macOS (black with transparency, auto-adapts to theme) + // Use colored icon for Windows + const trayIconPath = process.platform === 'darwin' + ? path.join(__dirname, 'tray-iconTemplate.png') + : path.join(__dirname, 'tray-icon-windows.png'); + + tray = new Tray(trayIconPath); + + const contextMenu = Menu.buildFromTemplate([ + { + label: 'Show New API', + click: () => { + if (mainWindow === null) { + createWindow(); + } else { + mainWindow.show(); + if (process.platform === 'darwin') { + app.dock.show(); + } + } + } + }, + { type: 'separator' }, + { + label: 'Quit', + click: () => { + app.isQuitting = true; + app.quit(); + } + } + ]); + + tray.setToolTip('New API'); + tray.setContextMenu(contextMenu); + + // On macOS, clicking the tray icon shows the window + tray.on('click', () => { + if (mainWindow === null) { + createWindow(); + } else { + mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show(); + if (mainWindow.isVisible() && process.platform === 'darwin') { + app.dock.show(); + } + } + }); +} + +app.whenReady().then(async () => { + try { + await startServer(); + createTray(); + createWindow(); + } catch (err) { + console.error('Failed to start application:', err); + + // 分析启动失败的错误 + const knownError = analyzeError(serverErrorLogs); + + if (knownError) { + dialog.showMessageBox({ + type: 'error', + title: knownError.title, + message: `启动失败: ${knownError.message}`, + detail: `${knownError.solution}\n\n━━━━━━━━━━━━━━━━━━━━━━\n\n错误信息: ${err.message}\n\n错误类型: ${knownError.type}`, + buttons: ['退出', '查看完整日志'], + defaultId: 0, + cancelId: 0 + }).then((result) => { + if (result.response === 1) { + // 用户选择查看日志 + const logPath = saveAndOpenErrorLog(); + + const confirmMessage = logPath + ? `日志已保存到:\n${logPath}\n\n日志文件已在默认文本编辑器中打开。\n\n点击"退出"关闭应用程序。` + : '日志保存失败,但已在控制台输出。\n\n点击"退出"关闭应用程序。'; + + dialog.showMessageBox({ + type: 'info', + title: '日志已保存', + message: confirmMessage, + buttons: ['退出'], + defaultId: 0 + }).then(() => { + app.quit(); + }); + + console.log('=== 完整错误日志 ==='); + console.log(serverErrorLogs.join('\n')); + } else { + app.quit(); + } + }); + } else { + dialog.showMessageBox({ + type: 'error', + title: '启动失败', + message: '无法启动服务器', + detail: `错误信息: ${err.message}\n\n请检查日志获取更多信息。`, + buttons: ['退出', '查看完整日志'], + defaultId: 0, + cancelId: 0 + }).then((result) => { + if (result.response === 1) { + // 用户选择查看日志 + const logPath = saveAndOpenErrorLog(); + + const confirmMessage = logPath + ? `日志已保存到:\n${logPath}\n\n日志文件已在默认文本编辑器中打开。\n\n点击"退出"关闭应用程序。` + : '日志保存失败,但已在控制台输出。\n\n点击"退出"关闭应用程序。'; + + dialog.showMessageBox({ + type: 'info', + title: '日志已保存', + message: confirmMessage, + buttons: ['退出'], + defaultId: 0 + }).then(() => { + app.quit(); + }); + + console.log('=== 完整错误日志 ==='); + console.log(serverErrorLogs.join('\n')); + } else { + app.quit(); + } + }); + } + } +}); + +app.on('window-all-closed', () => { + // Don't quit when window is closed, keep running in tray + // Only quit when explicitly choosing Quit from tray menu +}); + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } +}); + +app.on('before-quit', (event) => { + if (serverProcess) { + event.preventDefault(); + + console.log('Shutting down server...'); + serverProcess.kill('SIGTERM'); + + setTimeout(() => { + if (serverProcess) { + serverProcess.kill('SIGKILL'); + } + app.exit(); + }, 5000); + + serverProcess.on('close', () => { + serverProcess = null; + app.exit(); + }); + } +}); \ No newline at end of file diff --git a/electron/package-lock.json b/electron/package-lock.json new file mode 100644 index 000000000..c96d425a0 --- /dev/null +++ b/electron/package-lock.json @@ -0,0 +1,4117 @@ +{ + "name": "new-api-electron", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "new-api-electron", + "version": "1.0.0", + "devDependencies": { + "cross-env": "^7.0.3", + "electron": "35.7.5", + "electron-builder": "^24.9.1" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.2.1.tgz", + "integrity": "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz", + "integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", + "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.1", + "@malept/cross-spawn-promise": "^1.1.0", + "debug": "^4.3.1", + "dir-compare": "^3.0.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.18.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.8.tgz", + "integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.13.3.tgz", + "integrity": "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/notarize": "2.2.1", + "@electron/osx-sign": "1.0.5", + "@electron/universal": "1.5.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chromium-pickle-js": "^0.2.0", + "debug": "^4.3.4", + "ejs": "^3.1.8", + "electron-publish": "24.13.1", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "minimatch": "^5.1.1", + "read-config-file": "6.3.2", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "24.13.3", + "electron-builder-squirrel-windows": "24.13.3" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.13.1.tgz", + "integrity": "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "4.0.0", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-file-ts": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz", + "integrity": "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.10", + "typescript": "^5.3.3" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", + "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "^1.0.0", + "minimatch": "^3.0.4" + } + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz", + "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "35.7.5", + "resolved": "https://registry.npmjs.org/electron/-/electron-35.7.5.tgz", + "integrity": "sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.13.3.tgz", + "integrity": "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "dmg-builder": "24.13.3", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "read-config-file": "6.3.2", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz", + "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "24.13.3", + "archiver": "^5.3.1", + "builder-util": "24.13.1", + "fs-extra": "^10.1.0" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz", + "integrity": "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/isbinaryfile": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.6.tgz", + "integrity": "sha512-I+NmIfBHUl+r2wcDd6JwE9yWje/PIVY/R5/CmV8dXLZd5K+L9X2klAOwfAHNnondLXkbHyTAleQAWonpTJBTtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-config-file": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", + "integrity": "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-file-ts": "^0.2.4", + "dotenv": "^9.0.2", + "dotenv-expand": "^5.1.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.0", + "lazy-val": "^1.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/electron/package.json b/electron/package.json new file mode 100644 index 000000000..f861c91cb --- /dev/null +++ b/electron/package.json @@ -0,0 +1,101 @@ +{ + "name": "new-api-electron", + "version": "1.0.0", + "description": "New API - AI Model Gateway Desktop Application", + "main": "main.js", + "scripts": { + "start-app": "electron .", + "dev-app": "cross-env NODE_ENV=development electron .", + "build": "electron-builder", + "build:mac": "electron-builder --mac", + "build:win": "electron-builder --win", + "build:linux": "electron-builder --linux" + }, + "keywords": [ + "ai", + "api", + "gateway", + "openai", + "claude" + ], + "author": "QuantumNous", + "repository": { + "type": "git", + "url": "https://github.com/QuantumNous/new-api" + }, + "devDependencies": { + "cross-env": "^7.0.3", + "electron": "35.7.5", + "electron-builder": "^24.9.1" + }, + "build": { + "appId": "com.newapi.desktop", + "productName": "New-API-App", + "publish": null, + "directories": { + "output": "dist" + }, + "files": [ + "main.js", + "preload.js", + "icon.png", + "tray-iconTemplate.png", + "tray-iconTemplate@2x.png", + "tray-icon-windows.png" + ], + "mac": { + "category": "public.app-category.developer-tools", + "icon": "icon.png", + "identity": null, + "hardenedRuntime": false, + "gatekeeperAssess": false, + "entitlements": "entitlements.mac.plist", + "entitlementsInherit": "entitlements.mac.plist", + "target": [ + "dmg", + "zip" + ], + "extraResources": [ + { + "from": "../new-api", + "to": "bin/new-api" + }, + { + "from": "../web/dist", + "to": "web/dist" + } + ] + }, + "win": { + "icon": "icon.png", + "target": [ + "nsis", + "portable" + ], + "extraResources": [ + { + "from": "../new-api.exe", + "to": "bin/new-api.exe" + } + ] + }, + "linux": { + "icon": "icon.png", + "target": [ + "AppImage", + "deb" + ], + "category": "Development", + "extraResources": [ + { + "from": "../new-api", + "to": "bin/new-api" + } + ] + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true + } + } +} \ No newline at end of file diff --git a/electron/preload.js b/electron/preload.js new file mode 100644 index 000000000..ac971fd0a --- /dev/null +++ b/electron/preload.js @@ -0,0 +1,18 @@ +const { contextBridge } = require('electron'); + +// 获取数据目录路径(用于显示给用户) +// 优先使用主进程设置的真实路径,如果没有则回退到手动拼接 +function getDataDirPath() { + // 如果主进程已设置真实路径,直接使用 + if (process.env.ELECTRON_DATA_DIR) { + return process.env.ELECTRON_DATA_DIR; + } +} + +contextBridge.exposeInMainWorld('electron', { + isElectron: true, + version: process.versions.electron, + platform: process.platform, + versions: process.versions, + dataDir: getDataDirPath() +}); \ No newline at end of file diff --git a/electron/tray-icon-windows.png b/electron/tray-icon-windows.png new file mode 100644 index 000000000..57df8ead0 Binary files /dev/null and b/electron/tray-icon-windows.png differ diff --git a/electron/tray-iconTemplate.png b/electron/tray-iconTemplate.png new file mode 100644 index 000000000..187741e10 Binary files /dev/null and b/electron/tray-iconTemplate.png differ diff --git a/electron/tray-iconTemplate@2x.png b/electron/tray-iconTemplate@2x.png new file mode 100644 index 000000000..d5666a04e Binary files /dev/null and b/electron/tray-iconTemplate@2x.png differ diff --git a/go.mod b/go.mod index 501d966d5..b15bbadb2 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ -module one-api +module github.com/QuantumNous/new-api // +heroku goVersion go1.18 -go 1.23.4 +go 1.25.1 require ( github.com/Calcium-Ion/go-epay v0.0.4 @@ -11,7 +11,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.17.11 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.33.0 github.com/aws/smithy-go v1.22.5 - github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b + github.com/bytedance/gopkg v0.1.3 github.com/gin-contrib/cors v1.7.2 github.com/gin-contrib/gzip v0.0.6 github.com/gin-contrib/sessions v0.0.5 @@ -20,7 +20,8 @@ require ( github.com/glebarez/sqlite v1.9.0 github.com/go-playground/validator/v10 v10.20.0 github.com/go-redis/redis/v8 v8.11.5 - github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/go-webauthn/webauthn v0.14.0 + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.0 github.com/jinzhu/copier v0.4.0 @@ -35,10 +36,10 @@ require ( github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 github.com/tiktoken-go/tokenizer v0.6.2 - golang.org/x/crypto v0.35.0 + golang.org/x/crypto v0.42.0 golang.org/x/image v0.23.0 - golang.org/x/net v0.35.0 - golang.org/x/sync v0.11.0 + golang.org/x/net v0.43.0 + golang.org/x/sync v0.17.0 gorm.io/driver/mysql v1.4.3 gorm.io/driver/postgres v1.5.2 gorm.io/gorm v1.25.2 @@ -50,14 +51,14 @@ require ( github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.2 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.2 // indirect github.com/boombuler/barcode v1.1.0 // indirect - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/bytedance/sonic v1.14.1 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect @@ -65,8 +66,10 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/go-webauthn/x v0.1.25 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-tpm v0.9.5 // indirect github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/sessions v1.2.1 // indirect @@ -77,7 +80,7 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -91,11 +94,12 @@ require ( github.com/tklauser/numcpus v0.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect - golang.org/x/arch v0.12.0 // indirect + golang.org/x/arch v0.21.0 // indirect golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.22.5 // indirect diff --git a/go.sum b/go.sum index 189d09de4..bd6bae025 100644 --- a/go.sum +++ b/go.sum @@ -23,18 +23,16 @@ github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b h1:LTGVFpNmNHhj0vhOlfgWueFJ32eK9blaIlHR2ciXOT0= -github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= +github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -47,6 +45,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= @@ -89,16 +89,22 @@ github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx2QnhL0= +github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k= +github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88= +github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= +github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= @@ -132,10 +138,8 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -200,8 +204,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stripe/stripe-go/v81 v81.4.0 h1:AuD9XzdAvl193qUCSaLocf8H+nRopOouXhxqJUzCLbw= github.com/stripe/stripe-go/v81 v81.4.0/go.mod h1:C/F4jlmnGNacvYtBp/LUHCvVUJEZffFQCobkzwY1WOo= github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o= @@ -229,27 +234,29 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= -golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw= +golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -257,18 +264,17 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= @@ -305,5 +311,3 @@ modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/logger/logger.go b/logger/logger.go index d59e51cb8..67b8e0e66 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -6,12 +6,14 @@ import ( "fmt" "io" "log" - "one-api/common" "os" "path/filepath" "sync" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/setting/operation_setting" + "github.com/bytedance/gopkg/util/gopool" "github.com/gin-gonic/gin" ) @@ -92,18 +94,55 @@ func logHelper(ctx context.Context, level string, msg string) { } func LogQuota(quota int) string { - if common.DisplayInCurrencyEnabled { - return fmt.Sprintf("$%.6f 额度", float64(quota)/common.QuotaPerUnit) - } else { + // 新逻辑:根据额度展示类型输出 + q := float64(quota) + switch operation_setting.GetQuotaDisplayType() { + case operation_setting.QuotaDisplayTypeCNY: + usd := q / common.QuotaPerUnit + cny := usd * operation_setting.USDExchangeRate + return fmt.Sprintf("¥%.6f 额度", cny) + case operation_setting.QuotaDisplayTypeCustom: + usd := q / common.QuotaPerUnit + rate := operation_setting.GetGeneralSetting().CustomCurrencyExchangeRate + symbol := operation_setting.GetGeneralSetting().CustomCurrencySymbol + if symbol == "" { + symbol = "¤" + } + if rate <= 0 { + rate = 1 + } + v := usd * rate + return fmt.Sprintf("%s%.6f 额度", symbol, v) + case operation_setting.QuotaDisplayTypeTokens: return fmt.Sprintf("%d 点额度", quota) + default: // USD + return fmt.Sprintf("$%.6f 额度", q/common.QuotaPerUnit) } } func FormatQuota(quota int) string { - if common.DisplayInCurrencyEnabled { - return fmt.Sprintf("$%.6f", float64(quota)/common.QuotaPerUnit) - } else { + q := float64(quota) + switch operation_setting.GetQuotaDisplayType() { + case operation_setting.QuotaDisplayTypeCNY: + usd := q / common.QuotaPerUnit + cny := usd * operation_setting.USDExchangeRate + return fmt.Sprintf("¥%.6f", cny) + case operation_setting.QuotaDisplayTypeCustom: + usd := q / common.QuotaPerUnit + rate := operation_setting.GetGeneralSetting().CustomCurrencyExchangeRate + symbol := operation_setting.GetGeneralSetting().CustomCurrencySymbol + if symbol == "" { + symbol = "¤" + } + if rate <= 0 { + rate = 1 + } + v := usd * rate + return fmt.Sprintf("%s%.6f", symbol, v) + case operation_setting.QuotaDisplayTypeTokens: return fmt.Sprintf("%d", quota) + default: + return fmt.Sprintf("$%.6f", q/common.QuotaPerUnit) } } @@ -114,5 +153,5 @@ func LogJson(ctx context.Context, msg string, obj any) { LogError(ctx, fmt.Sprintf("json marshal failed: %s", err.Error())) return } - LogInfo(ctx, fmt.Sprintf("%s | %s", msg, string(jsonStr))) + LogDebug(ctx, fmt.Sprintf("%s | %s", msg, string(jsonStr))) } diff --git a/main.go b/main.go index b1421f9ef..481d0a600 100644 --- a/main.go +++ b/main.go @@ -1,23 +1,26 @@ package main import ( + "bytes" "embed" "fmt" "log" "net/http" - "one-api/common" - "one-api/constant" - "one-api/controller" - "one-api/logger" - "one-api/middleware" - "one-api/model" - "one-api/router" - "one-api/service" - "one-api/setting/ratio_setting" "os" "strconv" + "strings" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/controller" + "github.com/QuantumNous/new-api/logger" + "github.com/QuantumNous/new-api/middleware" + "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/router" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/setting/ratio_setting" + "github.com/bytedance/gopkg/util/gopool" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" @@ -147,6 +150,10 @@ func main() { }) server.Use(sessions.Sessions("session", store)) + InjectUmamiAnalytics() + InjectGoogleAnalytics() + + // 设置路由 router.SetRouter(server, buildFS, indexPage) var port = os.Getenv("PORT") if port == "" { @@ -162,13 +169,53 @@ func main() { } } +func InjectUmamiAnalytics() { + analyticsInjectBuilder := &strings.Builder{} + if os.Getenv("UMAMI_WEBSITE_ID") != "" { + umamiSiteID := os.Getenv("UMAMI_WEBSITE_ID") + umamiScriptURL := os.Getenv("UMAMI_SCRIPT_URL") + if umamiScriptURL == "" { + umamiScriptURL = "https://analytics.umami.is/script.js" + } + analyticsInjectBuilder.WriteString("") + } + analyticsInject := analyticsInjectBuilder.String() + indexPage = bytes.ReplaceAll(indexPage, []byte("\n"), []byte(analyticsInject)) +} + +func InjectGoogleAnalytics() { + analyticsInjectBuilder := &strings.Builder{} + if os.Getenv("GOOGLE_ANALYTICS_ID") != "" { + gaID := os.Getenv("GOOGLE_ANALYTICS_ID") + // Google Analytics 4 (gtag.js) + analyticsInjectBuilder.WriteString("") + analyticsInjectBuilder.WriteString("") + } + analyticsInject := analyticsInjectBuilder.String() + indexPage = bytes.ReplaceAll(indexPage, []byte("\n"), []byte(analyticsInject)) +} + func InitResources() error { // Initialize resources here if needed // This is a placeholder function for future resource initialization err := godotenv.Load(".env") if err != nil { - common.SysLog("未找到 .env 文件,使用默认环境变量,如果需要,请创建 .env 文件并设置相关变量") - common.SysLog("No .env file found, using default environment variables. If needed, please create a .env file and set the relevant variables.") + if common.DebugEnabled { + common.SysLog("No .env file found, using default environment variables. If needed, please create a .env file and set the relevant variables.") + } } // 加载环境变量 diff --git a/middleware/auth.go b/middleware/auth.go index 25caf50d9..b1a932a40 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -3,14 +3,15 @@ package middleware import ( "fmt" "net/http" - "one-api/common" - "one-api/constant" - "one-api/model" - "one-api/setting" - "one-api/setting/ratio_setting" "strconv" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/setting" + "github.com/QuantumNous/new-api/setting/ratio_setting" + "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" ) diff --git a/middleware/distributor.go b/middleware/distributor.go index 7fefeda49..2ff79e6ca 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -4,19 +4,20 @@ import ( "errors" "fmt" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/model" - relayconstant "one-api/relay/constant" - "one-api/service" - "one-api/setting" - "one-api/setting/ratio_setting" - "one-api/types" "strconv" "strings" "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" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/setting" + "github.com/QuantumNous/new-api/setting/ratio_setting" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) @@ -101,7 +102,7 @@ func Distribute() func(c *gin.Context) { if userGroup == "auto" { showGroup = fmt.Sprintf("auto(%s)", selectGroup) } - message := fmt.Sprintf("获取分组 %s 下模型 %s 的可用渠道失败(数据库一致性已被破坏,distributor): %s", showGroup, modelRequest.Model, err.Error()) + message := fmt.Sprintf("获取分组 %s 下模型 %s 的可用渠道失败(distributor): %s", showGroup, modelRequest.Model, err.Error()) // 如果错误,但是渠道不为空,说明是数据库一致性问题 //if channel != nil { // common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id)) @@ -165,10 +166,45 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) { } c.Set("platform", string(constant.TaskPlatformSuno)) c.Set("relay_mode", relayMode) + } else if strings.Contains(c.Request.URL.Path, "/v1/videos") { + //curl https://api.openai.com/v1/videos \ + // -H "Authorization: Bearer $OPENAI_API_KEY" \ + // -F "model=sora-2" \ + // -F "prompt=A calico cat playing a piano on stage" + // -F input_reference="@image.jpg" + relayMode := relayconstant.RelayModeUnknown + if c.Request.Method == http.MethodPost { + relayMode = relayconstant.RelayModeVideoSubmit + contentType := c.Request.Header.Get("Content-Type") + if strings.HasPrefix(contentType, "multipart/form-data") { + form, err := common.ParseMultipartFormReusable(c) + if err != nil { + return nil, false, errors.New("无效的video请求, " + err.Error()) + } + defer form.RemoveAll() + if form != nil { + if values, ok := form.Value["model"]; ok && len(values) > 0 { + modelRequest.Model = values[0] + } + } + } else if strings.HasPrefix(contentType, "application/json") { + err = common.UnmarshalBodyReusable(c, &modelRequest) + if err != nil { + return nil, false, errors.New("无效的video请求, " + err.Error()) + } + } + } else if c.Request.Method == http.MethodGet { + relayMode = relayconstant.RelayModeVideoFetchByID + shouldSelectChannel = false + } + c.Set("relay_mode", relayMode) } else if strings.Contains(c.Request.URL.Path, "/v1/video/generations") { relayMode := relayconstant.RelayModeUnknown if c.Request.Method == http.MethodPost { err = common.UnmarshalBodyReusable(c, &modelRequest) + if err != nil { + return nil, false, errors.New("video无效的请求, " + err.Error()) + } relayMode = relayconstant.RelayModeVideoSubmit } else if c.Request.Method == http.MethodGet { relayMode = relayconstant.RelayModeVideoFetchByID diff --git a/middleware/email-verification-rate-limit.go b/middleware/email-verification-rate-limit.go index a7d828d96..470d7731c 100644 --- a/middleware/email-verification-rate-limit.go +++ b/middleware/email-verification-rate-limit.go @@ -4,9 +4,10 @@ import ( "context" "fmt" "net/http" - "one-api/common" "time" + "github.com/QuantumNous/new-api/common" + "github.com/gin-gonic/gin" ) diff --git a/middleware/gzip.go b/middleware/gzip.go index 5b9d566a4..7fe2f3be3 100644 --- a/middleware/gzip.go +++ b/middleware/gzip.go @@ -2,10 +2,11 @@ package middleware import ( "compress/gzip" - "github.com/andybalholm/brotli" - "github.com/gin-gonic/gin" "io" "net/http" + + "github.com/andybalholm/brotli" + "github.com/gin-gonic/gin" ) func DecompressRequestMiddleware() gin.HandlerFunc { diff --git a/middleware/jimeng_adapter.go b/middleware/jimeng_adapter.go index ce5e14675..3e3dd7ae5 100644 --- a/middleware/jimeng_adapter.go +++ b/middleware/jimeng_adapter.go @@ -3,12 +3,13 @@ package middleware import ( "bytes" "encoding/json" - "github.com/gin-gonic/gin" "io" "net/http" - "one-api/common" - "one-api/constant" - relayconstant "one-api/relay/constant" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/gin-gonic/gin" ) func JimengRequestConvert() func(c *gin.Context) { diff --git a/middleware/kling_adapter.go b/middleware/kling_adapter.go index 20973c9f6..e200379c0 100644 --- a/middleware/kling_adapter.go +++ b/middleware/kling_adapter.go @@ -4,8 +4,9 @@ import ( "bytes" "encoding/json" "io" - "one-api/common" - "one-api/constant" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" "github.com/gin-gonic/gin" ) diff --git a/middleware/logger.go b/middleware/logger.go index 02f2e0a99..b4ed8c89d 100644 --- a/middleware/logger.go +++ b/middleware/logger.go @@ -2,8 +2,9 @@ package middleware import ( "fmt" + + "github.com/QuantumNous/new-api/common" "github.com/gin-gonic/gin" - "one-api/common" ) func SetUpLogger(server *gin.Engine) { diff --git a/middleware/model-rate-limit.go b/middleware/model-rate-limit.go index 14d9a737e..80a3995df 100644 --- a/middleware/model-rate-limit.go +++ b/middleware/model-rate-limit.go @@ -4,13 +4,14 @@ import ( "context" "fmt" "net/http" - "one-api/common" - "one-api/common/limiter" - "one-api/constant" - "one-api/setting" "strconv" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/common/limiter" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/setting" + "github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" ) diff --git a/middleware/rate-limit.go b/middleware/rate-limit.go index e38fb8f66..57aeb3190 100644 --- a/middleware/rate-limit.go +++ b/middleware/rate-limit.go @@ -3,10 +3,11 @@ package middleware import ( "context" "fmt" - "github.com/gin-gonic/gin" "net/http" - "one-api/common" "time" + + "github.com/QuantumNous/new-api/common" + "github.com/gin-gonic/gin" ) var timeFormat = "2006-01-02T15:04:05.000Z" diff --git a/middleware/recover.go b/middleware/recover.go index d78c8137f..745a61015 100644 --- a/middleware/recover.go +++ b/middleware/recover.go @@ -2,10 +2,11 @@ package middleware import ( "fmt" - "github.com/gin-gonic/gin" "net/http" - "one-api/common" "runtime/debug" + + "github.com/QuantumNous/new-api/common" + "github.com/gin-gonic/gin" ) func RelayPanicRecover() gin.HandlerFunc { diff --git a/middleware/request-id.go b/middleware/request-id.go index e623be7a2..2b3e5ddc1 100644 --- a/middleware/request-id.go +++ b/middleware/request-id.go @@ -2,8 +2,9 @@ package middleware import ( "context" + + "github.com/QuantumNous/new-api/common" "github.com/gin-gonic/gin" - "one-api/common" ) func RequestId() func(c *gin.Context) { diff --git a/middleware/secure_verification.go b/middleware/secure_verification.go new file mode 100644 index 000000000..19fae9a59 --- /dev/null +++ b/middleware/secure_verification.go @@ -0,0 +1,131 @@ +package middleware + +import ( + "net/http" + "time" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" +) + +const ( + // SecureVerificationSessionKey 安全验证的 session key(与 controller 保持一致) + SecureVerificationSessionKey = "secure_verified_at" + // SecureVerificationTimeout 验证有效期(秒) + SecureVerificationTimeout = 300 // 5分钟 +) + +// SecureVerificationRequired 安全验证中间件 +// 检查用户是否在有效时间内通过了安全验证 +// 如果未验证或验证已过期,返回 401 错误 +func SecureVerificationRequired() gin.HandlerFunc { + return func(c *gin.Context) { + // 检查用户是否已登录 + userId := c.GetInt("id") + if userId == 0 { + c.JSON(http.StatusUnauthorized, gin.H{ + "success": false, + "message": "未登录", + }) + c.Abort() + return + } + + // 检查 session 中的验证时间戳 + session := sessions.Default(c) + verifiedAtRaw := session.Get(SecureVerificationSessionKey) + + if verifiedAtRaw == nil { + c.JSON(http.StatusForbidden, gin.H{ + "success": false, + "message": "需要安全验证", + "code": "VERIFICATION_REQUIRED", + }) + c.Abort() + return + } + + verifiedAt, ok := verifiedAtRaw.(int64) + if !ok { + // session 数据格式错误 + session.Delete(SecureVerificationSessionKey) + _ = session.Save() + c.JSON(http.StatusForbidden, gin.H{ + "success": false, + "message": "验证状态异常,请重新验证", + "code": "VERIFICATION_INVALID", + }) + c.Abort() + return + } + + // 检查验证是否过期 + elapsed := time.Now().Unix() - verifiedAt + if elapsed >= SecureVerificationTimeout { + // 验证已过期,清除 session + session.Delete(SecureVerificationSessionKey) + _ = session.Save() + c.JSON(http.StatusForbidden, gin.H{ + "success": false, + "message": "验证已过期,请重新验证", + "code": "VERIFICATION_EXPIRED", + }) + c.Abort() + return + } + + // 验证有效,继续处理请求 + c.Next() + } +} + +// OptionalSecureVerification 可选的安全验证中间件 +// 如果用户已验证,则在 context 中设置标记,但不阻止请求继续 +// 用于某些需要区分是否已验证的场景 +func OptionalSecureVerification() gin.HandlerFunc { + return func(c *gin.Context) { + userId := c.GetInt("id") + if userId == 0 { + c.Set("secure_verified", false) + c.Next() + return + } + + session := sessions.Default(c) + verifiedAtRaw := session.Get(SecureVerificationSessionKey) + + if verifiedAtRaw == nil { + c.Set("secure_verified", false) + c.Next() + return + } + + verifiedAt, ok := verifiedAtRaw.(int64) + if !ok { + c.Set("secure_verified", false) + c.Next() + return + } + + elapsed := time.Now().Unix() - verifiedAt + if elapsed >= SecureVerificationTimeout { + session.Delete(SecureVerificationSessionKey) + _ = session.Save() + c.Set("secure_verified", false) + c.Next() + return + } + + c.Set("secure_verified", true) + c.Set("secure_verified_at", verifiedAt) + c.Next() + } +} + +// ClearSecureVerification 清除安全验证状态 +// 用于用户登出或需要强制重新验证的场景 +func ClearSecureVerification(c *gin.Context) { + session := sessions.Default(c) + session.Delete(SecureVerificationSessionKey) + _ = session.Save() +} diff --git a/middleware/turnstile-check.go b/middleware/turnstile-check.go index 106a72781..af87fad44 100644 --- a/middleware/turnstile-check.go +++ b/middleware/turnstile-check.go @@ -2,11 +2,12 @@ package middleware import ( "encoding/json" - "github.com/gin-contrib/sessions" - "github.com/gin-gonic/gin" "net/http" "net/url" - "one-api/common" + + "github.com/QuantumNous/new-api/common" + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" ) type turnstileCheckResponse struct { diff --git a/middleware/utils.go b/middleware/utils.go index 77d1eb805..24caa83c7 100644 --- a/middleware/utils.go +++ b/middleware/utils.go @@ -2,9 +2,10 @@ package middleware import ( "fmt" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/logger" "github.com/gin-gonic/gin" - "one-api/common" - "one-api/logger" ) func abortWithOpenAiMessage(c *gin.Context, statusCode int, message string, code ...string) { diff --git a/model/ability.go b/model/ability.go index 123fc7be5..62cb96fa0 100644 --- a/model/ability.go +++ b/model/ability.go @@ -3,10 +3,11 @@ package model import ( "errors" "fmt" - "one-api/common" "strings" "sync" + "github.com/QuantumNous/new-api/common" + "github.com/samber/lo" "gorm.io/gorm" "gorm.io/gorm/clause" diff --git a/model/channel.go b/model/channel.go index 534e2f3f2..d01f00c97 100644 --- a/model/channel.go +++ b/model/channel.go @@ -6,13 +6,14 @@ import ( "errors" "fmt" "math/rand" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/types" "strings" "sync" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/types" + "github.com/samber/lo" "gorm.io/gorm" ) @@ -46,7 +47,7 @@ type Channel struct { Setting *string `json:"setting" gorm:"type:text"` // 渠道额外设置 ParamOverride *string `json:"param_override" gorm:"type:text"` HeaderOverride *string `json:"header_override" gorm:"type:text"` - Remark string `json:"remark,omitempty" gorm:"type:varchar(255)" validate:"max=255"` + Remark *string `json:"remark" gorm:"type:varchar(255)" validate:"max=255"` // add after v0.8.5 ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"` diff --git a/model/channel_cache.go b/model/channel_cache.go index 86866e404..7637658d5 100644 --- a/model/channel_cache.go +++ b/model/channel_cache.go @@ -4,15 +4,16 @@ import ( "errors" "fmt" "math/rand" - "one-api/common" - "one-api/constant" - "one-api/setting" - "one-api/setting/ratio_setting" "sort" "strings" "sync" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/setting" + "github.com/QuantumNous/new-api/setting/ratio_setting" + "github.com/gin-gonic/gin" ) diff --git a/model/log.go b/model/log.go index 979cbe7b2..7495d647d 100644 --- a/model/log.go +++ b/model/log.go @@ -3,13 +3,14 @@ package model import ( "context" "fmt" - "one-api/common" - "one-api/logger" - "one-api/types" "os" "strings" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/logger" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" "github.com/bytedance/gopkg/util/gopool" @@ -38,13 +39,15 @@ type Log struct { Other string `json:"other"` } +// don't use iota, avoid change log type value const ( - LogTypeUnknown = iota - LogTypeTopup - LogTypeConsume - LogTypeManage - LogTypeSystem - LogTypeError + LogTypeUnknown = 0 + LogTypeTopup = 1 + LogTypeConsume = 2 + LogTypeManage = 3 + LogTypeSystem = 4 + LogTypeError = 5 + LogTypeRefund = 6 ) func formatUserLogs(logs []*Log) { diff --git a/model/main.go b/model/main.go index 1a38d371b..04842f13f 100644 --- a/model/main.go +++ b/model/main.go @@ -3,13 +3,14 @@ package model import ( "fmt" "log" - "one-api/common" - "one-api/constant" "os" "strings" "sync" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/glebarez/sqlite" "gorm.io/driver/mysql" "gorm.io/driver/postgres" @@ -251,6 +252,7 @@ func migrateDB() error { &Channel{}, &Token{}, &User{}, + &PasskeyCredential{}, &Option{}, &Redemption{}, &Ability{}, @@ -283,6 +285,7 @@ func migrateDBFast() error { {&Channel{}, "Channel"}, {&Token{}, "Token"}, {&User{}, "User"}, + {&PasskeyCredential{}, "PasskeyCredential"}, {&Option{}, "Option"}, {&Redemption{}, "Redemption"}, {&Ability{}, "Ability"}, diff --git a/model/model_meta.go b/model/model_meta.go index e41cbd090..465d15f1d 100644 --- a/model/model_meta.go +++ b/model/model_meta.go @@ -1,9 +1,10 @@ package model import ( - "one-api/common" "strconv" + "github.com/QuantumNous/new-api/common" + "gorm.io/gorm" ) diff --git a/model/option.go b/model/option.go index 5d177355d..e9fd50d7f 100644 --- a/model/option.go +++ b/model/option.go @@ -1,15 +1,16 @@ package model import ( - "one-api/common" - "one-api/setting" - "one-api/setting/config" - "one-api/setting/operation_setting" - "one-api/setting/ratio_setting" - "one-api/setting/system_setting" "strconv" "strings" "time" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/setting" + "github.com/QuantumNous/new-api/setting/config" + "github.com/QuantumNous/new-api/setting/operation_setting" + "github.com/QuantumNous/new-api/setting/ratio_setting" + "github.com/QuantumNous/new-api/setting/system_setting" ) type Option struct { @@ -82,6 +83,7 @@ func InitOptionMap() { common.OptionMap["StripeWebhookSecret"] = setting.StripeWebhookSecret common.OptionMap["StripePriceId"] = setting.StripePriceId common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(setting.StripeUnitPrice, 'f', -1, 64) + common.OptionMap["StripePromotionCodesEnabled"] = strconv.FormatBool(setting.StripePromotionCodesEnabled) common.OptionMap["CreemApiKey"] = setting.CreemApiKey common.OptionMap["CreemProducts"] = setting.CreemProducts common.OptionMap["CreemTestMode"] = strconv.FormatBool(setting.CreemTestMode) @@ -243,7 +245,15 @@ func updateOptionMap(key string, value string) (err error) { case "LogConsumeEnabled": common.LogConsumeEnabled = boolValue case "DisplayInCurrencyEnabled": - common.DisplayInCurrencyEnabled = boolValue + // 兼容旧字段:同步到新配置 general_setting.quota_display_type(运行时生效) + // true -> USD, false -> TOKENS + newVal := "USD" + if !boolValue { + newVal = "TOKENS" + } + if cfg := config.GlobalConfig.Get("general_setting"); cfg != nil { + _ = config.UpdateConfigFromMap(cfg, map[string]string{"quota_display_type": newVal}) + } case "DisplayTokenStatEnabled": common.DisplayTokenStatEnabled = boolValue case "DrawingEnabled": @@ -334,6 +344,8 @@ func updateOptionMap(key string, value string) (err error) { setting.StripeUnitPrice, _ = strconv.ParseFloat(value, 64) case "StripeMinTopUp": setting.StripeMinTopUp, _ = strconv.Atoi(value) + case "StripePromotionCodesEnabled": + setting.StripePromotionCodesEnabled = value == "true" case "CreemApiKey": setting.CreemApiKey = value case "CreemProducts": diff --git a/model/passkey.go b/model/passkey.go new file mode 100644 index 000000000..5d2595cf8 --- /dev/null +++ b/model/passkey.go @@ -0,0 +1,210 @@ +package model + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/QuantumNous/new-api/common" + + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "gorm.io/gorm" +) + +var ( + ErrPasskeyNotFound = errors.New("passkey credential not found") + ErrFriendlyPasskeyNotFound = errors.New("Passkey 验证失败,请重试或联系管理员") +) + +type PasskeyCredential struct { + ID int `json:"id" gorm:"primaryKey"` + UserID int `json:"user_id" gorm:"uniqueIndex;not null"` + CredentialID string `json:"credential_id" gorm:"type:varchar(512);uniqueIndex;not null"` // base64 encoded + PublicKey string `json:"public_key" gorm:"type:text;not null"` // base64 encoded + AttestationType string `json:"attestation_type" gorm:"type:varchar(255)"` + AAGUID string `json:"aaguid" gorm:"type:varchar(512)"` // base64 encoded + SignCount uint32 `json:"sign_count" gorm:"default:0"` + CloneWarning bool `json:"clone_warning"` + UserPresent bool `json:"user_present"` + UserVerified bool `json:"user_verified"` + BackupEligible bool `json:"backup_eligible"` + BackupState bool `json:"backup_state"` + Transports string `json:"transports" gorm:"type:text"` + Attachment string `json:"attachment" gorm:"type:varchar(32)"` + LastUsedAt *time.Time `json:"last_used_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +func (p *PasskeyCredential) TransportList() []protocol.AuthenticatorTransport { + if p == nil || strings.TrimSpace(p.Transports) == "" { + return nil + } + var transports []string + if err := json.Unmarshal([]byte(p.Transports), &transports); err != nil { + return nil + } + result := make([]protocol.AuthenticatorTransport, 0, len(transports)) + for _, transport := range transports { + result = append(result, protocol.AuthenticatorTransport(transport)) + } + return result +} + +func (p *PasskeyCredential) SetTransports(list []protocol.AuthenticatorTransport) { + if len(list) == 0 { + p.Transports = "" + return + } + stringList := make([]string, len(list)) + for i, transport := range list { + stringList[i] = string(transport) + } + encoded, err := json.Marshal(stringList) + if err != nil { + return + } + p.Transports = string(encoded) +} + +func (p *PasskeyCredential) ToWebAuthnCredential() webauthn.Credential { + flags := webauthn.CredentialFlags{ + UserPresent: p.UserPresent, + UserVerified: p.UserVerified, + BackupEligible: p.BackupEligible, + BackupState: p.BackupState, + } + + credID, _ := base64.StdEncoding.DecodeString(p.CredentialID) + pubKey, _ := base64.StdEncoding.DecodeString(p.PublicKey) + aaguid, _ := base64.StdEncoding.DecodeString(p.AAGUID) + + return webauthn.Credential{ + ID: credID, + PublicKey: pubKey, + AttestationType: p.AttestationType, + Transport: p.TransportList(), + Flags: flags, + Authenticator: webauthn.Authenticator{ + AAGUID: aaguid, + SignCount: p.SignCount, + CloneWarning: p.CloneWarning, + Attachment: protocol.AuthenticatorAttachment(p.Attachment), + }, + } +} + +func NewPasskeyCredentialFromWebAuthn(userID int, credential *webauthn.Credential) *PasskeyCredential { + if credential == nil { + return nil + } + passkey := &PasskeyCredential{ + UserID: userID, + CredentialID: base64.StdEncoding.EncodeToString(credential.ID), + PublicKey: base64.StdEncoding.EncodeToString(credential.PublicKey), + AttestationType: credential.AttestationType, + AAGUID: base64.StdEncoding.EncodeToString(credential.Authenticator.AAGUID), + SignCount: credential.Authenticator.SignCount, + CloneWarning: credential.Authenticator.CloneWarning, + UserPresent: credential.Flags.UserPresent, + UserVerified: credential.Flags.UserVerified, + BackupEligible: credential.Flags.BackupEligible, + BackupState: credential.Flags.BackupState, + Attachment: string(credential.Authenticator.Attachment), + } + passkey.SetTransports(credential.Transport) + return passkey +} + +func (p *PasskeyCredential) ApplyValidatedCredential(credential *webauthn.Credential) { + if credential == nil || p == nil { + return + } + p.CredentialID = base64.StdEncoding.EncodeToString(credential.ID) + p.PublicKey = base64.StdEncoding.EncodeToString(credential.PublicKey) + p.AttestationType = credential.AttestationType + p.AAGUID = base64.StdEncoding.EncodeToString(credential.Authenticator.AAGUID) + p.SignCount = credential.Authenticator.SignCount + p.CloneWarning = credential.Authenticator.CloneWarning + p.UserPresent = credential.Flags.UserPresent + p.UserVerified = credential.Flags.UserVerified + p.BackupEligible = credential.Flags.BackupEligible + p.BackupState = credential.Flags.BackupState + p.Attachment = string(credential.Authenticator.Attachment) + p.SetTransports(credential.Transport) +} + +func GetPasskeyByUserID(userID int) (*PasskeyCredential, error) { + if userID == 0 { + common.SysLog("GetPasskeyByUserID: empty user ID") + return nil, ErrFriendlyPasskeyNotFound + } + var credential PasskeyCredential + if err := DB.Where("user_id = ?", userID).First(&credential).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // 未找到记录是正常情况(用户未绑定),返回 ErrPasskeyNotFound 而不记录日志 + return nil, ErrPasskeyNotFound + } + // 只有真正的数据库错误才记录日志 + common.SysLog(fmt.Sprintf("GetPasskeyByUserID: database error for user %d: %v", userID, err)) + return nil, ErrFriendlyPasskeyNotFound + } + return &credential, nil +} + +func GetPasskeyByCredentialID(credentialID []byte) (*PasskeyCredential, error) { + if len(credentialID) == 0 { + common.SysLog("GetPasskeyByCredentialID: empty credential ID") + return nil, ErrFriendlyPasskeyNotFound + } + + credIDStr := base64.StdEncoding.EncodeToString(credentialID) + var credential PasskeyCredential + if err := DB.Where("credential_id = ?", credIDStr).First(&credential).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + common.SysLog(fmt.Sprintf("GetPasskeyByCredentialID: passkey not found for credential ID length %d", len(credentialID))) + return nil, ErrFriendlyPasskeyNotFound + } + common.SysLog(fmt.Sprintf("GetPasskeyByCredentialID: database error for credential ID: %v", err)) + return nil, ErrFriendlyPasskeyNotFound + } + + return &credential, nil +} + +func UpsertPasskeyCredential(credential *PasskeyCredential) error { + if credential == nil { + common.SysLog("UpsertPasskeyCredential: nil credential provided") + return fmt.Errorf("Passkey 保存失败,请重试") + } + return DB.Transaction(func(tx *gorm.DB) error { + // 使用Unscoped()进行硬删除,避免唯一索引冲突 + if err := tx.Unscoped().Where("user_id = ?", credential.UserID).Delete(&PasskeyCredential{}).Error; err != nil { + common.SysLog(fmt.Sprintf("UpsertPasskeyCredential: failed to delete existing credential for user %d: %v", credential.UserID, err)) + return fmt.Errorf("Passkey 保存失败,请重试") + } + if err := tx.Create(credential).Error; err != nil { + common.SysLog(fmt.Sprintf("UpsertPasskeyCredential: failed to create credential for user %d: %v", credential.UserID, err)) + return fmt.Errorf("Passkey 保存失败,请重试") + } + return nil + }) +} + +func DeletePasskeyByUserID(userID int) error { + if userID == 0 { + common.SysLog("DeletePasskeyByUserID: empty user ID") + return fmt.Errorf("删除失败,请重试") + } + // 使用Unscoped()进行硬删除,避免唯一索引冲突 + if err := DB.Unscoped().Where("user_id = ?", userID).Delete(&PasskeyCredential{}).Error; err != nil { + common.SysLog(fmt.Sprintf("DeletePasskeyByUserID: failed to delete passkey for user %d: %v", userID, err)) + return fmt.Errorf("删除失败,请重试") + } + return nil +} diff --git a/model/prefill_group.go b/model/prefill_group.go index a21b76fe2..cc2e64da9 100644 --- a/model/prefill_group.go +++ b/model/prefill_group.go @@ -3,7 +3,8 @@ package model import ( "database/sql/driver" "encoding/json" - "one-api/common" + + "github.com/QuantumNous/new-api/common" "gorm.io/gorm" ) diff --git a/model/pricing.go b/model/pricing.go index c1192a3d9..d56068a10 100644 --- a/model/pricing.go +++ b/model/pricing.go @@ -5,12 +5,13 @@ import ( "fmt" "strings" - "one-api/common" - "one-api/constant" - "one-api/setting/ratio_setting" - "one-api/types" "sync" "time" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/setting/ratio_setting" + "github.com/QuantumNous/new-api/types" ) type Pricing struct { diff --git a/model/redemption.go b/model/redemption.go index 1ab84f45c..7dd2d9527 100644 --- a/model/redemption.go +++ b/model/redemption.go @@ -3,10 +3,11 @@ package model import ( "errors" "fmt" - "one-api/common" - "one-api/logger" "strconv" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/logger" + "gorm.io/gorm" ) diff --git a/model/task.go b/model/task.go index 4c64a5293..a8c3a7d4d 100644 --- a/model/task.go +++ b/model/task.go @@ -3,13 +3,32 @@ package model import ( "database/sql/driver" "encoding/json" - "one-api/constant" - commonRelay "one-api/relay/common" "time" + + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + commonRelay "github.com/QuantumNous/new-api/relay/common" ) type TaskStatus string +func (t TaskStatus) ToVideoStatus() string { + var status string + switch t { + case TaskStatusQueued, TaskStatusSubmitted: + status = dto.VideoStatusQueued + case TaskStatusInProgress: + status = dto.VideoStatusInProgress + case TaskStatusSuccess: + status = dto.VideoStatusCompleted + case TaskStatusFailure: + status = dto.VideoStatusFailed + default: + status = dto.VideoStatusUnknown // Default fallback + } + return status +} + const ( TaskStatusNotStart TaskStatus = "NOT_START" TaskStatusSubmitted = "SUBMITTED" @@ -24,9 +43,10 @@ type Task struct { ID int64 `json:"id" gorm:"primary_key;AUTO_INCREMENT"` CreatedAt int64 `json:"created_at" gorm:"index"` UpdatedAt int64 `json:"updated_at"` - TaskID string `json:"task_id" gorm:"type:varchar(50);index"` // 第三方id,不一定有/ song id\ Task id + TaskID string `json:"task_id" gorm:"type:varchar(191);index"` // 第三方id,不一定有/ song id\ Task id Platform constant.TaskPlatform `json:"platform" gorm:"type:varchar(30);index"` // 平台 UserId int `json:"user_id" gorm:"index"` + Group string `json:"group" gorm:"type:varchar(50)"` // 修正计费用 ChannelId int `json:"channel_id" gorm:"index"` Quota int `json:"quota"` Action string `json:"action" gorm:"type:varchar(40);index"` // 任务类型, song, lyrics, description-mode @@ -80,6 +100,7 @@ type SyncTaskQueryParams struct { func InitTask(platform constant.TaskPlatform, relayInfo *commonRelay.RelayInfo) *Task { t := &Task{ UserId: relayInfo.UserId, + Group: relayInfo.UsingGroup, SubmitTime: time.Now().Unix(), Status: TaskStatusNotStart, Progress: "0%", @@ -174,7 +195,7 @@ func GetAllUnFinishSyncTasks(limit int) []*Task { var tasks []*Task var err error // get all tasks progress is not 100% - err = DB.Where("progress != ?", "100%").Limit(limit).Order("id").Find(&tasks).Error + err = DB.Where("progress != ?", "100%").Where("status != ?", TaskStatusFailure).Where("status != ?", TaskStatusSuccess).Limit(limit).Order("id").Find(&tasks).Error if err != nil { return nil } diff --git a/model/token.go b/model/token.go index 320b5cf04..c1fe2a670 100644 --- a/model/token.go +++ b/model/token.go @@ -3,9 +3,10 @@ package model import ( "errors" "fmt" - "one-api/common" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/bytedance/gopkg/util/gopool" "gorm.io/gorm" ) diff --git a/model/token_cache.go b/model/token_cache.go index 5399dbc82..947f587d6 100644 --- a/model/token_cache.go +++ b/model/token_cache.go @@ -2,9 +2,10 @@ package model import ( "fmt" - "one-api/common" - "one-api/constant" "time" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" ) func cacheSetToken(token Token) error { diff --git a/model/topup.go b/model/topup.go index bea507477..994094d9d 100644 --- a/model/topup.go +++ b/model/topup.go @@ -3,21 +3,24 @@ package model import ( "errors" "fmt" - "one-api/common" - "one-api/logger" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/logger" + + "github.com/shopspring/decimal" "gorm.io/gorm" ) type TopUp struct { - Id int `json:"id"` - UserId int `json:"user_id" gorm:"index"` - Amount int64 `json:"amount"` - Money float64 `json:"money"` - TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"` - CreateTime int64 `json:"create_time"` - CompleteTime int64 `json:"complete_time"` - Status string `json:"status"` + Id int `json:"id"` + UserId int `json:"user_id" gorm:"index"` + Amount int64 `json:"amount"` + Money float64 `json:"money"` + TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"` + PaymentMethod string `json:"payment_method" gorm:"type:varchar(50)"` + CreateTime int64 `json:"create_time"` + CompleteTime int64 `json:"complete_time"` + Status string `json:"status"` } func (topUp *TopUp) Insert() error { @@ -99,6 +102,209 @@ func Recharge(referenceId string, customerId string) (err error) { return nil } + +func GetUserTopUps(userId int, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) { + // Start transaction + tx := DB.Begin() + if tx.Error != nil { + return nil, 0, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // Get total count within transaction + err = tx.Model(&TopUp{}).Where("user_id = ?", userId).Count(&total).Error + if err != nil { + tx.Rollback() + return nil, 0, err + } + + // Get paginated topups within same transaction + err = tx.Where("user_id = ?", userId).Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error + if err != nil { + tx.Rollback() + return nil, 0, err + } + + // Commit transaction + if err = tx.Commit().Error; err != nil { + return nil, 0, err + } + + return topups, total, nil +} + +// GetAllTopUps 获取全平台的充值记录(管理员使用) +func GetAllTopUps(pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) { + tx := DB.Begin() + if tx.Error != nil { + return nil, 0, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + if err = tx.Model(&TopUp{}).Count(&total).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = tx.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = tx.Commit().Error; err != nil { + return nil, 0, err + } + + return topups, total, nil +} + +// SearchUserTopUps 按订单号搜索某用户的充值记录 +func SearchUserTopUps(userId int, keyword string, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) { + tx := DB.Begin() + if tx.Error != nil { + return nil, 0, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + query := tx.Model(&TopUp{}).Where("user_id = ?", userId) + if keyword != "" { + like := "%%" + keyword + "%%" + query = query.Where("trade_no LIKE ?", like) + } + + if err = query.Count(&total).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = query.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = tx.Commit().Error; err != nil { + return nil, 0, err + } + return topups, total, nil +} + +// SearchAllTopUps 按订单号搜索全平台充值记录(管理员使用) +func SearchAllTopUps(keyword string, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) { + tx := DB.Begin() + if tx.Error != nil { + return nil, 0, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + query := tx.Model(&TopUp{}) + if keyword != "" { + like := "%%" + keyword + "%%" + query = query.Where("trade_no LIKE ?", like) + } + + if err = query.Count(&total).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = query.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = tx.Commit().Error; err != nil { + return nil, 0, err + } + return topups, total, nil +} + +// ManualCompleteTopUp 管理员手动完成订单并给用户充值 +func ManualCompleteTopUp(tradeNo string) error { + if tradeNo == "" { + return errors.New("未提供订单号") + } + + refCol := "`trade_no`" + if common.UsingPostgreSQL { + refCol = `"trade_no"` + } + + var userId int + var quotaToAdd int + var payMoney float64 + + err := DB.Transaction(func(tx *gorm.DB) error { + topUp := &TopUp{} + // 行级锁,避免并发补单 + if err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", tradeNo).First(topUp).Error; err != nil { + return errors.New("充值订单不存在") + } + + // 幂等处理:已成功直接返回 + if topUp.Status == common.TopUpStatusSuccess { + return nil + } + + if topUp.Status != common.TopUpStatusPending { + return errors.New("订单状态不是待支付,无法补单") + } + + // 计算应充值额度: + // - Stripe 订单:Money 代表经分组倍率换算后的美元数量,直接 * QuotaPerUnit + // - 其他订单(如易支付):Amount 为美元数量,* QuotaPerUnit + if topUp.PaymentMethod == "stripe" { + dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) + quotaToAdd = int(decimal.NewFromFloat(topUp.Money).Mul(dQuotaPerUnit).IntPart()) + } else { + dAmount := decimal.NewFromInt(topUp.Amount) + dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) + quotaToAdd = int(dAmount.Mul(dQuotaPerUnit).IntPart()) + } + if quotaToAdd <= 0 { + return errors.New("无效的充值额度") + } + + // 标记完成 + topUp.CompleteTime = common.GetTimestamp() + topUp.Status = common.TopUpStatusSuccess + if err := tx.Save(topUp).Error; err != nil { + return err + } + + // 增加用户额度(立即写库,保持一致性) + if err := tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error; err != nil { + return err + } + + userId = topUp.UserId + payMoney = topUp.Money + return nil + }) + + if err != nil { + return err + } + + // 事务外记录日志,避免阻塞 + RecordLog(userId, LogTypeTopup, fmt.Sprintf("管理员补单成功,充值金额: %v,支付金额:%f", logger.FormatQuota(quotaToAdd), payMoney)) + return nil +} func RechargeCreem(referenceId string, customerEmail string, customerName string) (err error) { if referenceId == "" { return errors.New("未提供支付单号") diff --git a/model/twofa.go b/model/twofa.go index 2a3d33530..e63c66629 100644 --- a/model/twofa.go +++ b/model/twofa.go @@ -3,9 +3,10 @@ package model import ( "errors" "fmt" - "one-api/common" "time" + "github.com/QuantumNous/new-api/common" + "gorm.io/gorm" ) diff --git a/model/usedata.go b/model/usedata.go index 7e525d2e1..f84beb8d9 100644 --- a/model/usedata.go +++ b/model/usedata.go @@ -2,10 +2,11 @@ package model import ( "fmt" - "gorm.io/gorm" - "one-api/common" "sync" "time" + + "github.com/QuantumNous/new-api/common" + "gorm.io/gorm" ) // QuotaData 柱状图数据 diff --git a/model/user.go b/model/user.go index ea0584c5a..78365e06e 100644 --- a/model/user.go +++ b/model/user.go @@ -4,12 +4,13 @@ import ( "encoding/json" "errors" "fmt" - "one-api/common" - "one-api/dto" - "one-api/logger" "strconv" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/logger" + "github.com/bytedance/gopkg/util/gopool" "gorm.io/gorm" ) @@ -18,7 +19,7 @@ import ( // Otherwise, the sensitive information will be saved on local storage in plain text! type User struct { Id int `json:"id"` - Username string `json:"username" gorm:"unique;index" validate:"max=12"` + Username string `json:"username" gorm:"unique;index" validate:"max=20"` Password string `json:"password" gorm:"not null;" validate:"min=8,max=20"` OriginalPassword string `json:"original_password" gorm:"-:all"` // this field is only for Password change verification, don't save it to database! DisplayName string `json:"display_name" gorm:"index" validate:"max=20"` diff --git a/model/user_cache.go b/model/user_cache.go index 936e1a431..d06acd80e 100644 --- a/model/user_cache.go +++ b/model/user_cache.go @@ -2,11 +2,12 @@ package model import ( "fmt" - "one-api/common" - "one-api/constant" - "one-api/dto" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/gin-gonic/gin" "github.com/bytedance/gopkg/util/gopool" diff --git a/model/utils.go b/model/utils.go index dced2bc61..adfd8e139 100644 --- a/model/utils.go +++ b/model/utils.go @@ -2,10 +2,11 @@ package model import ( "errors" - "one-api/common" "sync" "time" + "github.com/QuantumNous/new-api/common" + "github.com/bytedance/gopkg/util/gopool" "gorm.io/gorm" ) diff --git a/model/vendor_meta.go b/model/vendor_meta.go index 20deaea9b..2bb357f82 100644 --- a/model/vendor_meta.go +++ b/model/vendor_meta.go @@ -1,7 +1,7 @@ package model import ( - "one-api/common" + "github.com/QuantumNous/new-api/common" "gorm.io/gorm" ) diff --git a/new-api.service b/new-api.service new file mode 100644 index 000000000..5a2933615 --- /dev/null +++ b/new-api.service @@ -0,0 +1,18 @@ +# File path: /etc/systemd/system/new-api.service +# sudo systemctl daemon-reload +# sudo systemctl start new-api +# sudo systemctl enable new-api +# sudo systemctl status new-api +[Unit] +Description=One API Service +After=network.target + +[Service] +User=ubuntu # 注意修改用户名 +WorkingDirectory=/path/to/new-api # 注意修改路径 +ExecStart=/path/to/new-api/new-api --port 3000 --log-dir /path/to/new-api/logs # 注意修改路径和端口号 +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/one-api.service b/one-api.service deleted file mode 100644 index 17e236bc5..000000000 --- a/one-api.service +++ /dev/null @@ -1,18 +0,0 @@ -# File path: /etc/systemd/system/one-api.service -# sudo systemctl daemon-reload -# sudo systemctl start one-api -# sudo systemctl enable one-api -# sudo systemctl status one-api -[Unit] -Description=One API Service -After=network.target - -[Service] -User=ubuntu # 注意修改用户名 -WorkingDirectory=/path/to/one-api # 注意修改路径 -ExecStart=/path/to/one-api/one-api --port 3000 --log-dir /path/to/one-api/logs # 注意修改路径和端口号 -Restart=always -RestartSec=5 - -[Install] -WantedBy=multi-user.target diff --git a/relay/audio_handler.go b/relay/audio_handler.go index 1357e3816..15fbb9390 100644 --- a/relay/audio_handler.go +++ b/relay/audio_handler.go @@ -4,12 +4,13 @@ import ( "errors" "fmt" "net/http" - "one-api/common" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/types" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/adapter.go b/relay/channel/adapter.go index 02de99567..7f8faf22d 100644 --- a/relay/channel/adapter.go +++ b/relay/channel/adapter.go @@ -3,9 +3,11 @@ package channel import ( "io" "net/http" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/model" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) @@ -49,3 +51,7 @@ type TaskAdaptor interface { ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) } + +type OpenAIVideoConverter interface { + ConvertToOpenAIVideo(originTask *model.Task) ([]byte, error) +} diff --git a/relay/channel/ali/adaptor.go b/relay/channel/ali/adaptor.go index 3ce9e22d3..f289cab35 100644 --- a/relay/channel/ali/adaptor.go +++ b/relay/channel/ali/adaptor.go @@ -5,15 +5,16 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/claude" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/relay/constant" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/claude" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/ali/dto.go b/relay/channel/ali/dto.go index 0873c99f0..b8072b601 100644 --- a/relay/channel/ali/dto.go +++ b/relay/channel/ali/dto.go @@ -1,6 +1,6 @@ package ali -import "one-api/dto" +import "github.com/QuantumNous/new-api/dto" type AliMessage struct { Content any `json:"content"` diff --git a/relay/channel/ali/image.go b/relay/channel/ali/image.go index 490c9d0ad..f2687e047 100644 --- a/relay/channel/ali/image.go +++ b/relay/channel/ali/image.go @@ -8,15 +8,16 @@ import ( "io" "mime/multipart" "net/http" - "one-api/common" - "one-api/dto" - "one-api/logger" - relaycommon "one-api/relay/common" - "one-api/service" - "one-api/types" "strings" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/logger" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/ali/rerank.go b/relay/channel/ali/rerank.go index e7d6b5141..1323fc830 100644 --- a/relay/channel/ali/rerank.go +++ b/relay/channel/ali/rerank.go @@ -4,10 +4,11 @@ import ( "encoding/json" "io" "net/http" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/service" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/ali/text.go b/relay/channel/ali/text.go index 67b63286c..7c206d04d 100644 --- a/relay/channel/ali/text.go +++ b/relay/channel/ali/text.go @@ -5,13 +5,14 @@ import ( "encoding/json" "io" "net/http" - "one-api/common" - "one-api/dto" - "one-api/relay/helper" - "one-api/service" "strings" - "one-api/types" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/api_request.go b/relay/channel/api_request.go index a065caff7..1ff1e2392 100644 --- a/relay/channel/api_request.go +++ b/relay/channel/api_request.go @@ -6,17 +6,19 @@ import ( "fmt" "io" "net/http" - common2 "one-api/common" - "one-api/logger" - "one-api/relay/common" - "one-api/relay/constant" - "one-api/relay/helper" - "one-api/service" - "one-api/setting/operation_setting" - "one-api/types" + "strings" "sync" "time" + common2 "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/logger" + "github.com/QuantumNous/new-api/relay/common" + "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/gin-gonic/gin" "github.com/gorilla/websocket" @@ -36,6 +38,26 @@ func SetupApiRequestHeader(info *common.RelayInfo, c *gin.Context, req *http.Hea } } +// processHeaderOverride 处理请求头覆盖,支持变量替换 +// 支持的变量:{api_key} +func processHeaderOverride(info *common.RelayInfo) (map[string]string, error) { + headerOverride := make(map[string]string) + for k, v := range info.HeadersOverride { + str, ok := v.(string) + if !ok { + return nil, types.NewError(nil, types.ErrorCodeChannelHeaderOverrideInvalid) + } + + // 替换支持的变量 + if strings.Contains(str, "{api_key}") { + str = strings.ReplaceAll(str, "{api_key}", info.ApiKey) + } + + headerOverride[k] = str + } + return headerOverride, nil +} + func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody io.Reader) (*http.Response, error) { fullRequestURL, err := a.GetRequestURL(info) if err != nil { @@ -49,13 +71,9 @@ func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody return nil, fmt.Errorf("new request failed: %w", err) } headers := req.Header - headerOverride := make(map[string]string) - for k, v := range info.HeadersOverride { - if str, ok := v.(string); ok { - headerOverride[k] = str - } else { - return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid) - } + headerOverride, err := processHeaderOverride(info) + if err != nil { + return nil, err } for key, value := range headerOverride { headers.Set(key, value) @@ -86,13 +104,9 @@ func DoFormRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBod // set form data req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) headers := req.Header - headerOverride := make(map[string]string) - for k, v := range info.HeadersOverride { - if str, ok := v.(string); ok { - headerOverride[k] = str - } else { - return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid) - } + headerOverride, err := processHeaderOverride(info) + if err != nil { + return nil, err } for key, value := range headerOverride { headers.Set(key, value) @@ -114,6 +128,13 @@ func DoWssRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody return nil, fmt.Errorf("get request url failed: %w", err) } targetHeader := http.Header{} + headerOverride, err := processHeaderOverride(info) + if err != nil { + return nil, err + } + for key, value := range headerOverride { + targetHeader.Set(key, value) + } err = a.SetupRequestHeader(c, &targetHeader, info) if err != nil { return nil, fmt.Errorf("setup request header failed: %w", err) @@ -265,6 +286,7 @@ func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http resp, err := client.Do(req) if err != nil { + logger.LogError(c, "do request failed: "+err.Error()) return nil, types.NewError(err, types.ErrorCodeDoRequestFailed, types.ErrOptionWithHideErrMsg("upstream error: do request failed")) } if resp == nil { diff --git a/relay/channel/aws/adaptor.go b/relay/channel/aws/adaptor.go index 9d5e5891e..423cf842d 100644 --- a/relay/channel/aws/adaptor.go +++ b/relay/channel/aws/adaptor.go @@ -1,25 +1,36 @@ package aws import ( - "errors" + "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel/claude" - relaycommon "one-api/relay/common" - "one-api/setting/model_setting" - "one-api/types" + "strings" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/claude" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" + "github.com/pkg/errors" "github.com/gin-gonic/gin" ) +type ClientMode int + const ( - RequestModeCompletion = 1 - RequestModeMessage = 2 + ClientModeApiKey ClientMode = iota + 1 + ClientModeAKSK ) type Adaptor struct { - RequestMode int + ClientMode ClientMode + AwsClient *bedrockruntime.Client + AwsModelId string + AwsReq any + IsNova bool } func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) { @@ -28,8 +39,37 @@ func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dt } func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { - c.Set("request_model", request.Model) - c.Set("converted_request", request) + for i, message := range request.Messages { + updated := false + if !message.IsStringContent() { + content, err := message.ParseContent() + if err != nil { + return nil, errors.Wrap(err, "failed to parse message content") + } + for i2, mediaMessage := range content { + if mediaMessage.Source != nil { + if mediaMessage.Source.Type == "url" { + fileData, err := service.GetFileBase64FromUrl(c, mediaMessage.Source.Url, "formatting image for Claude") + if err != nil { + return nil, fmt.Errorf("get file base64 from url failed: %s", err.Error()) + } + mediaMessage.Source.MediaType = fileData.MimeType + mediaMessage.Source.Data = fileData.Base64Data + mediaMessage.Source.Url = "" + mediaMessage.Source.Type = "base64" + content[i2] = mediaMessage + updated = true + } + } + } + if updated { + message.SetContent(content) + } + } + if updated { + request.Messages[i] = message + } + } return request, nil } @@ -44,15 +84,28 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf } func (a *Adaptor) Init(info *relaycommon.RelayInfo) { - a.RequestMode = RequestModeMessage } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { - return "", nil + if info.ChannelOtherSettings.AwsKeyType == dto.AwsKeyTypeApiKey { + awsModelId := getAwsModelID(info.UpstreamModelName) + a.ClientMode = ClientModeApiKey + awsSecret := strings.Split(info.ApiKey, "|") + if len(awsSecret) != 2 { + return "", errors.New("invalid aws api key, should be in format of |") + } + return fmt.Sprintf("https://bedrock-runtime.%s.amazonaws.com/model/%s/converse", awsModelId, awsSecret[1]), nil + } else { + a.ClientMode = ClientModeAKSK + return "", nil + } } func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { - model_setting.GetClaudeSettings().WriteHeaders(info.OriginModelName, req) + claude.CommonClaudeHeadersOperation(c, req, info) + if a.ClientMode == ClientModeApiKey { + req.Set("Authorization", "Bearer "+info.ApiKey) + } return nil } @@ -63,22 +116,16 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn // 检查是否为Nova模型 if isNovaModel(request.Model) { novaReq := convertToNovaRequest(request) - c.Set("request_model", request.Model) - c.Set("converted_request", novaReq) - c.Set("is_nova_model", true) + a.IsNova = true return novaReq, nil } // 原有的Claude模型处理逻辑 - var claudeReq *dto.ClaudeRequest - var err error - claudeReq, err = claude.RequestOpenAI2ClaudeMessage(c, *request) + claudeReq, err := claude.RequestOpenAI2ClaudeMessage(c, *request) if err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to convert openai request to claude request") } - c.Set("request_model", claudeReq.Model) - c.Set("converted_request", claudeReq) - c.Set("is_nova_model", false) + info.UpstreamModelName = claudeReq.Model return claudeReq, err } @@ -97,14 +144,27 @@ func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommo } func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { - return nil, nil + if a.ClientMode == ClientModeApiKey { + return channel.DoApiRequest(a, c, info, requestBody) + } else { + return doAwsClientRequest(c, info, a, requestBody) + } } func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { - if info.IsStream { - err, usage = awsStreamHandler(c, resp, info, a.RequestMode) + if a.ClientMode == ClientModeApiKey { + claudeAdaptor := claude.Adaptor{} + usage, err = claudeAdaptor.DoResponse(c, resp, info) } else { - err, usage = awsHandler(c, info, a.RequestMode) + if a.IsNova { + err, usage = handleNovaRequest(c, info, a) + } else { + if info.IsStream { + err, usage = awsStreamHandler(c, info, a) + } else { + err, usage = awsHandler(c, info, a) + } + } } return } diff --git a/relay/channel/aws/constants.go b/relay/channel/aws/constants.go index 5ac7ce998..b2060b2ad 100644 --- a/relay/channel/aws/constants.go +++ b/relay/channel/aws/constants.go @@ -16,6 +16,7 @@ var awsModelIDMap = map[string]string{ "claude-sonnet-4-20250514": "anthropic.claude-sonnet-4-20250514-v1:0", "claude-opus-4-20250514": "anthropic.claude-opus-4-20250514-v1:0", "claude-opus-4-1-20250805": "anthropic.claude-opus-4-1-20250805-v1:0", + "claude-sonnet-4-5-20250929": "anthropic.claude-sonnet-4-5-20250929-v1:0", // Nova models "nova-micro-v1:0": "amazon.nova-micro-v1:0", "nova-lite-v1:0": "amazon.nova-lite-v1:0", @@ -69,6 +70,11 @@ var awsModelCanCrossRegionMap = map[string]map[string]bool{ "anthropic.claude-opus-4-1-20250805-v1:0": { "us": true, }, + "anthropic.claude-sonnet-4-5-20250929-v1:0": { + "us": true, + "ap": true, + "eu": true, + }, // Nova models - all support three major regions "amazon.nova-micro-v1:0": { "us": true, @@ -118,5 +124,5 @@ var ChannelName = "aws" // 判断是否为Nova模型 func isNovaModel(modelId string) bool { - return strings.HasPrefix(modelId, "nova-") + return strings.Contains(modelId, "nova-") } diff --git a/relay/channel/aws/dto.go b/relay/channel/aws/dto.go index 9c9fe946f..1f3952047 100644 --- a/relay/channel/aws/dto.go +++ b/relay/channel/aws/dto.go @@ -1,7 +1,10 @@ package aws import ( - "one-api/dto" + "io" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" ) type AwsClaudeRequest struct { @@ -35,6 +38,16 @@ func copyRequest(req *dto.ClaudeRequest) *AwsClaudeRequest { } } +func formatRequest(requestBody io.Reader) (*AwsClaudeRequest, error) { + var awsClaudeRequest AwsClaudeRequest + err := common.DecodeJson(requestBody, &awsClaudeRequest) + if err != nil { + return nil, err + } + awsClaudeRequest.AnthropicVersion = "bedrock-2023-05-31" + return &awsClaudeRequest, nil +} + // NovaMessage Nova模型使用messages-v1格式 type NovaMessage struct { Role string `json:"role"` diff --git a/relay/channel/aws/relay-aws.go b/relay/channel/aws/relay-aws.go index eef26855a..8a06003a5 100644 --- a/relay/channel/aws/relay-aws.go +++ b/relay/channel/aws/relay-aws.go @@ -3,15 +3,17 @@ package aws import ( "encoding/json" "fmt" + "io" "net/http" - "one-api/common" - "one-api/dto" - "one-api/relay/channel/claude" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel/claude" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" "github.com/pkg/errors" @@ -48,16 +50,78 @@ func newAwsClient(c *gin.Context, info *relaycommon.RelayInfo) (*bedrockruntime. return client, nil } -func wrapErr(err error) *dto.OpenAIErrorWithStatusCode { - return &dto.OpenAIErrorWithStatusCode{ - StatusCode: http.StatusInternalServerError, - Error: dto.OpenAIError{ - Message: fmt.Sprintf("%s", err.Error()), - }, +func doAwsClientRequest(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor, requestBody io.Reader) (any, error) { + awsCli, err := newAwsClient(c, info) + if err != nil { + return nil, types.NewError(err, types.ErrorCodeChannelAwsClientError) + } + a.AwsClient = awsCli + + println(info.UpstreamModelName) + // 获取对应的AWS模型ID + awsModelId := getAwsModelID(info.UpstreamModelName) + + awsRegionPrefix := getAwsRegionPrefix(awsCli.Options().Region) + canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix) + if canCrossRegion { + awsModelId = awsModelCrossRegion(awsModelId, awsRegionPrefix) + } + + if isNovaModel(awsModelId) { + var novaReq *NovaRequest + err = common.DecodeJson(requestBody, &novaReq) + if err != nil { + return nil, types.NewError(errors.Wrap(err, "decode nova request fail"), types.ErrorCodeBadRequestBody) + } + + // 使用InvokeModel API,但使用Nova格式的请求体 + awsReq := &bedrockruntime.InvokeModelInput{ + ModelId: aws.String(awsModelId), + Accept: aws.String("application/json"), + ContentType: aws.String("application/json"), + } + + reqBody, err := common.Marshal(novaReq) + if err != nil { + return nil, types.NewError(errors.Wrap(err, "marshal nova request"), types.ErrorCodeBadResponseBody) + } + awsReq.Body = reqBody + return nil, nil + } else { + awsClaudeReq, err := formatRequest(requestBody) + if err != nil { + return nil, types.NewError(errors.Wrap(err, "format aws request fail"), types.ErrorCodeBadRequestBody) + } + + if info.IsStream { + awsReq := &bedrockruntime.InvokeModelWithResponseStreamInput{ + ModelId: aws.String(awsModelId), + Accept: aws.String("application/json"), + ContentType: aws.String("application/json"), + } + awsReq.Body, err = common.Marshal(awsClaudeReq) + if err != nil { + return nil, types.NewError(errors.Wrap(err, "marshal aws request fail"), types.ErrorCodeBadRequestBody) + } + a.AwsReq = awsReq + return nil, nil + } else { + awsReq := &bedrockruntime.InvokeModelInput{ + ModelId: aws.String(awsModelId), + Accept: aws.String("application/json"), + ContentType: aws.String("application/json"), + } + awsReq.Body, err = common.Marshal(awsClaudeReq) + if err != nil { + return nil, types.NewError(errors.Wrap(err, "marshal aws request fail"), types.ErrorCodeBadRequestBody) + } + a.AwsReq = awsReq + return nil, nil + } } } -func awsRegionPrefix(awsRegionId string) string { +func getAwsRegionPrefix(awsRegionId string) string { parts := strings.Split(awsRegionId, "-") regionPrefix := "" if len(parts) > 0 { @@ -79,58 +143,16 @@ func awsModelCrossRegion(awsModelId, awsRegionPrefix string) string { return modelPrefix + "." + awsModelId } -func awsModelID(requestModel string) string { - if awsModelID, ok := awsModelIDMap[requestModel]; ok { - return awsModelID +func getAwsModelID(requestModel string) string { + if awsModelIDName, ok := awsModelIDMap[requestModel]; ok { + return awsModelIDName } - return requestModel } -func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (*types.NewAPIError, *dto.Usage) { - awsCli, err := newAwsClient(c, info) - if err != nil { - return types.NewError(err, types.ErrorCodeChannelAwsClientError), nil - } +func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor) (*types.NewAPIError, *dto.Usage) { - awsModelId := awsModelID(c.GetString("request_model")) - // 检查是否为Nova模型 - isNova, _ := c.Get("is_nova_model") - if isNova == true { - // Nova模型也支持跨区域 - awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region) - canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix) - if canCrossRegion { - awsModelId = awsModelCrossRegion(awsModelId, awsRegionPrefix) - } - return handleNovaRequest(c, awsCli, info, awsModelId) - } - - // 原有的Claude处理逻辑 - awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region) - canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix) - if canCrossRegion { - awsModelId = awsModelCrossRegion(awsModelId, awsRegionPrefix) - } - - awsReq := &bedrockruntime.InvokeModelInput{ - ModelId: aws.String(awsModelId), - Accept: aws.String("application/json"), - ContentType: aws.String("application/json"), - } - - claudeReq_, ok := c.Get("converted_request") - if !ok { - return types.NewError(errors.New("aws claude request not found"), types.ErrorCodeInvalidRequest), nil - } - claudeReq := claudeReq_.(*dto.ClaudeRequest) - awsClaudeReq := copyRequest(claudeReq) - awsReq.Body, err = common.Marshal(awsClaudeReq) - if err != nil { - return types.NewError(errors.Wrap(err, "marshal request"), types.ErrorCodeBadResponseBody), nil - } - - awsResp, err := awsCli.InvokeModel(c.Request.Context(), awsReq) + awsResp, err := a.AwsClient.InvokeModel(c.Request.Context(), a.AwsReq.(*bedrockruntime.InvokeModelInput)) if err != nil { return types.NewOpenAIError(errors.Wrap(err, "InvokeModel"), types.ErrorCodeAwsInvokeError, http.StatusInternalServerError), nil } @@ -148,46 +170,15 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (* c.Writer.Header().Set("Content-Type", *awsResp.ContentType) } - handlerErr := claude.HandleClaudeResponseData(c, info, claudeInfo, nil, awsResp.Body, RequestModeMessage) + handlerErr := claude.HandleClaudeResponseData(c, info, claudeInfo, nil, awsResp.Body, claude.RequestModeMessage) if handlerErr != nil { return handlerErr, nil } return nil, claudeInfo.Usage } -func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, requestMode int) (*types.NewAPIError, *dto.Usage) { - awsCli, err := newAwsClient(c, info) - if err != nil { - return types.NewError(err, types.ErrorCodeChannelAwsClientError), nil - } - - awsModelId := awsModelID(c.GetString("request_model")) - - awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region) - canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix) - if canCrossRegion { - awsModelId = awsModelCrossRegion(awsModelId, awsRegionPrefix) - } - - awsReq := &bedrockruntime.InvokeModelWithResponseStreamInput{ - ModelId: aws.String(awsModelId), - Accept: aws.String("application/json"), - ContentType: aws.String("application/json"), - } - - claudeReq_, ok := c.Get("converted_request") - if !ok { - return types.NewError(errors.New("aws claude request not found"), types.ErrorCodeInvalidRequest), nil - } - claudeReq := claudeReq_.(*dto.ClaudeRequest) - - awsClaudeReq := copyRequest(claudeReq) - awsReq.Body, err = common.Marshal(awsClaudeReq) - if err != nil { - return types.NewError(errors.Wrap(err, "marshal request"), types.ErrorCodeBadResponseBody), nil - } - - awsResp, err := awsCli.InvokeModelWithResponseStream(c.Request.Context(), awsReq) +func awsStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor) (*types.NewAPIError, *dto.Usage) { + awsResp, err := a.AwsClient.InvokeModelWithResponseStream(c.Request.Context(), a.AwsReq.(*bedrockruntime.InvokeModelWithResponseStreamInput)) if err != nil { return types.NewOpenAIError(errors.Wrap(err, "InvokeModelWithResponseStream"), types.ErrorCodeAwsInvokeError, http.StatusInternalServerError), nil } @@ -206,7 +197,7 @@ func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel switch v := event.(type) { case *bedrockruntimeTypes.ResponseStreamMemberChunk: info.SetFirstResponseTime() - respErr := claude.HandleStreamResponseData(c, info, claudeInfo, string(v.Value.Bytes), RequestModeMessage) + respErr := claude.HandleStreamResponseData(c, info, claudeInfo, string(v.Value.Bytes), claude.RequestModeMessage) if respErr != nil { return respErr, nil } @@ -219,32 +210,14 @@ func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel } } - claude.HandleStreamFinalResponse(c, info, claudeInfo, RequestModeMessage) + claude.HandleStreamFinalResponse(c, info, claudeInfo, claude.RequestModeMessage) return nil, claudeInfo.Usage } // Nova模型处理函数 -func handleNovaRequest(c *gin.Context, awsCli *bedrockruntime.Client, info *relaycommon.RelayInfo, awsModelId string) (*types.NewAPIError, *dto.Usage) { - novaReq_, ok := c.Get("converted_request") - if !ok { - return types.NewError(errors.New("nova request not found"), types.ErrorCodeInvalidRequest), nil - } - novaReq := novaReq_.(*NovaRequest) +func handleNovaRequest(c *gin.Context, info *relaycommon.RelayInfo, a *Adaptor) (*types.NewAPIError, *dto.Usage) { - // 使用InvokeModel API,但使用Nova格式的请求体 - awsReq := &bedrockruntime.InvokeModelInput{ - ModelId: aws.String(awsModelId), - Accept: aws.String("application/json"), - ContentType: aws.String("application/json"), - } - - reqBody, err := json.Marshal(novaReq) - if err != nil { - return types.NewError(errors.Wrap(err, "marshal nova request"), types.ErrorCodeBadResponseBody), nil - } - awsReq.Body = reqBody - - awsResp, err := awsCli.InvokeModel(c.Request.Context(), awsReq) + awsResp, err := a.AwsClient.InvokeModel(c.Request.Context(), a.AwsReq.(*bedrockruntime.InvokeModelInput)) if err != nil { return types.NewError(errors.Wrap(err, "InvokeModel"), types.ErrorCodeChannelAwsClientError), nil } diff --git a/relay/channel/baidu/adaptor.go b/relay/channel/baidu/adaptor.go index 32e301eed..b8b4735b3 100644 --- a/relay/channel/baidu/adaptor.go +++ b/relay/channel/baidu/adaptor.go @@ -5,13 +5,14 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - relaycommon "one-api/relay/common" - "one-api/relay/constant" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/baidu/dto.go b/relay/channel/baidu/dto.go index a486de5a7..426f63f54 100644 --- a/relay/channel/baidu/dto.go +++ b/relay/channel/baidu/dto.go @@ -1,8 +1,9 @@ package baidu import ( - "one-api/dto" "time" + + "github.com/QuantumNous/new-api/dto" ) type BaiduMessage struct { diff --git a/relay/channel/baidu/relay-baidu.go b/relay/channel/baidu/relay-baidu.go index 31e8319e5..8597e50ef 100644 --- a/relay/channel/baidu/relay-baidu.go +++ b/relay/channel/baidu/relay-baidu.go @@ -6,17 +6,18 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/types" "strings" "sync" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/baidu_v2/adaptor.go b/relay/channel/baidu_v2/adaptor.go index 0577ebcb7..94091e387 100644 --- a/relay/channel/baidu_v2/adaptor.go +++ b/relay/channel/baidu_v2/adaptor.go @@ -5,14 +5,15 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/relay/constant" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/claude/adaptor.go b/relay/channel/claude/adaptor.go index 959327e16..b9b7447f2 100644 --- a/relay/channel/claude/adaptor.go +++ b/relay/channel/claude/adaptor.go @@ -5,13 +5,14 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - relaycommon "one-api/relay/common" - "one-api/setting/model_setting" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/setting/model_setting" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) @@ -52,11 +53,25 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) { } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + baseURL := "" if a.RequestMode == RequestModeMessage { - return fmt.Sprintf("%s/v1/messages", info.ChannelBaseUrl), nil + baseURL = fmt.Sprintf("%s/v1/messages", info.ChannelBaseUrl) } else { - return fmt.Sprintf("%s/v1/complete", info.ChannelBaseUrl), nil + baseURL = fmt.Sprintf("%s/v1/complete", info.ChannelBaseUrl) } + if info.IsClaudeBetaQuery { + baseURL = baseURL + "?beta=true" + } + return baseURL, nil +} + +func CommonClaudeHeadersOperation(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) { + // common headers operation + anthropicBeta := c.Request.Header.Get("anthropic-beta") + if anthropicBeta != "" { + req.Set("anthropic-beta", anthropicBeta) + } + model_setting.GetClaudeSettings().WriteHeaders(info.OriginModelName, req) } func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { @@ -67,7 +82,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel anthropicVersion = "2023-06-01" } req.Set("anthropic-version", anthropicVersion) - model_setting.GetClaudeSettings().WriteHeaders(info.OriginModelName, req) + CommonClaudeHeadersOperation(c, req, info) return nil } diff --git a/relay/channel/claude/constants.go b/relay/channel/claude/constants.go index a23543d21..d0b36fe4f 100644 --- a/relay/channel/claude/constants.go +++ b/relay/channel/claude/constants.go @@ -19,6 +19,8 @@ var ModelList = []string{ "claude-opus-4-20250514-thinking", "claude-opus-4-1-20250805", "claude-opus-4-1-20250805-thinking", + "claude-sonnet-4-5-20250929", + "claude-sonnet-4-5-20250929-thinking", } var ChannelName = "claude" diff --git a/relay/channel/claude/relay-claude.go b/relay/channel/claude/relay-claude.go index 682256416..fe523deff 100644 --- a/relay/channel/claude/relay-claude.go +++ b/relay/channel/claude/relay-claude.go @@ -5,17 +5,18 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/dto" - "one-api/logger" - "one-api/relay/channel/openrouter" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/setting/model_setting" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/logger" + "github.com/QuantumNous/new-api/relay/channel/openrouter" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/setting/model_setting" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) @@ -476,8 +477,7 @@ func StreamResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse signatureContent := "\n" choice.Delta.ReasoningContent = &signatureContent case "thinking_delta": - thinkingContent := claudeResponse.Delta.Thinking - choice.Delta.ReasoningContent = &thinkingContent + choice.Delta.ReasoningContent = claudeResponse.Delta.Thinking } } } else if claudeResponse.Type == "message_delta" { @@ -512,7 +512,9 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse) *dto var responseThinking string if len(claudeResponse.Content) > 0 { responseText = claudeResponse.Content[0].GetText() - responseThinking = claudeResponse.Content[0].Thinking + if claudeResponse.Content[0].Thinking != nil { + responseThinking = *claudeResponse.Content[0].Thinking + } } tools := make([]dto.ToolCallResponse, 0) thinkingContent := "" @@ -544,7 +546,9 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse) *dto }) case "thinking": // 加密的不管, 只输出明文的推理过程 - thinkingContent = message.Thinking + if message.Thinking != nil { + thinkingContent = *message.Thinking + } case "text": responseText = message.GetText() } @@ -597,8 +601,8 @@ func FormatClaudeResponseInfo(requestMode int, claudeResponse *dto.ClaudeRespons if claudeResponse.Delta.Text != nil { claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Text) } - if claudeResponse.Delta.Thinking != "" { - claudeInfo.ResponseText.WriteString(claudeResponse.Delta.Thinking) + if claudeResponse.Delta.Thinking != nil { + claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Thinking) } } else if claudeResponse.Type == "message_delta" { // 最终的usage获取 diff --git a/relay/channel/cloudflare/adaptor.go b/relay/channel/cloudflare/adaptor.go index bdea72f01..af3446238 100644 --- a/relay/channel/cloudflare/adaptor.go +++ b/relay/channel/cloudflare/adaptor.go @@ -6,12 +6,13 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/relay/constant" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/cloudflare/dto.go b/relay/channel/cloudflare/dto.go index 72b406155..7dcb67224 100644 --- a/relay/channel/cloudflare/dto.go +++ b/relay/channel/cloudflare/dto.go @@ -1,6 +1,6 @@ package cloudflare -import "one-api/dto" +import "github.com/QuantumNous/new-api/dto" type CfRequest struct { Messages []dto.Message `json:"messages,omitempty"` diff --git a/relay/channel/cloudflare/relay_cloudflare.go b/relay/channel/cloudflare/relay_cloudflare.go index 00f6b6c5e..0419103d4 100644 --- a/relay/channel/cloudflare/relay_cloudflare.go +++ b/relay/channel/cloudflare/relay_cloudflare.go @@ -5,15 +5,16 @@ import ( "encoding/json" "io" "net/http" - "one-api/dto" - "one-api/logger" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/types" "strings" "time" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/logger" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/cohere/adaptor.go b/relay/channel/cohere/adaptor.go index c8a38d465..664eb6784 100644 --- a/relay/channel/cohere/adaptor.go +++ b/relay/channel/cohere/adaptor.go @@ -5,11 +5,12 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - relaycommon "one-api/relay/common" - "one-api/relay/constant" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/cohere/dto.go b/relay/channel/cohere/dto.go index d51279633..2ab6385c2 100644 --- a/relay/channel/cohere/dto.go +++ b/relay/channel/cohere/dto.go @@ -1,6 +1,6 @@ package cohere -import "one-api/dto" +import "github.com/QuantumNous/new-api/dto" type CohereRequest struct { Model string `json:"model"` diff --git a/relay/channel/cohere/relay-cohere.go b/relay/channel/cohere/relay-cohere.go index af3573480..3f86e1e90 100644 --- a/relay/channel/cohere/relay-cohere.go +++ b/relay/channel/cohere/relay-cohere.go @@ -5,15 +5,16 @@ import ( "encoding/json" "io" "net/http" - "one-api/common" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/types" "strings" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/coze/adaptor.go b/relay/channel/coze/adaptor.go index 0f2a6fd3f..30f229a31 100644 --- a/relay/channel/coze/adaptor.go +++ b/relay/channel/coze/adaptor.go @@ -6,12 +6,13 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/common" - "one-api/types" "time" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/coze/relay-coze.go b/relay/channel/coze/relay-coze.go index c480045f4..5a186d262 100644 --- a/relay/channel/coze/relay-coze.go +++ b/relay/channel/coze/relay-coze.go @@ -7,14 +7,15 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/deepseek/adaptor.go b/relay/channel/deepseek/adaptor.go index 962f8794a..53488f0d5 100644 --- a/relay/channel/deepseek/adaptor.go +++ b/relay/channel/deepseek/adaptor.go @@ -3,17 +3,18 @@ package deepseek import ( "errors" "fmt" - "github.com/gin-gonic/gin" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/claude" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/relay/constant" - "one-api/types" "strings" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/claude" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) type Adaptor struct { diff --git a/relay/channel/dify/adaptor.go b/relay/channel/dify/adaptor.go index 0a08d035a..4ffee3e60 100644 --- a/relay/channel/dify/adaptor.go +++ b/relay/channel/dify/adaptor.go @@ -5,10 +5,11 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - relaycommon "one-api/relay/common" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/dify/dto.go b/relay/channel/dify/dto.go index 7c6f39b69..cb127554d 100644 --- a/relay/channel/dify/dto.go +++ b/relay/channel/dify/dto.go @@ -1,6 +1,6 @@ package dify -import "one-api/dto" +import "github.com/QuantumNous/new-api/dto" type DifyChatRequest struct { Inputs map[string]interface{} `json:"inputs"` diff --git a/relay/channel/dify/relay-dify.go b/relay/channel/dify/relay-dify.go index 2336fd4c9..fb49dea2a 100644 --- a/relay/channel/dify/relay-dify.go +++ b/relay/channel/dify/relay-dify.go @@ -8,16 +8,17 @@ import ( "io" "mime/multipart" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/types" "os" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/gemini/adaptor.go b/relay/channel/gemini/adaptor.go index 57542aa5a..b2a8b186f 100644 --- a/relay/channel/gemini/adaptor.go +++ b/relay/channel/gemini/adaptor.go @@ -5,15 +5,16 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/relay/constant" - "one-api/setting/model_setting" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/setting/model_setting" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) @@ -67,8 +68,12 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf aspectRatio = size } else { switch size { - case "1024x1024": + case "256x256", "512x512", "1024x1024": aspectRatio = "1:1" + case "1536x1024": + aspectRatio = "3:2" + case "1024x1536": + aspectRatio = "2:3" case "1024x1792": aspectRatio = "9:16" case "1792x1024": @@ -91,6 +96,28 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf }, } + // Set imageSize when quality parameter is specified + // Map quality parameter to imageSize (only supported by Standard and Ultra models) + // quality values: auto, high, medium, low (for gpt-image-1), hd, standard (for dall-e-3) + // imageSize values: 1K (default), 2K + // https://ai.google.dev/gemini-api/docs/imagen + // https://platform.openai.com/docs/api-reference/images/create + if request.Quality != "" { + imageSize := "1K" // default + switch request.Quality { + case "hd", "high": + imageSize = "2K" + case "2K": + imageSize = "2K" + case "standard", "medium", "low", "auto", "1K": + imageSize = "1K" + default: + // unknown quality value, default to 1K + imageSize = "1K" + } + geminiRequest.Parameters.ImageSize = imageSize + } + return geminiRequest, nil } diff --git a/relay/channel/gemini/relay-gemini-native.go b/relay/channel/gemini/relay-gemini-native.go index 974a22f50..517e519d6 100644 --- a/relay/channel/gemini/relay-gemini-native.go +++ b/relay/channel/gemini/relay-gemini-native.go @@ -3,15 +3,16 @@ package gemini import ( "io" "net/http" - "one-api/common" - "one-api/dto" - "one-api/logger" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/logger" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/pkg/errors" "github.com/gin-gonic/gin" diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index 199c84664..51a0d615d 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -6,20 +6,21 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/logger" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/setting/model_setting" - "one-api/types" "strconv" "strings" "unicode/utf8" + "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/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/setting/model_setting" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) @@ -210,7 +211,16 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i // eg. {"google":{"thinking_config":{"thinking_budget":5324,"include_thoughts":true}}} if googleBody, ok := extraBody["google"].(map[string]interface{}); ok { adaptorWithExtraBody = true + // check error param name like thinkingConfig, should be thinking_config + if _, hasErrorParam := googleBody["thinkingConfig"]; hasErrorParam { + return nil, errors.New("extra_body.google.thinkingConfig is not supported, use extra_body.google.thinking_config instead") + } + if thinkingConfig, ok := googleBody["thinking_config"].(map[string]interface{}); ok { + // check error param name like thinkingBudget, should be thinking_budget + if _, hasErrorParam := thinkingConfig["thinkingBudget"]; hasErrorParam { + return nil, errors.New("extra_body.google.thinking_config.thinkingBudget is not supported, use extra_body.google.thinking_config.thinking_budget instead") + } if budget, ok := thinkingConfig["thinking_budget"].(float64); ok { budgetInt := int(budget) geminiRequest.GenerationConfig.ThinkingConfig = &dto.GeminiThinkingConfig{ @@ -245,6 +255,7 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i functions := make([]dto.FunctionRequest, 0, len(textRequest.Tools)) googleSearch := false codeExecution := false + urlContext := false for _, tool := range textRequest.Tools { if tool.Function.Name == "googleSearch" { googleSearch = true @@ -254,6 +265,10 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i codeExecution = true continue } + if tool.Function.Name == "urlContext" { + urlContext = true + continue + } if tool.Function.Parameters != nil { params, ok := tool.Function.Parameters.(map[string]interface{}) @@ -281,6 +296,11 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i GoogleSearch: make(map[string]string), }) } + if urlContext { + geminiTools = append(geminiTools, dto.GeminiChatTool{ + URLContext: make(map[string]string), + }) + } if len(functions) > 0 { geminiTools = append(geminiTools, dto.GeminiChatTool{ FunctionDeclarations: functions, @@ -951,9 +971,15 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp * // send first response emptyResponse := helper.GenerateStartEmptyResponse(id, createAt, info.UpstreamModelName, nil) if response.IsToolCall() { - emptyResponse.Choices[0].Delta.ToolCalls = make([]dto.ToolCallResponse, 1) - emptyResponse.Choices[0].Delta.ToolCalls[0] = *response.GetFirstToolCall() - emptyResponse.Choices[0].Delta.ToolCalls[0].Function.Arguments = "" + if len(emptyResponse.Choices) > 0 && len(response.Choices) > 0 { + toolCalls := response.Choices[0].Delta.ToolCalls + copiedToolCalls := make([]dto.ToolCallResponse, len(toolCalls)) + for idx := range toolCalls { + copiedToolCalls[idx] = toolCalls[idx] + copiedToolCalls[idx].Function.Arguments = "" + } + emptyResponse.Choices[0].Delta.ToolCalls = copiedToolCalls + } finishReason = constant.FinishReasonToolCalls err = handleStream(c, info, emptyResponse) if err != nil { @@ -1034,7 +1060,12 @@ func GeminiChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.R return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } if len(geminiResponse.Candidates) == 0 { - return nil, types.NewOpenAIError(errors.New("no candidates returned"), types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + //return nil, types.NewOpenAIError(errors.New("no candidates returned"), types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + //if geminiResponse.PromptFeedback != nil && geminiResponse.PromptFeedback.BlockReason != nil { + // return nil, types.NewOpenAIError(errors.New("request blocked by Gemini API: "+*geminiResponse.PromptFeedback.BlockReason), types.ErrorCodePromptBlocked, http.StatusBadRequest) + //} else { + // return nil, types.NewOpenAIError(errors.New("empty response from Gemini API"), types.ErrorCodeEmptyResponse, http.StatusInternalServerError) + //} } fullTextResponse := responseGeminiChat2OpenAI(c, &geminiResponse) fullTextResponse.Model = info.UpstreamModelName diff --git a/relay/channel/jimeng/adaptor.go b/relay/channel/jimeng/adaptor.go index 885a1427f..1938ac1be 100644 --- a/relay/channel/jimeng/adaptor.go +++ b/relay/channel/jimeng/adaptor.go @@ -6,12 +6,13 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - relayconstant "one-api/relay/constant" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/jimeng/image.go b/relay/channel/jimeng/image.go index 11a0117bb..e422e061d 100644 --- a/relay/channel/jimeng/image.go +++ b/relay/channel/jimeng/image.go @@ -5,10 +5,11 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/service" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/jimeng/sign.go b/relay/channel/jimeng/sign.go index d8b598dce..7c67531e4 100644 --- a/relay/channel/jimeng/sign.go +++ b/relay/channel/jimeng/sign.go @@ -8,14 +8,15 @@ import ( "encoding/json" "errors" "fmt" - "github.com/gin-gonic/gin" "io" "net/http" "net/url" - "one-api/logger" "sort" "strings" "time" + + "github.com/QuantumNous/new-api/logger" + "github.com/gin-gonic/gin" ) // SignRequestForJimeng 对即梦 API 请求进行签名,支持 http.Request 或 header+url+body 方式 diff --git a/relay/channel/jina/adaptor.go b/relay/channel/jina/adaptor.go index a383728f7..3f2d01d96 100644 --- a/relay/channel/jina/adaptor.go +++ b/relay/channel/jina/adaptor.go @@ -5,13 +5,14 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/relay/common_handler" - "one-api/relay/constant" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/common_handler" + "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) @@ -76,6 +77,7 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt } func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + request.EncodingFormat = "" return request, nil } diff --git a/relay/channel/minimax/adaptor.go b/relay/channel/minimax/adaptor.go new file mode 100644 index 000000000..8235abc05 --- /dev/null +++ b/relay/channel/minimax/adaptor.go @@ -0,0 +1,132 @@ +package minimax + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" + + "github.com/gin-gonic/gin" +) + +type Adaptor struct { +} + +func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) { + return nil, errors.New("not implemented") +} + +func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) { + return nil, errors.New("not implemented") +} + +func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { + if info.RelayMode != constant.RelayModeAudioSpeech { + return nil, errors.New("unsupported audio relay mode") + } + + voiceID := request.Voice + speed := request.Speed + outputFormat := request.ResponseFormat + + minimaxRequest := MiniMaxTTSRequest{ + Model: info.OriginModelName, + Text: request.Input, + VoiceSetting: VoiceSetting{ + VoiceID: voiceID, + Speed: speed, + }, + AudioSetting: &AudioSetting{ + Format: outputFormat, + }, + OutputFormat: outputFormat, + } + + // 同步扩展字段的厂商自定义metadata + if len(request.Metadata) > 0 { + if err := json.Unmarshal(request.Metadata, &minimaxRequest); err != nil { + return nil, fmt.Errorf("error unmarshalling metadata to minimax request: %w", err) + } + } + + jsonData, err := json.Marshal(minimaxRequest) + if err != nil { + return nil, fmt.Errorf("error marshalling minimax request: %w", err) + } + if outputFormat != "hex" { + outputFormat = "url" + } + + c.Set("response_format", outputFormat) + + // Debug: log the request structure + // fmt.Printf("MiniMax TTS Request: %s\n", string(jsonData)) + + return bytes.NewReader(jsonData), nil +} + +func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { + return request, nil +} + +func (a *Adaptor) Init(info *relaycommon.RelayInfo) { +} + +func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + return GetRequestURL(info) +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { + channel.SetupApiRequestHeader(info, c, req) + req.Set("Authorization", "Bearer "+info.ApiKey) + return nil +} + +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + return request, nil +} + +func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { + return nil, nil +} + +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + return request, nil +} + +func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) { + return nil, errors.New("not implemented") +} + +func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { + return channel.DoApiRequest(a, c, info, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { + if info.RelayMode == constant.RelayModeAudioSpeech { + return handleTTSResponse(c, resp, info) + } + + adaptor := openai.Adaptor{} + return adaptor.DoResponse(c, resp, info) +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return ChannelName +} diff --git a/relay/channel/minimax/constants.go b/relay/channel/minimax/constants.go index c480cac95..df420d4b1 100644 --- a/relay/channel/minimax/constants.go +++ b/relay/channel/minimax/constants.go @@ -8,6 +8,12 @@ var ModelList = []string{ "abab6-chat", "abab5.5-chat", "abab5.5s-chat", + "speech-2.5-hd-preview", + "speech-2.5-turbo-preview", + "speech-02-hd", + "speech-02-turbo", + "speech-01-hd", + "speech-01-turbo", } var ChannelName = "minimax" diff --git a/relay/channel/minimax/relay-minimax.go b/relay/channel/minimax/relay-minimax.go index ff9b72ea3..b314e69d7 100644 --- a/relay/channel/minimax/relay-minimax.go +++ b/relay/channel/minimax/relay-minimax.go @@ -2,9 +2,24 @@ package minimax import ( "fmt" - relaycommon "one-api/relay/common" + + channelconstant "github.com/QuantumNous/new-api/constant" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/constant" ) func GetRequestURL(info *relaycommon.RelayInfo) (string, error) { - return fmt.Sprintf("%s/v1/text/chatcompletion_v2", info.ChannelBaseUrl), nil + baseUrl := info.ChannelBaseUrl + if baseUrl == "" { + baseUrl = channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeMiniMax] + } + + switch info.RelayMode { + case constant.RelayModeChatCompletions: + return fmt.Sprintf("%s/v1/text/chatcompletion_v2", baseUrl), nil + case constant.RelayModeAudioSpeech: + return fmt.Sprintf("%s/v1/t2a_v2", baseUrl), nil + default: + return "", fmt.Errorf("unsupported relay mode: %d", info.RelayMode) + } } diff --git a/relay/channel/minimax/tts.go b/relay/channel/minimax/tts.go new file mode 100644 index 000000000..4a52d2145 --- /dev/null +++ b/relay/channel/minimax/tts.go @@ -0,0 +1,194 @@ +package minimax + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" +) + +type MiniMaxTTSRequest struct { + Model string `json:"model"` + Text string `json:"text"` + Stream bool `json:"stream,omitempty"` + StreamOptions *StreamOptions `json:"stream_options,omitempty"` + VoiceSetting VoiceSetting `json:"voice_setting"` + PronunciationDict *PronunciationDict `json:"pronunciation_dict,omitempty"` + AudioSetting *AudioSetting `json:"audio_setting,omitempty"` + TimbreWeights []TimbreWeight `json:"timbre_weights,omitempty"` + LanguageBoost string `json:"language_boost,omitempty"` + VoiceModify *VoiceModify `json:"voice_modify,omitempty"` + SubtitleEnable bool `json:"subtitle_enable,omitempty"` + OutputFormat string `json:"output_format,omitempty"` + AigcWatermark bool `json:"aigc_watermark,omitempty"` +} + +type StreamOptions struct { + ExcludeAggregatedAudio bool `json:"exclude_aggregated_audio,omitempty"` +} + +type VoiceSetting struct { + VoiceID string `json:"voice_id"` + Speed float64 `json:"speed,omitempty"` + Vol float64 `json:"vol,omitempty"` + Pitch int `json:"pitch,omitempty"` + Emotion string `json:"emotion,omitempty"` + TextNormalization bool `json:"text_normalization,omitempty"` + LatexRead bool `json:"latex_read,omitempty"` +} + +type PronunciationDict struct { + Tone []string `json:"tone,omitempty"` +} + +type AudioSetting struct { + SampleRate int `json:"sample_rate,omitempty"` + Bitrate int `json:"bitrate,omitempty"` + Format string `json:"format,omitempty"` + Channel int `json:"channel,omitempty"` + ForceCbr bool `json:"force_cbr,omitempty"` +} + +type TimbreWeight struct { + VoiceID string `json:"voice_id"` + Weight int `json:"weight"` +} + +type VoiceModify struct { + Pitch int `json:"pitch,omitempty"` + Intensity int `json:"intensity,omitempty"` + Timbre int `json:"timbre,omitempty"` + SoundEffects string `json:"sound_effects,omitempty"` +} + +type MiniMaxTTSResponse struct { + Data MiniMaxTTSData `json:"data"` + ExtraInfo MiniMaxExtraInfo `json:"extra_info"` + TraceID string `json:"trace_id"` + BaseResp MiniMaxBaseResp `json:"base_resp"` +} + +type MiniMaxTTSData struct { + Audio string `json:"audio"` + Status int `json:"status"` +} + +type MiniMaxExtraInfo struct { + UsageCharacters int64 `json:"usage_characters"` +} + +type MiniMaxBaseResp struct { + StatusCode int64 `json:"status_code"` + StatusMsg string `json:"status_msg"` +} + +func getContentTypeByFormat(format string) string { + contentTypeMap := map[string]string{ + "mp3": "audio/mpeg", + "wav": "audio/wav", + "flac": "audio/flac", + "aac": "audio/aac", + "pcm": "audio/pcm", + } + if ct, ok := contentTypeMap[format]; ok { + return ct + } + return "audio/mpeg" // default to mp3 +} + +func handleTTSResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { + body, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return nil, types.NewErrorWithStatusCode( + fmt.Errorf("failed to read minimax response: %w", readErr), + types.ErrorCodeReadResponseBodyFailed, + http.StatusInternalServerError, + ) + } + defer resp.Body.Close() + + // Parse response + var minimaxResp MiniMaxTTSResponse + if unmarshalErr := json.Unmarshal(body, &minimaxResp); unmarshalErr != nil { + return nil, types.NewErrorWithStatusCode( + fmt.Errorf("failed to unmarshal minimax TTS response: %w", unmarshalErr), + types.ErrorCodeBadResponseBody, + http.StatusInternalServerError, + ) + } + + // Check base_resp status code + if minimaxResp.BaseResp.StatusCode != 0 { + return nil, types.NewErrorWithStatusCode( + fmt.Errorf("minimax TTS error: %d - %s", minimaxResp.BaseResp.StatusCode, minimaxResp.BaseResp.StatusMsg), + types.ErrorCodeBadResponse, + http.StatusBadRequest, + ) + } + + // Check if we have audio data + if minimaxResp.Data.Audio == "" { + return nil, types.NewErrorWithStatusCode( + fmt.Errorf("no audio data in minimax TTS response"), + types.ErrorCodeBadResponse, + http.StatusBadRequest, + ) + } + + if strings.HasPrefix(minimaxResp.Data.Audio, "http") { + c.Redirect(http.StatusFound, minimaxResp.Data.Audio) + } else { + // Handle hex-encoded audio data + audioData, decodeErr := hex.DecodeString(minimaxResp.Data.Audio) + if decodeErr != nil { + return nil, types.NewErrorWithStatusCode( + fmt.Errorf("failed to decode hex audio data: %w", decodeErr), + types.ErrorCodeBadResponse, + http.StatusInternalServerError, + ) + } + + // Determine content type - default to mp3 + contentType := "audio/mpeg" + + c.Data(http.StatusOK, contentType, audioData) + } + + usage = &dto.Usage{ + PromptTokens: info.PromptTokens, + CompletionTokens: 0, + TotalTokens: int(minimaxResp.ExtraInfo.UsageCharacters), + } + + return usage, nil +} + +func handleChatCompletionResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { + body, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return nil, types.NewErrorWithStatusCode( + errors.New("failed to read minimax response"), + types.ErrorCodeReadResponseBodyFailed, + http.StatusInternalServerError, + ) + } + defer resp.Body.Close() + + // Set response headers + for key, values := range resp.Header { + for _, value := range values { + c.Header(key, value) + } + } + + c.Data(resp.StatusCode, "application/json", body) + return nil, nil +} diff --git a/relay/channel/mistral/adaptor.go b/relay/channel/mistral/adaptor.go index f98ff8698..88d72e0fc 100644 --- a/relay/channel/mistral/adaptor.go +++ b/relay/channel/mistral/adaptor.go @@ -4,11 +4,12 @@ import ( "errors" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/mistral/text.go b/relay/channel/mistral/text.go index aa9257811..a6d48f68b 100644 --- a/relay/channel/mistral/text.go +++ b/relay/channel/mistral/text.go @@ -1,9 +1,10 @@ package mistral import ( - "one-api/common" - "one-api/dto" "regexp" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" ) var mistralToolCallIdRegexp = regexp.MustCompile("^[a-zA-Z0-9]{9}$") diff --git a/relay/channel/mokaai/adaptor.go b/relay/channel/mokaai/adaptor.go index f9da685f2..f50c1e6be 100644 --- a/relay/channel/mokaai/adaptor.go +++ b/relay/channel/mokaai/adaptor.go @@ -5,13 +5,14 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - relaycommon "one-api/relay/common" - "one-api/relay/constant" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/mokaai/relay-mokaai.go b/relay/channel/mokaai/relay-mokaai.go index d91aceb3d..4949ed643 100644 --- a/relay/channel/mokaai/relay-mokaai.go +++ b/relay/channel/mokaai/relay-mokaai.go @@ -4,11 +4,12 @@ import ( "encoding/json" "io" "net/http" - "one-api/common" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/service" - "one-api/types" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/moonshot/adaptor.go b/relay/channel/moonshot/adaptor.go index f24976bb3..f02cb5e87 100644 --- a/relay/channel/moonshot/adaptor.go +++ b/relay/channel/moonshot/adaptor.go @@ -5,13 +5,14 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/claude" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/relay/constant" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/claude" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/ollama/adaptor.go b/relay/channel/ollama/adaptor.go index d6b5b697e..a3013e2fb 100644 --- a/relay/channel/ollama/adaptor.go +++ b/relay/channel/ollama/adaptor.go @@ -4,12 +4,14 @@ import ( "errors" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - relayconstant "one-api/relay/constant" - "one-api/types" + "strings" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) @@ -18,7 +20,6 @@ type Adaptor struct { } func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) { - //TODO implement me return nil, errors.New("not implemented") } @@ -31,16 +32,15 @@ func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayIn openaiRequest.(*dto.GeneralOpenAIRequest).StreamOptions = &dto.StreamOptions{ IncludeUsage: true, } - return requestOpenAI2Ollama(c, openaiRequest.(*dto.GeneralOpenAIRequest)) + // map to ollama chat request (Claude -> OpenAI -> Ollama chat) + return openAIChatToOllamaChat(c, openaiRequest.(*dto.GeneralOpenAIRequest)) } func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { - //TODO implement me return nil, errors.New("not implemented") } func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { - //TODO implement me return nil, errors.New("not implemented") } @@ -48,15 +48,13 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) { } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { - if info.RelayFormat == types.RelayFormatClaude { - return info.ChannelBaseUrl + "/v1/chat/completions", nil - } - switch info.RelayMode { - case relayconstant.RelayModeEmbeddings: + if info.RelayMode == relayconstant.RelayModeEmbeddings { return info.ChannelBaseUrl + "/api/embed", nil - default: - return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, info.RequestURLPath, info.ChannelType), nil } + if strings.Contains(info.RequestURLPath, "/v1/completions") || info.RelayMode == relayconstant.RelayModeCompletions { + return info.ChannelBaseUrl + "/api/generate", nil + } + return info.ChannelBaseUrl + "/api/chat", nil } func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { @@ -69,7 +67,11 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn if request == nil { return nil, errors.New("request is nil") } - return requestOpenAI2Ollama(c, request) + // decide generate or chat + if strings.Contains(info.RequestURLPath, "/v1/completions") || info.RelayMode == relayconstant.RelayModeCompletions { + return openAIToGenerate(c, request) + } + return openAIChatToOllamaChat(c, request) } func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { @@ -81,7 +83,6 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela } func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) { - // TODO implement me return nil, errors.New("not implemented") } @@ -92,15 +93,13 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { switch info.RelayMode { case relayconstant.RelayModeEmbeddings: - usage, err = ollamaEmbeddingHandler(c, info, resp) + return ollamaEmbeddingHandler(c, info, resp) default: if info.IsStream { - usage, err = openai.OaiStreamHandler(c, info, resp) - } else { - usage, err = openai.OpenaiHandler(c, info, resp) + return ollamaStreamHandler(c, info, resp) } + return ollamaChatHandler(c, info, resp) } - return } func (a *Adaptor) GetModelList() []string { diff --git a/relay/channel/ollama/dto.go b/relay/channel/ollama/dto.go index 317c2a4a1..2434a4cbc 100644 --- a/relay/channel/ollama/dto.go +++ b/relay/channel/ollama/dto.go @@ -2,48 +2,68 @@ package ollama import ( "encoding/json" - "one-api/dto" ) -type OllamaRequest struct { - Model string `json:"model,omitempty"` - Messages []dto.Message `json:"messages,omitempty"` - Stream bool `json:"stream,omitempty"` - Temperature *float64 `json:"temperature,omitempty"` - Seed float64 `json:"seed,omitempty"` - Topp float64 `json:"top_p,omitempty"` - TopK int `json:"top_k,omitempty"` - Stop any `json:"stop,omitempty"` - MaxTokens uint `json:"max_tokens,omitempty"` - Tools []dto.ToolCallRequest `json:"tools,omitempty"` - ResponseFormat any `json:"response_format,omitempty"` - FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` - PresencePenalty float64 `json:"presence_penalty,omitempty"` - Suffix any `json:"suffix,omitempty"` - StreamOptions *dto.StreamOptions `json:"stream_options,omitempty"` - Prompt any `json:"prompt,omitempty"` - Think json.RawMessage `json:"think,omitempty"` +type OllamaChatMessage struct { + Role string `json:"role"` + Content string `json:"content,omitempty"` + Images []string `json:"images,omitempty"` + ToolCalls []OllamaToolCall `json:"tool_calls,omitempty"` + ToolName string `json:"tool_name,omitempty"` + Thinking json.RawMessage `json:"thinking,omitempty"` } -type Options struct { - Seed int `json:"seed,omitempty"` - Temperature *float64 `json:"temperature,omitempty"` - TopK int `json:"top_k,omitempty"` - TopP float64 `json:"top_p,omitempty"` - FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` - PresencePenalty float64 `json:"presence_penalty,omitempty"` - NumPredict int `json:"num_predict,omitempty"` - NumCtx int `json:"num_ctx,omitempty"` +type OllamaToolFunction struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Parameters interface{} `json:"parameters,omitempty"` +} + +type OllamaTool struct { + Type string `json:"type"` + Function OllamaToolFunction `json:"function"` +} + +type OllamaToolCall struct { + Function struct { + Name string `json:"name"` + Arguments interface{} `json:"arguments"` + } `json:"function"` +} + +type OllamaChatRequest struct { + Model string `json:"model"` + Messages []OllamaChatMessage `json:"messages"` + Tools interface{} `json:"tools,omitempty"` + Format interface{} `json:"format,omitempty"` + Stream bool `json:"stream,omitempty"` + Options map[string]any `json:"options,omitempty"` + KeepAlive interface{} `json:"keep_alive,omitempty"` + Think json.RawMessage `json:"think,omitempty"` +} + +type OllamaGenerateRequest struct { + Model string `json:"model"` + Prompt string `json:"prompt,omitempty"` + Suffix string `json:"suffix,omitempty"` + Images []string `json:"images,omitempty"` + Format interface{} `json:"format,omitempty"` + Stream bool `json:"stream,omitempty"` + Options map[string]any `json:"options,omitempty"` + KeepAlive interface{} `json:"keep_alive,omitempty"` + Think json.RawMessage `json:"think,omitempty"` } type OllamaEmbeddingRequest struct { - Model string `json:"model,omitempty"` - Input []string `json:"input"` - Options *Options `json:"options,omitempty"` + Model string `json:"model"` + Input interface{} `json:"input"` + Options map[string]any `json:"options,omitempty"` + Dimensions int `json:"dimensions,omitempty"` } type OllamaEmbeddingResponse struct { - Error string `json:"error,omitempty"` - Model string `json:"model"` - Embedding [][]float64 `json:"embeddings,omitempty"` + Error string `json:"error,omitempty"` + Model string `json:"model"` + Embeddings [][]float64 `json:"embeddings"` + PromptEvalCount int `json:"prompt_eval_count,omitempty"` } diff --git a/relay/channel/ollama/relay-ollama.go b/relay/channel/ollama/relay-ollama.go index 27c67b4ec..9c05b1357 100644 --- a/relay/channel/ollama/relay-ollama.go +++ b/relay/channel/ollama/relay-ollama.go @@ -1,134 +1,285 @@ package ollama import ( + "encoding/json" "fmt" "io" "net/http" - "one-api/common" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/service" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) -func requestOpenAI2Ollama(c *gin.Context, request *dto.GeneralOpenAIRequest) (*OllamaRequest, error) { - messages := make([]dto.Message, 0, len(request.Messages)) - for _, message := range request.Messages { - if !message.IsStringContent() { - mediaMessages := message.ParseContent() - for j, mediaMessage := range mediaMessages { - if mediaMessage.Type == dto.ContentTypeImageURL { - imageUrl := mediaMessage.GetImageMedia() - // check if not base64 - if strings.HasPrefix(imageUrl.Url, "http") { - fileData, err := service.GetFileBase64FromUrl(c, imageUrl.Url, "formatting image for Ollama") - if err != nil { - return nil, err - } - imageUrl.Url = fmt.Sprintf("data:%s;base64,%s", fileData.MimeType, fileData.Base64Data) - } - mediaMessage.ImageUrl = imageUrl - mediaMessages[j] = mediaMessage +func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*OllamaChatRequest, error) { + chatReq := &OllamaChatRequest{ + Model: r.Model, + Stream: r.Stream, + Options: map[string]any{}, + Think: r.Think, + } + if r.ResponseFormat != nil { + if r.ResponseFormat.Type == "json" { + chatReq.Format = "json" + } else if r.ResponseFormat.Type == "json_schema" { + if len(r.ResponseFormat.JsonSchema) > 0 { + var schema any + _ = json.Unmarshal(r.ResponseFormat.JsonSchema, &schema) + chatReq.Format = schema + } + } + } + + // options mapping + if r.Temperature != nil { + chatReq.Options["temperature"] = r.Temperature + } + if r.TopP != 0 { + chatReq.Options["top_p"] = r.TopP + } + if r.TopK != 0 { + chatReq.Options["top_k"] = r.TopK + } + if r.FrequencyPenalty != 0 { + chatReq.Options["frequency_penalty"] = r.FrequencyPenalty + } + if r.PresencePenalty != 0 { + chatReq.Options["presence_penalty"] = r.PresencePenalty + } + if r.Seed != 0 { + chatReq.Options["seed"] = int(r.Seed) + } + if mt := r.GetMaxTokens(); mt != 0 { + chatReq.Options["num_predict"] = int(mt) + } + + if r.Stop != nil { + switch v := r.Stop.(type) { + case string: + chatReq.Options["stop"] = []string{v} + case []string: + chatReq.Options["stop"] = v + case []any: + arr := make([]string, 0, len(v)) + for _, i := range v { + if s, ok := i.(string); ok { + arr = append(arr, s) } } - message.SetMediaContent(mediaMessages) + if len(arr) > 0 { + chatReq.Options["stop"] = arr + } } - messages = append(messages, dto.Message{ - Role: message.Role, - Content: message.Content, - ToolCalls: message.ToolCalls, - ToolCallId: message.ToolCallId, - }) } - str, ok := request.Stop.(string) - var Stop []string - if ok { - Stop = []string{str} - } else { - Stop, _ = request.Stop.([]string) + + if len(r.Tools) > 0 { + tools := make([]OllamaTool, 0, len(r.Tools)) + for _, t := range r.Tools { + tools = append(tools, OllamaTool{Type: "function", Function: OllamaToolFunction{Name: t.Function.Name, Description: t.Function.Description, Parameters: t.Function.Parameters}}) + } + chatReq.Tools = tools } - ollamaRequest := &OllamaRequest{ - Model: request.Model, - Messages: messages, - Stream: request.Stream, - Temperature: request.Temperature, - Seed: request.Seed, - Topp: request.TopP, - TopK: request.TopK, - Stop: Stop, - Tools: request.Tools, - MaxTokens: request.GetMaxTokens(), - ResponseFormat: request.ResponseFormat, - FrequencyPenalty: request.FrequencyPenalty, - PresencePenalty: request.PresencePenalty, - Prompt: request.Prompt, - StreamOptions: request.StreamOptions, - Suffix: request.Suffix, + + chatReq.Messages = make([]OllamaChatMessage, 0, len(r.Messages)) + for _, m := range r.Messages { + var textBuilder strings.Builder + var images []string + if m.IsStringContent() { + textBuilder.WriteString(m.StringContent()) + } else { + parts := m.ParseContent() + for _, part := range parts { + if part.Type == dto.ContentTypeImageURL { + img := part.GetImageMedia() + if img != nil && img.Url != "" { + var base64Data string + if strings.HasPrefix(img.Url, "http") { + fileData, err := service.GetFileBase64FromUrl(c, img.Url, "fetch image for ollama chat") + if err != nil { + return nil, err + } + base64Data = fileData.Base64Data + } else if strings.HasPrefix(img.Url, "data:") { + if idx := strings.Index(img.Url, ","); idx != -1 && idx+1 < len(img.Url) { + base64Data = img.Url[idx+1:] + } + } else { + base64Data = img.Url + } + if base64Data != "" { + images = append(images, base64Data) + } + } + } else if part.Type == dto.ContentTypeText { + textBuilder.WriteString(part.Text) + } + } + } + cm := OllamaChatMessage{Role: m.Role, Content: textBuilder.String()} + if len(images) > 0 { + cm.Images = images + } + if m.Role == "tool" && m.Name != nil { + cm.ToolName = *m.Name + } + if m.ToolCalls != nil && len(m.ToolCalls) > 0 { + parsed := m.ParseToolCalls() + if len(parsed) > 0 { + calls := make([]OllamaToolCall, 0, len(parsed)) + for _, tc := range parsed { + var args interface{} + if tc.Function.Arguments != "" { + _ = json.Unmarshal([]byte(tc.Function.Arguments), &args) + } + if args == nil { + args = map[string]any{} + } + oc := OllamaToolCall{} + oc.Function.Name = tc.Function.Name + oc.Function.Arguments = args + calls = append(calls, oc) + } + cm.ToolCalls = calls + } + } + chatReq.Messages = append(chatReq.Messages, cm) } - ollamaRequest.Think = request.Think - return ollamaRequest, nil + return chatReq, nil } -func requestOpenAI2Embeddings(request dto.EmbeddingRequest) *OllamaEmbeddingRequest { - return &OllamaEmbeddingRequest{ - Model: request.Model, - Input: request.ParseInput(), - Options: &Options{ - Seed: int(request.Seed), - Temperature: request.Temperature, - TopP: request.TopP, - FrequencyPenalty: request.FrequencyPenalty, - PresencePenalty: request.PresencePenalty, - }, +// openAIToGenerate converts OpenAI completions request to Ollama generate +func openAIToGenerate(c *gin.Context, r *dto.GeneralOpenAIRequest) (*OllamaGenerateRequest, error) { + gen := &OllamaGenerateRequest{ + Model: r.Model, + Stream: r.Stream, + Options: map[string]any{}, + Think: r.Think, } + // Prompt may be in r.Prompt (string or []any) + if r.Prompt != nil { + switch v := r.Prompt.(type) { + case string: + gen.Prompt = v + case []any: + var sb strings.Builder + for _, it := range v { + if s, ok := it.(string); ok { + sb.WriteString(s) + } + } + gen.Prompt = sb.String() + default: + gen.Prompt = fmt.Sprintf("%v", r.Prompt) + } + } + if r.Suffix != nil { + if s, ok := r.Suffix.(string); ok { + gen.Suffix = s + } + } + if r.ResponseFormat != nil { + if r.ResponseFormat.Type == "json" { + gen.Format = "json" + } else if r.ResponseFormat.Type == "json_schema" { + var schema any + _ = json.Unmarshal(r.ResponseFormat.JsonSchema, &schema) + gen.Format = schema + } + } + if r.Temperature != nil { + gen.Options["temperature"] = r.Temperature + } + if r.TopP != 0 { + gen.Options["top_p"] = r.TopP + } + if r.TopK != 0 { + gen.Options["top_k"] = r.TopK + } + if r.FrequencyPenalty != 0 { + gen.Options["frequency_penalty"] = r.FrequencyPenalty + } + if r.PresencePenalty != 0 { + gen.Options["presence_penalty"] = r.PresencePenalty + } + if r.Seed != 0 { + gen.Options["seed"] = int(r.Seed) + } + if mt := r.GetMaxTokens(); mt != 0 { + gen.Options["num_predict"] = int(mt) + } + if r.Stop != nil { + switch v := r.Stop.(type) { + case string: + gen.Options["stop"] = []string{v} + case []string: + gen.Options["stop"] = v + case []any: + arr := make([]string, 0, len(v)) + for _, i := range v { + if s, ok := i.(string); ok { + arr = append(arr, s) + } + } + if len(arr) > 0 { + gen.Options["stop"] = arr + } + } + } + return gen, nil +} + +func requestOpenAI2Embeddings(r dto.EmbeddingRequest) *OllamaEmbeddingRequest { + opts := map[string]any{} + if r.Temperature != nil { + opts["temperature"] = r.Temperature + } + if r.TopP != 0 { + opts["top_p"] = r.TopP + } + if r.FrequencyPenalty != 0 { + opts["frequency_penalty"] = r.FrequencyPenalty + } + if r.PresencePenalty != 0 { + opts["presence_penalty"] = r.PresencePenalty + } + if r.Seed != 0 { + opts["seed"] = int(r.Seed) + } + if r.Dimensions != 0 { + opts["dimensions"] = r.Dimensions + } + input := r.ParseInput() + if len(input) == 1 { + return &OllamaEmbeddingRequest{Model: r.Model, Input: input[0], Options: opts, Dimensions: r.Dimensions} + } + return &OllamaEmbeddingRequest{Model: r.Model, Input: input, Options: opts, Dimensions: r.Dimensions} } func ollamaEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) { - var ollamaEmbeddingResponse OllamaEmbeddingResponse - responseBody, err := io.ReadAll(resp.Body) + var oResp OllamaEmbeddingResponse + body, err := io.ReadAll(resp.Body) if err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } service.CloseResponseBodyGracefully(resp) - err = common.Unmarshal(responseBody, &ollamaEmbeddingResponse) - if err != nil { + if err = common.Unmarshal(body, &oResp); err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } - if ollamaEmbeddingResponse.Error != "" { - return nil, types.NewOpenAIError(fmt.Errorf("ollama error: %s", ollamaEmbeddingResponse.Error), types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + if oResp.Error != "" { + return nil, types.NewOpenAIError(fmt.Errorf("ollama error: %s", oResp.Error), types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } - flattenedEmbeddings := flattenEmbeddings(ollamaEmbeddingResponse.Embedding) - data := make([]dto.OpenAIEmbeddingResponseItem, 0, 1) - data = append(data, dto.OpenAIEmbeddingResponseItem{ - Embedding: flattenedEmbeddings, - Object: "embedding", - }) - usage := &dto.Usage{ - TotalTokens: info.PromptTokens, - CompletionTokens: 0, - PromptTokens: info.PromptTokens, + data := make([]dto.OpenAIEmbeddingResponseItem, 0, len(oResp.Embeddings)) + for i, emb := range oResp.Embeddings { + data = append(data, dto.OpenAIEmbeddingResponseItem{Index: i, Object: "embedding", Embedding: emb}) } - embeddingResponse := &dto.OpenAIEmbeddingResponse{ - Object: "list", - Data: data, - Model: info.UpstreamModelName, - Usage: *usage, - } - doResponseBody, err := common.Marshal(embeddingResponse) - if err != nil { - return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) - } - service.IOCopyBytesGracefully(c, resp, doResponseBody) + usage := &dto.Usage{PromptTokens: oResp.PromptEvalCount, CompletionTokens: 0, TotalTokens: oResp.PromptEvalCount} + embResp := &dto.OpenAIEmbeddingResponse{Object: "list", Data: data, Model: info.UpstreamModelName, Usage: *usage} + out, _ := common.Marshal(embResp) + service.IOCopyBytesGracefully(c, resp, out) return usage, nil } - -func flattenEmbeddings(embeddings [][]float64) []float64 { - flattened := []float64{} - for _, row := range embeddings { - flattened = append(flattened, row...) - } - return flattened -} diff --git a/relay/channel/ollama/stream.go b/relay/channel/ollama/stream.go new file mode 100644 index 000000000..2a264b27e --- /dev/null +++ b/relay/channel/ollama/stream.go @@ -0,0 +1,300 @@ +package ollama + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/logger" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + + "github.com/gin-gonic/gin" +) + +type ollamaChatStreamChunk struct { + Model string `json:"model"` + CreatedAt string `json:"created_at"` + // chat + Message *struct { + Role string `json:"role"` + Content string `json:"content"` + Thinking json.RawMessage `json:"thinking"` + ToolCalls []struct { + Function struct { + Name string `json:"name"` + Arguments interface{} `json:"arguments"` + } `json:"function"` + } `json:"tool_calls"` + } `json:"message"` + // generate + Response string `json:"response"` + Done bool `json:"done"` + DoneReason string `json:"done_reason"` + TotalDuration int64 `json:"total_duration"` + LoadDuration int64 `json:"load_duration"` + PromptEvalCount int `json:"prompt_eval_count"` + EvalCount int `json:"eval_count"` + PromptEvalDuration int64 `json:"prompt_eval_duration"` + EvalDuration int64 `json:"eval_duration"` +} + +func toUnix(ts string) int64 { + if ts == "" { + return time.Now().Unix() + } + // try time.RFC3339 or with nanoseconds + t, err := time.Parse(time.RFC3339Nano, ts) + if err != nil { + t2, err2 := time.Parse(time.RFC3339, ts) + if err2 == nil { + return t2.Unix() + } + return time.Now().Unix() + } + return t.Unix() +} + +func ollamaStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) { + if resp == nil || resp.Body == nil { + return nil, types.NewOpenAIError(fmt.Errorf("empty response"), types.ErrorCodeBadResponse, http.StatusBadRequest) + } + defer service.CloseResponseBodyGracefully(resp) + + helper.SetEventStreamHeaders(c) + scanner := bufio.NewScanner(resp.Body) + usage := &dto.Usage{} + var model = info.UpstreamModelName + var responseId = common.GetUUID() + var created = time.Now().Unix() + var toolCallIndex int + start := helper.GenerateStartEmptyResponse(responseId, created, model, nil) + if data, err := common.Marshal(start); err == nil { + _ = helper.StringData(c, string(data)) + } + + for scanner.Scan() { + line := scanner.Text() + line = strings.TrimSpace(line) + if line == "" { + continue + } + var chunk ollamaChatStreamChunk + if err := json.Unmarshal([]byte(line), &chunk); err != nil { + logger.LogError(c, "ollama stream json decode error: "+err.Error()+" line="+line) + return usage, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } + if chunk.Model != "" { + model = chunk.Model + } + created = toUnix(chunk.CreatedAt) + + if !chunk.Done { + // delta content + var content string + if chunk.Message != nil { + content = chunk.Message.Content + } else { + content = chunk.Response + } + delta := dto.ChatCompletionsStreamResponse{ + Id: responseId, + Object: "chat.completion.chunk", + Created: created, + Model: model, + Choices: []dto.ChatCompletionsStreamResponseChoice{{ + Index: 0, + Delta: dto.ChatCompletionsStreamResponseChoiceDelta{Role: "assistant"}, + }}, + } + if content != "" { + delta.Choices[0].Delta.SetContentString(content) + } + if chunk.Message != nil && len(chunk.Message.Thinking) > 0 { + raw := strings.TrimSpace(string(chunk.Message.Thinking)) + if raw != "" && raw != "null" { + // Unmarshal the JSON string to get the actual content without quotes + var thinkingContent string + if err := json.Unmarshal(chunk.Message.Thinking, &thinkingContent); err == nil { + delta.Choices[0].Delta.SetReasoningContent(thinkingContent) + } else { + // Fallback to raw string if it's not a JSON string + delta.Choices[0].Delta.SetReasoningContent(raw) + } + } + } + // tool calls + if chunk.Message != nil && len(chunk.Message.ToolCalls) > 0 { + delta.Choices[0].Delta.ToolCalls = make([]dto.ToolCallResponse, 0, len(chunk.Message.ToolCalls)) + for _, tc := range chunk.Message.ToolCalls { + // arguments -> string + argBytes, _ := json.Marshal(tc.Function.Arguments) + toolId := fmt.Sprintf("call_%d", toolCallIndex) + tr := dto.ToolCallResponse{ID: toolId, Type: "function", Function: dto.FunctionResponse{Name: tc.Function.Name, Arguments: string(argBytes)}} + tr.SetIndex(toolCallIndex) + toolCallIndex++ + delta.Choices[0].Delta.ToolCalls = append(delta.Choices[0].Delta.ToolCalls, tr) + } + } + if data, err := common.Marshal(delta); err == nil { + _ = helper.StringData(c, string(data)) + } + continue + } + // done frame + // finalize once and break loop + usage.PromptTokens = chunk.PromptEvalCount + usage.CompletionTokens = chunk.EvalCount + usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens + finishReason := chunk.DoneReason + if finishReason == "" { + finishReason = "stop" + } + // emit stop delta + if stop := helper.GenerateStopResponse(responseId, created, model, finishReason); stop != nil { + if data, err := common.Marshal(stop); err == nil { + _ = helper.StringData(c, string(data)) + } + } + // emit usage frame + if final := helper.GenerateFinalUsageResponse(responseId, created, model, *usage); final != nil { + if data, err := common.Marshal(final); err == nil { + _ = helper.StringData(c, string(data)) + } + } + // send [DONE] + helper.Done(c) + break + } + if err := scanner.Err(); err != nil && err != io.EOF { + logger.LogError(c, "ollama stream scan error: "+err.Error()) + } + return usage, nil +} + +// non-stream handler for chat/generate +func ollamaChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError) + } + service.CloseResponseBodyGracefully(resp) + raw := string(body) + if common.DebugEnabled { + println("ollama non-stream raw resp:", raw) + } + + lines := strings.Split(raw, "\n") + var ( + aggContent strings.Builder + reasoningBuilder strings.Builder + lastChunk ollamaChatStreamChunk + parsedAny bool + ) + for _, ln := range lines { + ln = strings.TrimSpace(ln) + if ln == "" { + continue + } + var ck ollamaChatStreamChunk + if err := json.Unmarshal([]byte(ln), &ck); err != nil { + if len(lines) == 1 { + return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } + continue + } + parsedAny = true + lastChunk = ck + if ck.Message != nil && len(ck.Message.Thinking) > 0 { + raw := strings.TrimSpace(string(ck.Message.Thinking)) + if raw != "" && raw != "null" { + // Unmarshal the JSON string to get the actual content without quotes + var thinkingContent string + if err := json.Unmarshal(ck.Message.Thinking, &thinkingContent); err == nil { + reasoningBuilder.WriteString(thinkingContent) + } else { + // Fallback to raw string if it's not a JSON string + reasoningBuilder.WriteString(raw) + } + } + } + if ck.Message != nil && ck.Message.Content != "" { + aggContent.WriteString(ck.Message.Content) + } else if ck.Response != "" { + aggContent.WriteString(ck.Response) + } + } + + if !parsedAny { + var single ollamaChatStreamChunk + if err := json.Unmarshal(body, &single); err != nil { + return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } + lastChunk = single + if single.Message != nil { + if len(single.Message.Thinking) > 0 { + raw := strings.TrimSpace(string(single.Message.Thinking)) + if raw != "" && raw != "null" { + // Unmarshal the JSON string to get the actual content without quotes + var thinkingContent string + if err := json.Unmarshal(single.Message.Thinking, &thinkingContent); err == nil { + reasoningBuilder.WriteString(thinkingContent) + } else { + // Fallback to raw string if it's not a JSON string + reasoningBuilder.WriteString(raw) + } + } + } + aggContent.WriteString(single.Message.Content) + } else { + aggContent.WriteString(single.Response) + } + } + + model := lastChunk.Model + if model == "" { + model = info.UpstreamModelName + } + created := toUnix(lastChunk.CreatedAt) + usage := &dto.Usage{PromptTokens: lastChunk.PromptEvalCount, CompletionTokens: lastChunk.EvalCount, TotalTokens: lastChunk.PromptEvalCount + lastChunk.EvalCount} + content := aggContent.String() + finishReason := lastChunk.DoneReason + if finishReason == "" { + finishReason = "stop" + } + + msg := dto.Message{Role: "assistant", Content: contentPtr(content)} + if rc := reasoningBuilder.String(); rc != "" { + msg.ReasoningContent = rc + } + full := dto.OpenAITextResponse{ + Id: common.GetUUID(), + Model: model, + Object: "chat.completion", + Created: created, + Choices: []dto.OpenAITextResponseChoice{{ + Index: 0, + Message: msg, + FinishReason: finishReason, + }}, + Usage: *usage, + } + out, _ := common.Marshal(full) + service.IOCopyBytesGracefully(c, resp, out) + return usage, nil +} + +func contentPtr(s string) *string { + if s == "" { + return nil + } + return &s +} diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index 1d8286a43..4e41c866a 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -9,23 +9,24 @@ import ( "mime/multipart" "net/http" "net/textproto" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/ai360" - "one-api/relay/channel/lingyiwanwu" - "one-api/relay/channel/minimax" - "one-api/relay/channel/openrouter" - "one-api/relay/channel/xinference" - relaycommon "one-api/relay/common" - "one-api/relay/common_handler" - relayconstant "one-api/relay/constant" - "one-api/service" - "one-api/types" "path/filepath" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "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/openrouter" + "github.com/QuantumNous/new-api/relay/channel/xinference" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/common_handler" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) @@ -160,8 +161,8 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { requestURL = fmt.Sprintf("/openai/realtime?deployment=%s&api-version=%s", model_, apiVersion) } return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, requestURL, info.ChannelType), nil - case constant.ChannelTypeMiniMax: - return minimax.GetRequestURL(info) + //case constant.ChannelTypeMiniMax: + // return minimax.GetRequestURL(info) case constant.ChannelTypeCustom: url := info.ChannelBaseUrl url = strings.Replace(url, "{model}", info.UpstreamModelName, -1) @@ -598,8 +599,8 @@ func (a *Adaptor) GetModelList() []string { return ai360.ModelList case constant.ChannelTypeLingYiWanWu: return lingyiwanwu.ModelList - case constant.ChannelTypeMiniMax: - return minimax.ModelList + //case constant.ChannelTypeMiniMax: + // return minimax.ModelList case constant.ChannelTypeXinference: return xinference.ModelList case constant.ChannelTypeOpenRouter: @@ -615,8 +616,8 @@ func (a *Adaptor) GetChannelName() string { return ai360.ChannelName case constant.ChannelTypeLingYiWanWu: return lingyiwanwu.ChannelName - case constant.ChannelTypeMiniMax: - return minimax.ChannelName + //case constant.ChannelTypeMiniMax: + // return minimax.ChannelName case constant.ChannelTypeXinference: return xinference.ChannelName case constant.ChannelTypeOpenRouter: diff --git a/relay/channel/openai/helper.go b/relay/channel/openai/helper.go index e84f6cc4a..69731d4d2 100644 --- a/relay/channel/openai/helper.go +++ b/relay/channel/openai/helper.go @@ -2,16 +2,17 @@ package openai import ( "encoding/json" - "one-api/common" - "one-api/dto" - "one-api/logger" - relaycommon "one-api/relay/common" - relayconstant "one-api/relay/constant" - "one-api/relay/helper" - "one-api/service" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/logger" + 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/types" + "github.com/samber/lo" "github.com/gin-gonic/gin" diff --git a/relay/channel/openai/relay-openai.go b/relay/channel/openai/relay-openai.go index 4b13a7df1..f24dec812 100644 --- a/relay/channel/openai/relay-openai.go +++ b/relay/channel/openai/relay-openai.go @@ -8,18 +8,20 @@ import ( "math" "mime/multipart" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/logger" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" "os" "path/filepath" "strings" - "one-api/types" + "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/relay/channel/openrouter" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + + "github.com/QuantumNous/new-api/types" "github.com/bytedance/gopkg/util/gopool" "github.com/gin-gonic/gin" @@ -162,13 +164,10 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re if !containStreamUsage { usage = service.ResponseText2Usage(responseTextBuilder.String(), info.UpstreamModelName, info.PromptTokens) usage.CompletionTokens += toolCount * 7 - } else { - if info.ChannelType == constant.ChannelTypeDeepSeek { - if usage.PromptCacheHitTokens != 0 { - usage.PromptTokensDetails.CachedTokens = usage.PromptCacheHitTokens - } - } } + + applyUsagePostProcessing(info, usage, nil) + HandleFinalResponse(c, info, lastStreamData, responseId, createAt, model, systemFingerprint, usage, containStreamUsage) return usage, nil @@ -185,10 +184,27 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo if common.DebugEnabled { println("upstream response body:", string(responseBody)) } + // Unmarshal to simpleResponse + if info.ChannelType == constant.ChannelTypeOpenRouter && info.ChannelOtherSettings.IsOpenRouterEnterprise() { + // 尝试解析为 openrouter enterprise + var enterpriseResponse openrouter.OpenRouterEnterpriseResponse + err = common.Unmarshal(responseBody, &enterpriseResponse) + if err != nil { + return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } + if enterpriseResponse.Success { + responseBody = enterpriseResponse.Data + } else { + logger.LogError(c, fmt.Sprintf("openrouter enterprise response success=false, data: %s", enterpriseResponse.Data)) + return nil, types.NewOpenAIError(fmt.Errorf("openrouter response success=false"), types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } + } + err = common.Unmarshal(responseBody, &simpleResponse) if err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } + if oaiError := simpleResponse.GetOpenAIError(); oaiError != nil && oaiError.Type != "" { return nil, types.WithOpenAIError(*oaiError, resp.StatusCode) } @@ -215,6 +231,8 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo usageModified = true } + applyUsagePostProcessing(info, &simpleResponse.Usage, responseBody) + switch info.RelayFormat { case types.RelayFormatOpenAI: if usageModified { @@ -613,5 +631,60 @@ func OpenaiHandlerWithUsage(c *gin.Context, info *relaycommon.RelayInfo, resp *h usageResp.PromptTokensDetails.ImageTokens += usageResp.InputTokensDetails.ImageTokens usageResp.PromptTokensDetails.TextTokens += usageResp.InputTokensDetails.TextTokens } + applyUsagePostProcessing(info, &usageResp.Usage, responseBody) return &usageResp.Usage, nil } + +func applyUsagePostProcessing(info *relaycommon.RelayInfo, usage *dto.Usage, responseBody []byte) { + if info == nil || usage == nil { + return + } + + switch info.ChannelType { + case constant.ChannelTypeDeepSeek: + if usage.PromptTokensDetails.CachedTokens == 0 && usage.PromptCacheHitTokens != 0 { + usage.PromptTokensDetails.CachedTokens = usage.PromptCacheHitTokens + } + case constant.ChannelTypeZhipu_v4: + if usage.PromptTokensDetails.CachedTokens == 0 { + if usage.InputTokensDetails != nil && usage.InputTokensDetails.CachedTokens > 0 { + usage.PromptTokensDetails.CachedTokens = usage.InputTokensDetails.CachedTokens + } else if cachedTokens, ok := extractCachedTokensFromBody(responseBody); ok { + usage.PromptTokensDetails.CachedTokens = cachedTokens + } else if usage.PromptCacheHitTokens > 0 { + usage.PromptTokensDetails.CachedTokens = usage.PromptCacheHitTokens + } + } + } +} + +func extractCachedTokensFromBody(body []byte) (int, bool) { + if len(body) == 0 { + return 0, false + } + + var payload struct { + Usage struct { + PromptTokensDetails struct { + CachedTokens *int `json:"cached_tokens"` + } `json:"prompt_tokens_details"` + CachedTokens *int `json:"cached_tokens"` + PromptCacheHitTokens *int `json:"prompt_cache_hit_tokens"` + } `json:"usage"` + } + + if err := json.Unmarshal(body, &payload); err != nil { + return 0, false + } + + if payload.Usage.PromptTokensDetails.CachedTokens != nil { + return *payload.Usage.PromptTokensDetails.CachedTokens, true + } + if payload.Usage.CachedTokens != nil { + return *payload.Usage.CachedTokens, true + } + if payload.Usage.PromptCacheHitTokens != nil { + return *payload.Usage.PromptCacheHitTokens, true + } + return 0, false +} diff --git a/relay/channel/openai/relay_responses.go b/relay/channel/openai/relay_responses.go index 85938a771..3f8eb69a8 100644 --- a/relay/channel/openai/relay_responses.go +++ b/relay/channel/openai/relay_responses.go @@ -4,15 +4,16 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/dto" - "one-api/logger" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/logger" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) @@ -115,7 +116,11 @@ func OaiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp if streamResponse.Item != nil { switch streamResponse.Item.Type { case dto.BuildInCallWebSearchCall: - info.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview].CallCount++ + if info != nil && info.ResponsesUsageInfo != nil && info.ResponsesUsageInfo.BuiltInTools != nil { + if webSearchTool, exists := info.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview]; exists && webSearchTool != nil { + webSearchTool.CallCount++ + } + } } } } diff --git a/relay/channel/openrouter/dto.go b/relay/channel/openrouter/dto.go index 607f495bf..a32499852 100644 --- a/relay/channel/openrouter/dto.go +++ b/relay/channel/openrouter/dto.go @@ -1,5 +1,7 @@ package openrouter +import "encoding/json" + type RequestReasoning struct { // One of the following (not both): Effort string `json:"effort,omitempty"` // Can be "high", "medium", or "low" (OpenAI-style) @@ -7,3 +9,8 @@ type RequestReasoning struct { // Optional: Default is false. All models support this. Exclude bool `json:"exclude,omitempty"` // Set to true to exclude reasoning tokens from response } + +type OpenRouterEnterpriseResponse struct { + Data json.RawMessage `json:"data"` + Success bool `json:"success"` +} diff --git a/relay/channel/palm/adaptor.go b/relay/channel/palm/adaptor.go index 2a022a1b8..f06be051c 100644 --- a/relay/channel/palm/adaptor.go +++ b/relay/channel/palm/adaptor.go @@ -5,11 +5,12 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - relaycommon "one-api/relay/common" - "one-api/service" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/palm/dto.go b/relay/channel/palm/dto.go index b8a48e73c..47ca3fc66 100644 --- a/relay/channel/palm/dto.go +++ b/relay/channel/palm/dto.go @@ -1,6 +1,6 @@ package palm -import "one-api/dto" +import "github.com/QuantumNous/new-api/dto" type PaLMChatMessage struct { Author string `json:"author"` diff --git a/relay/channel/palm/relay-palm.go b/relay/channel/palm/relay-palm.go index 3a6ec2f4b..abfb92c0e 100644 --- a/relay/channel/palm/relay-palm.go +++ b/relay/channel/palm/relay-palm.go @@ -4,13 +4,14 @@ import ( "encoding/json" "io" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/types" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/perplexity/adaptor.go b/relay/channel/perplexity/adaptor.go index 8ab9c8547..43e39b12e 100644 --- a/relay/channel/perplexity/adaptor.go +++ b/relay/channel/perplexity/adaptor.go @@ -5,11 +5,12 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) @@ -22,10 +23,9 @@ func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dt return nil, errors.New("not implemented") } -func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { - //TODO implement me - panic("implement me") - return nil, nil +func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) { + adaptor := openai.Adaptor{} + return adaptor.ConvertClaudeRequest(c, info, req) } func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { @@ -80,11 +80,8 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request } func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { - if info.IsStream { - usage, err = openai.OaiStreamHandler(c, info, resp) - } else { - usage, err = openai.OpenaiHandler(c, info, resp) - } + adaptor := openai.Adaptor{} + usage, err = adaptor.DoResponse(c, resp, info) return } diff --git a/relay/channel/perplexity/constants.go b/relay/channel/perplexity/constants.go index f9f030e09..d37c3b877 100644 --- a/relay/channel/perplexity/constants.go +++ b/relay/channel/perplexity/constants.go @@ -2,6 +2,7 @@ package perplexity var ModelList = []string{ "llama-3-sonar-small-32k-chat", "llama-3-sonar-small-32k-online", "llama-3-sonar-large-32k-chat", "llama-3-sonar-large-32k-online", "llama-3-8b-instruct", "llama-3-70b-instruct", "mixtral-8x7b-instruct", + "sonar", "sonar-pro", "sonar-reasoning", } var ChannelName = "perplexity" diff --git a/relay/channel/perplexity/relay-perplexity.go b/relay/channel/perplexity/relay-perplexity.go index 7ebadd0f9..b07bed68a 100644 --- a/relay/channel/perplexity/relay-perplexity.go +++ b/relay/channel/perplexity/relay-perplexity.go @@ -1,6 +1,6 @@ package perplexity -import "one-api/dto" +import "github.com/QuantumNous/new-api/dto" func requestOpenAI2Perplexity(request dto.GeneralOpenAIRequest) *dto.GeneralOpenAIRequest { messages := make([]dto.Message, 0, len(request.Messages)) @@ -11,11 +11,18 @@ func requestOpenAI2Perplexity(request dto.GeneralOpenAIRequest) *dto.GeneralOpen }) } return &dto.GeneralOpenAIRequest{ - Model: request.Model, - Stream: request.Stream, - Messages: messages, - Temperature: request.Temperature, - TopP: request.TopP, - MaxTokens: request.GetMaxTokens(), + Model: request.Model, + Stream: request.Stream, + Messages: messages, + Temperature: request.Temperature, + TopP: request.TopP, + MaxTokens: request.GetMaxTokens(), + FrequencyPenalty: request.FrequencyPenalty, + PresencePenalty: request.PresencePenalty, + SearchDomainFilter: request.SearchDomainFilter, + SearchRecencyFilter: request.SearchRecencyFilter, + ReturnImages: request.ReturnImages, + ReturnRelatedQuestions: request.ReturnRelatedQuestions, + SearchMode: request.SearchMode, } } diff --git a/relay/channel/siliconflow/adaptor.go b/relay/channel/siliconflow/adaptor.go index 4c176c088..daffff180 100644 --- a/relay/channel/siliconflow/adaptor.go +++ b/relay/channel/siliconflow/adaptor.go @@ -5,12 +5,14 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/relay/constant" - "one-api/types" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) @@ -34,8 +36,27 @@ func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInf } func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { - adaptor := openai.Adaptor{} - return adaptor.ConvertImageRequest(c, info, request) + // 解析extra到SFImageRequest里,以填入SiliconFlow特殊字段。若失败重建一个空的。 + sfRequest := &SFImageRequest{} + extra, err := common.Marshal(request.Extra) + if err == nil { + err = common.Unmarshal(extra, sfRequest) + if err != nil { + sfRequest = &SFImageRequest{} + } + } + + sfRequest.Model = request.Model + sfRequest.Prompt = request.Prompt + // 优先使用image_size/batch_size,否则使用OpenAI标准的size/n + if sfRequest.ImageSize == "" { + sfRequest.ImageSize = request.Size + } + if sfRequest.BatchSize == 0 { + sfRequest.BatchSize = request.N + } + + return sfRequest, nil } func (a *Adaptor) Init(info *relaycommon.RelayInfo) { @@ -50,6 +71,8 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { return fmt.Sprintf("%s/v1/chat/completions", info.ChannelBaseUrl), nil } else if info.RelayMode == constant.RelayModeCompletions { return fmt.Sprintf("%s/v1/completions", info.ChannelBaseUrl), nil + } else if info.RelayMode == constant.RelayModeImagesGenerations { + return fmt.Sprintf("%s/v1/images/generations", info.ChannelBaseUrl), nil } return fmt.Sprintf("%s/v1/chat/completions", info.ChannelBaseUrl), nil } @@ -61,6 +84,16 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel } func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { + // SiliconFlow requires messages array for FIM requests, even if client doesn't send it + if (request.Prefix != nil || request.Suffix != nil) && len(request.Messages) == 0 { + // Add an empty user message to satisfy SiliconFlow's requirement + request.Messages = []dto.Message{ + { + Role: "user", + Content: "", + }, + } + } return request, nil } @@ -91,6 +124,8 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom fallthrough case constant.RelayModeChatCompletions: fallthrough + case constant.RelayModeImagesGenerations: + fallthrough default: if info.IsStream { usage, err = openai.OaiStreamHandler(c, info, resp) diff --git a/relay/channel/siliconflow/dto.go b/relay/channel/siliconflow/dto.go index add0fd070..100975107 100644 --- a/relay/channel/siliconflow/dto.go +++ b/relay/channel/siliconflow/dto.go @@ -1,6 +1,6 @@ package siliconflow -import "one-api/dto" +import "github.com/QuantumNous/new-api/dto" type SFTokens struct { InputTokens int `json:"input_tokens"` @@ -15,3 +15,18 @@ type SFRerankResponse struct { Results []dto.RerankResponseResult `json:"results"` Meta SFMeta `json:"meta"` } + +type SFImageRequest struct { + Model string `json:"model"` + Prompt string `json:"prompt"` + NegativePrompt string `json:"negative_prompt,omitempty"` + ImageSize string `json:"image_size,omitempty"` + BatchSize uint `json:"batch_size,omitempty"` + Seed uint64 `json:"seed,omitempty"` + NumInferenceSteps uint `json:"num_inference_steps,omitempty"` + GuidanceScale float64 `json:"guidance_scale,omitempty"` + Cfg float64 `json:"cfg,omitempty"` + Image string `json:"image,omitempty"` + Image2 string `json:"image2,omitempty"` + Image3 string `json:"image3,omitempty"` +} diff --git a/relay/channel/siliconflow/relay-siliconflow.go b/relay/channel/siliconflow/relay-siliconflow.go index b21faccb7..421731fb1 100644 --- a/relay/channel/siliconflow/relay-siliconflow.go +++ b/relay/channel/siliconflow/relay-siliconflow.go @@ -4,10 +4,11 @@ import ( "encoding/json" "io" "net/http" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/service" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/submodel/adaptor.go b/relay/channel/submodel/adaptor.go new file mode 100644 index 000000000..58b2a3b29 --- /dev/null +++ b/relay/channel/submodel/adaptor.go @@ -0,0 +1,87 @@ +package submodel + +import ( + "errors" + "io" + "net/http" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" + + "github.com/gin-gonic/gin" +) + +type Adaptor struct { +} + +func (a *Adaptor) ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error) { + return nil, errors.New("submodel channel: endpoint not supported") +} + +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + return nil, errors.New("submodel channel: endpoint not supported") +} + +func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { + return nil, errors.New("submodel channel: endpoint not supported") +} + +func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { + return nil, errors.New("submodel channel: endpoint not supported") +} + +func (a *Adaptor) Init(info *relaycommon.RelayInfo) { +} + +func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, info.RequestURLPath, info.ChannelType), nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { + channel.SetupApiRequestHeader(info, c, req) + req.Set("Authorization", "Bearer "+info.ApiKey) + return nil +} + +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + return request, nil +} + +func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { + return nil, errors.New("submodel channel: endpoint not supported") +} + +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + return nil, errors.New("submodel channel: endpoint not supported") +} + +func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) { + return nil, errors.New("submodel channel: endpoint not supported") +} + +func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { + return channel.DoApiRequest(a, c, info, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { + if info.IsStream { + usage, err = openai.OaiStreamHandler(c, info, resp) + } else { + usage, err = openai.OpenaiHandler(c, info, resp) + } + return +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return ChannelName +} diff --git a/relay/channel/submodel/constants.go b/relay/channel/submodel/constants.go new file mode 100644 index 000000000..72d6fee31 --- /dev/null +++ b/relay/channel/submodel/constants.go @@ -0,0 +1,16 @@ +package submodel + +var ModelList = []string{ + "NousResearch/Hermes-4-405B-FP8", + "Qwen/Qwen3-235B-A22B-Thinking-2507", + "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8", + "Qwen/Qwen3-235B-A22B-Instruct-2507", + "zai-org/GLM-4.5-FP8", + "openai/gpt-oss-120b", + "deepseek-ai/DeepSeek-R1-0528", + "deepseek-ai/DeepSeek-R1", + "deepseek-ai/DeepSeek-V3-0324", + "deepseek-ai/DeepSeek-V3.1", +} + +const ChannelName = "submodel" diff --git a/relay/channel/task/doubao/adaptor.go b/relay/channel/task/doubao/adaptor.go new file mode 100644 index 000000000..1bacb2019 --- /dev/null +++ b/relay/channel/task/doubao/adaptor.go @@ -0,0 +1,249 @@ +package doubao + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// ============================ +// Request / Response structures +// ============================ + +type ContentItem struct { + Type string `json:"type"` // "text" or "image_url" + Text string `json:"text,omitempty"` // for text type + ImageURL *ImageURL `json:"image_url,omitempty"` // for image_url type +} + +type ImageURL struct { + URL string `json:"url"` +} + +type requestPayload struct { + Model string `json:"model"` + Content []ContentItem `json:"content"` +} + +type responsePayload struct { + ID string `json:"id"` // task_id +} + +type responseTask struct { + ID string `json:"id"` + Model string `json:"model"` + Status string `json:"status"` + Content struct { + VideoURL string `json:"video_url"` + } `json:"content"` + Seed int `json:"seed"` + Resolution string `json:"resolution"` + Duration int `json:"duration"` + Ratio string `json:"ratio"` + FramesPerSecond int `json:"framespersecond"` + Usage struct { + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + } `json:"usage"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +// ============================ +// Adaptor implementation +// ============================ + +type TaskAdaptor struct { + ChannelType int + apiKey string + baseURL string +} + +func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { + a.ChannelType = info.ChannelType + a.baseURL = info.ChannelBaseUrl + a.apiKey = info.ApiKey +} + +// ValidateRequestAndSetAction parses body, validates fields and sets default action. +func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) { + // Accept only POST /v1/video/generations as "generate" action. + return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate) +} + +// BuildRequestURL constructs the upstream URL. +func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) { + return fmt.Sprintf("%s/api/v3/contents/generations/tasks", a.baseURL), nil +} + +// BuildRequestHeader sets required headers. +func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error { + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+a.apiKey) + return nil +} + +// BuildRequestBody converts request into Doubao specific format. +func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) { + v, exists := c.Get("task_request") + if !exists { + return nil, fmt.Errorf("request not found in context") + } + req := v.(relaycommon.TaskSubmitReq) + + body, err := a.convertToRequestPayload(&req) + if err != nil { + return nil, errors.Wrap(err, "convert request payload failed") + } + data, err := json.Marshal(body) + if err != nil { + return nil, err + } + return bytes.NewReader(data), nil +} + +// DoRequest delegates to common helper. +func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) { + return channel.DoTaskApiRequest(a, c, info, requestBody) +} + +// DoResponse handles upstream response, returns taskID etc. +func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) + return + } + _ = resp.Body.Close() + + // Parse Doubao response + var dResp responsePayload + if err := json.Unmarshal(responseBody, &dResp); err != nil { + taskErr = service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError) + return + } + + if dResp.ID == "" { + taskErr = service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, gin.H{"task_id": dResp.ID}) + return dResp.ID, responseBody, nil +} + +// FetchTask fetch task status +func (a *TaskAdaptor) FetchTask(baseUrl, key string, body map[string]any) (*http.Response, error) { + taskID, ok := body["task_id"].(string) + if !ok { + return nil, fmt.Errorf("invalid task_id") + } + + uri := fmt.Sprintf("%s/api/v3/contents/generations/tasks/%s", baseUrl, taskID) + + req, err := http.NewRequest(http.MethodGet, uri, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+key) + + return service.GetHttpClient().Do(req) +} + +func (a *TaskAdaptor) GetModelList() []string { + return ModelList +} + +func (a *TaskAdaptor) GetChannelName() string { + return ChannelName +} + +func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (*requestPayload, error) { + r := requestPayload{ + Model: req.Model, + Content: []ContentItem{}, + } + + // Add text prompt + if req.Prompt != "" { + r.Content = append(r.Content, ContentItem{ + Type: "text", + Text: req.Prompt, + }) + } + + // Add images if present + if req.HasImage() { + for _, imgURL := range req.Images { + r.Content = append(r.Content, ContentItem{ + Type: "image_url", + ImageURL: &ImageURL{ + URL: imgURL, + }, + }) + } + } + + // TODO: Add support for additional parameters from metadata + // such as ratio, duration, seed, etc. + // metadata := req.Metadata + // if metadata != nil { + // // Parse and apply metadata parameters + // } + + return &r, nil +} + +func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) { + resTask := responseTask{} + if err := json.Unmarshal(respBody, &resTask); err != nil { + return nil, errors.Wrap(err, "unmarshal task result failed") + } + + taskResult := relaycommon.TaskInfo{ + Code: 0, + } + + // Map Doubao status to internal status + switch resTask.Status { + case "pending", "queued": + taskResult.Status = model.TaskStatusQueued + taskResult.Progress = "10%" + case "processing": + taskResult.Status = model.TaskStatusInProgress + taskResult.Progress = "50%" + case "succeeded": + taskResult.Status = model.TaskStatusSuccess + taskResult.Progress = "100%" + taskResult.Url = resTask.Content.VideoURL + // 解析 usage 信息用于按倍率计费 + taskResult.CompletionTokens = resTask.Usage.CompletionTokens + taskResult.TotalTokens = resTask.Usage.TotalTokens + case "failed": + taskResult.Status = model.TaskStatusFailure + taskResult.Progress = "100%" + taskResult.Reason = "task failed" + default: + // Unknown status, treat as processing + taskResult.Status = model.TaskStatusInProgress + taskResult.Progress = "30%" + } + + return &taskResult, nil +} diff --git a/relay/channel/task/doubao/constants.go b/relay/channel/task/doubao/constants.go new file mode 100644 index 000000000..74b416c6d --- /dev/null +++ b/relay/channel/task/doubao/constants.go @@ -0,0 +1,9 @@ +package doubao + +var ModelList = []string{ + "doubao-seedance-1-0-pro-250528", + "doubao-seedance-1-0-lite-t2v", + "doubao-seedance-1-0-lite-i2v", +} + +var ChannelName = "doubao-video" diff --git a/relay/channel/task/jimeng/adaptor.go b/relay/channel/task/jimeng/adaptor.go index a2545a273..f47143d8a 100644 --- a/relay/channel/task/jimeng/adaptor.go +++ b/relay/channel/task/jimeng/adaptor.go @@ -4,25 +4,28 @@ import ( "bytes" "crypto/hmac" "crypto/sha256" + "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/url" - "one-api/model" "sort" "strings" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/model" + "github.com/gin-gonic/gin" "github.com/pkg/errors" - "one-api/constant" - "one-api/dto" - "one-api/relay/channel" - relaycommon "one-api/relay/common" - "one-api/service" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" ) // ============================ @@ -63,6 +66,11 @@ type responseTask struct { TimeElapsed string `json:"time_elapsed"` } +const ( + // 即梦限制单个文件最大4.7MB https://www.volcengine.com/docs/85621/1747301 + MaxFileSize int64 = 4*1024*1024 + 700*1024 // 4.7MB (4MB + 724KB) +) + // ============================ // Adaptor implementation // ============================ @@ -88,7 +96,6 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { // ValidateRequestAndSetAction parses body, validates fields and sets default action. func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) { - // Accept only POST /v1/video/generations as "generate" action. return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate) } @@ -112,13 +119,49 @@ func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info return nil } -// BuildRequestBody converts request into Jimeng specific format. func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) { v, exists := c.Get("task_request") if !exists { return nil, fmt.Errorf("request not found in context") } - req := v.(relaycommon.TaskSubmitReq) + req, ok := v.(relaycommon.TaskSubmitReq) + if !ok { + return nil, fmt.Errorf("invalid request type in context") + } + // 支持openai sdk的图片上传方式 + if mf, err := c.MultipartForm(); err == nil { + if files, exists := mf.File["input_reference"]; exists && len(files) > 0 { + if len(files) == 1 { + info.Action = constant.TaskActionGenerate + } else if len(files) > 1 { + info.Action = constant.TaskActionFirstTailGenerate + } + + // 将上传的文件转换为base64格式 + var images []string + + for _, fileHeader := range files { + // 检查文件大小 + if fileHeader.Size > MaxFileSize { + return nil, fmt.Errorf("文件 %s 大小超过限制,最大允许 %d MB", fileHeader.Filename, MaxFileSize/(1024*1024)) + } + + file, err := fileHeader.Open() + if err != nil { + continue + } + fileBytes, err := io.ReadAll(file) + file.Close() + if err != nil { + continue + } + // 将文件内容转换为base64 + base64Str := base64.StdEncoding.EncodeToString(fileBytes) + images = append(images, base64Str) + } + req.Images = images + } + } body, err := a.convertToRequestPayload(&req) if err != nil { @@ -157,7 +200,12 @@ func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *rela return } - c.JSON(http.StatusOK, gin.H{"task_id": jResp.Data.TaskID}) + ov := dto.NewOpenAIVideo() + ov.ID = jResp.Data.TaskID + ov.TaskID = jResp.Data.TaskID + ov.CreatedAt = time.Now().Unix() + ov.Model = info.OriginModelName + c.JSON(http.StatusOK, ov) return jResp.Data.TaskID, responseBody, nil } @@ -358,10 +406,10 @@ func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (* // 即梦视频3.0 ReqKey转换 // https://www.volcengine.com/docs/85621/1792707 if strings.Contains(r.ReqKey, "jimeng_v30") { - if len(r.ImageUrls) > 1 { + if len(req.Images) > 1 { // 多张图片:首尾帧生成 r.ReqKey = strings.Replace(r.ReqKey, "jimeng_v30", "jimeng_i2v_first_tail_v30", 1) - } else if len(r.ImageUrls) == 1 { + } else if len(req.Images) == 1 { // 单张图片:图生视频 r.ReqKey = strings.Replace(r.ReqKey, "jimeng_v30", "jimeng_i2v_first_v30", 1) } else { @@ -399,6 +447,31 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e return &taskResult, nil } +func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) ([]byte, error) { + var jimengResp responseTask + if err := json.Unmarshal(originTask.Data, &jimengResp); err != nil { + return nil, errors.Wrap(err, "unmarshal jimeng task data failed") + } + + openAIVideo := dto.NewOpenAIVideo() + openAIVideo.ID = originTask.TaskID + openAIVideo.Status = originTask.Status.ToVideoStatus() + openAIVideo.SetProgressStr(originTask.Progress) + openAIVideo.SetMetadata("url", jimengResp.Data.VideoUrl) + openAIVideo.CreatedAt = originTask.CreatedAt + openAIVideo.CompletedAt = originTask.UpdatedAt + + if jimengResp.Code != 10000 { + openAIVideo.Error = &dto.OpenAIVideoError{ + Message: jimengResp.Message, + Code: fmt.Sprintf("%d", jimengResp.Code), + } + } + + jsonData, _ := common.Marshal(openAIVideo) + return jsonData, nil +} + func isNewAPIRelay(apiKey string) bool { return strings.HasPrefix(apiKey, "sk-") } diff --git a/relay/channel/task/kling/adaptor.go b/relay/channel/task/kling/adaptor.go index fec3396ae..c1bbd9d59 100644 --- a/relay/channel/task/kling/adaptor.go +++ b/relay/channel/task/kling/adaptor.go @@ -6,21 +6,23 @@ import ( "fmt" "io" "net/http" - "one-api/model" "strings" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/model" + "github.com/samber/lo" "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v5" "github.com/pkg/errors" - "one-api/constant" - "one-api/dto" - "one-api/relay/channel" - relaycommon "one-api/relay/common" - "one-api/service" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" ) // ============================ @@ -187,8 +189,12 @@ func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *rela taskErr = service.TaskErrorWrapperLocal(fmt.Errorf(kResp.Message), "task_failed", http.StatusBadRequest) return } - kResp.TaskId = kResp.Data.TaskId - c.JSON(http.StatusOK, kResp) + ov := dto.NewOpenAIVideo() + ov.ID = kResp.Data.TaskId + ov.TaskID = kResp.Data.TaskId + ov.CreatedAt = time.Now().Unix() + ov.Model = info.OriginModelName + c.JSON(http.StatusOK, ov) return kResp.Data.TaskId, responseBody, nil } @@ -303,14 +309,6 @@ func (a *TaskAdaptor) createJWTToken() (string, error) { return a.createJWTTokenWithKey(a.apiKey) } -//func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) { -// parts := strings.Split(apiKey, "|") -// if len(parts) != 2 { -// return "", fmt.Errorf("invalid API key format, expected 'access_key,secret_key'") -// } -// return a.createJWTTokenWithKey(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])) -//} - func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) { if isNewAPIRelay(apiKey) { return apiKey, nil // new api relay @@ -369,3 +367,36 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e func isNewAPIRelay(apiKey string) bool { return strings.HasPrefix(apiKey, "sk-") } + +func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) ([]byte, error) { + var klingResp responsePayload + if err := json.Unmarshal(originTask.Data, &klingResp); err != nil { + return nil, errors.Wrap(err, "unmarshal kling task data failed") + } + + openAIVideo := dto.NewOpenAIVideo() + openAIVideo.ID = originTask.TaskID + openAIVideo.Status = originTask.Status.ToVideoStatus() + openAIVideo.SetProgressStr(originTask.Progress) + openAIVideo.CreatedAt = klingResp.Data.CreatedAt + openAIVideo.CompletedAt = klingResp.Data.UpdatedAt + + if len(klingResp.Data.TaskResult.Videos) > 0 { + video := klingResp.Data.TaskResult.Videos[0] + if video.Url != "" { + openAIVideo.SetMetadata("url", video.Url) + } + if video.Duration != "" { + openAIVideo.Seconds = video.Duration + } + } + + if klingResp.Code != 0 && klingResp.Message != "" { + openAIVideo.Error = &dto.OpenAIVideoError{ + Message: klingResp.Message, + Code: fmt.Sprintf("%d", klingResp.Code), + } + } + jsonData, _ := common.Marshal(openAIVideo) + return jsonData, nil +} diff --git a/relay/channel/task/sora/adaptor.go b/relay/channel/task/sora/adaptor.go new file mode 100644 index 000000000..17aec18f0 --- /dev/null +++ b/relay/channel/task/sora/adaptor.go @@ -0,0 +1,190 @@ +package sora + +import ( + "bytes" + "fmt" + "io" + "net/http" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/setting/system_setting" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +// ============================ +// Request / Response structures +// ============================ + +type ContentItem struct { + Type string `json:"type"` // "text" or "image_url" + Text string `json:"text,omitempty"` // for text type + ImageURL *ImageURL `json:"image_url,omitempty"` // for image_url type +} + +type ImageURL struct { + URL string `json:"url"` +} + +type responseTask struct { + ID string `json:"id"` + TaskID string `json:"task_id,omitempty"` //兼容旧接口 + Object string `json:"object"` + Model string `json:"model"` + Status string `json:"status"` + 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 *struct { + Message string `json:"message"` + Code string `json:"code"` + } `json:"error,omitempty"` +} + +// ============================ +// Adaptor implementation +// ============================ + +type TaskAdaptor struct { + ChannelType int + apiKey string + baseURL string +} + +func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) { + a.ChannelType = info.ChannelType + a.baseURL = info.ChannelBaseUrl + a.apiKey = info.ApiKey +} + +func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto.TaskError) { + return relaycommon.ValidateMultipartDirect(c, info) +} + +func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) { + return fmt.Sprintf("%s/v1/videos", a.baseURL), nil +} + +// BuildRequestHeader sets required headers. +func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error { + req.Header.Set("Authorization", "Bearer "+a.apiKey) + req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) + return nil +} + +func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) { + cachedBody, err := common.GetRequestBody(c) + if err != nil { + return nil, errors.Wrap(err, "get_request_body_failed") + } + return bytes.NewReader(cachedBody), nil +} + +// DoRequest delegates to common helper. +func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) { + return channel.DoTaskApiRequest(a, c, info, requestBody) +} + +// DoResponse handles upstream response, returns taskID etc. +func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, _ *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) + return + } + _ = resp.Body.Close() + + // Parse Sora response + var dResp responseTask + if err := common.Unmarshal(responseBody, &dResp); err != nil { + taskErr = service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError) + return + } + + if dResp.ID == "" { + if dResp.TaskID == "" { + taskErr = service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError) + return + } + dResp.ID = dResp.TaskID + dResp.TaskID = "" + } + + c.JSON(http.StatusOK, dResp) + return dResp.ID, responseBody, nil +} + +// FetchTask fetch task status +func (a *TaskAdaptor) FetchTask(baseUrl, key string, body map[string]any) (*http.Response, error) { + taskID, ok := body["task_id"].(string) + if !ok { + return nil, fmt.Errorf("invalid task_id") + } + + uri := fmt.Sprintf("%s/v1/videos/%s", baseUrl, taskID) + + req, err := http.NewRequest(http.MethodGet, uri, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+key) + + return service.GetHttpClient().Do(req) +} + +func (a *TaskAdaptor) GetModelList() []string { + return ModelList +} + +func (a *TaskAdaptor) GetChannelName() string { + return ChannelName +} + +func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) { + resTask := responseTask{} + if err := common.Unmarshal(respBody, &resTask); err != nil { + return nil, errors.Wrap(err, "unmarshal task result failed") + } + + taskResult := relaycommon.TaskInfo{ + Code: 0, + } + + switch resTask.Status { + case "queued", "pending": + taskResult.Status = model.TaskStatusQueued + case "processing", "in_progress": + taskResult.Status = model.TaskStatusInProgress + case "completed": + taskResult.Status = model.TaskStatusSuccess + taskResult.Url = fmt.Sprintf("%s/v1/videos/%s/content", system_setting.ServerAddress, resTask.ID) + case "failed", "cancelled": + taskResult.Status = model.TaskStatusFailure + if resTask.Error != nil { + taskResult.Reason = resTask.Error.Message + } else { + taskResult.Reason = "task failed" + } + default: + } + if resTask.Progress > 0 && resTask.Progress < 100 { + taskResult.Progress = fmt.Sprintf("%d%%", resTask.Progress) + } + + return &taskResult, nil +} + +func (a *TaskAdaptor) ConvertToOpenAIVideo(task *model.Task) ([]byte, error) { + return task.Data, nil +} diff --git a/relay/channel/task/sora/constants.go b/relay/channel/task/sora/constants.go new file mode 100644 index 000000000..e2f6536ea --- /dev/null +++ b/relay/channel/task/sora/constants.go @@ -0,0 +1,8 @@ +package sora + +var ModelList = []string{ + "sora-2", + "sora-2-pro", +} + +var ChannelName = "sora" diff --git a/relay/channel/task/suno/adaptor.go b/relay/channel/task/suno/adaptor.go index 237513d75..c4858d0c0 100644 --- a/relay/channel/task/suno/adaptor.go +++ b/relay/channel/task/suno/adaptor.go @@ -7,15 +7,16 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/relay/channel" - relaycommon "one-api/relay/common" - "one-api/service" "strings" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/task/vertex/adaptor.go b/relay/channel/task/vertex/adaptor.go index 4a236b2f0..faedebdf2 100644 --- a/relay/channel/task/vertex/adaptor.go +++ b/relay/channel/task/vertex/adaptor.go @@ -7,18 +7,19 @@ import ( "fmt" "io" "net/http" - "one-api/model" "regexp" "strings" + "github.com/QuantumNous/new-api/model" + "github.com/gin-gonic/gin" - "one-api/constant" - "one-api/dto" - "one-api/relay/channel" - vertexcore "one-api/relay/channel/vertex" - relaycommon "one-api/relay/common" - "one-api/service" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + vertexcore "github.com/QuantumNous/new-api/relay/channel/vertex" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" ) // ============================ diff --git a/relay/channel/task/vidu/adaptor.go b/relay/channel/task/vidu/adaptor.go index 358aef583..7ccac2ff2 100644 --- a/relay/channel/task/vidu/adaptor.go +++ b/relay/channel/task/vidu/adaptor.go @@ -6,15 +6,17 @@ import ( "fmt" "io" "net/http" + "time" + "github.com/QuantumNous/new-api/common" "github.com/gin-gonic/gin" - "one-api/constant" - "one-api/dto" - "one-api/model" - "one-api/relay/channel" - relaycommon "one-api/relay/common" - "one-api/service" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" "github.com/pkg/errors" ) @@ -135,7 +137,7 @@ func (a *TaskAdaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, req return channel.DoTaskApiRequest(a, c, info, requestBody) } -func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, _ *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { +func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { responseBody, err := io.ReadAll(resp.Body) if err != nil { taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) @@ -154,7 +156,12 @@ func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, _ *relayco return } - c.JSON(http.StatusOK, vResp) + ov := dto.NewOpenAIVideo() + ov.ID = vResp.TaskId + ov.TaskID = vResp.TaskId + ov.CreatedAt = time.Now().Unix() + ov.Model = info.OriginModelName + c.JSON(http.StatusOK, ov) return vResp.TaskId, responseBody, nil } @@ -256,3 +263,31 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e return taskInfo, nil } + +func (a *TaskAdaptor) ConvertToOpenAIVideo(originTask *model.Task) ([]byte, error) { + var viduResp taskResultResponse + if err := json.Unmarshal(originTask.Data, &viduResp); err != nil { + return nil, errors.Wrap(err, "unmarshal vidu task data failed") + } + + openAIVideo := dto.NewOpenAIVideo() + openAIVideo.ID = originTask.TaskID + openAIVideo.Status = originTask.Status.ToVideoStatus() + openAIVideo.SetProgressStr(originTask.Progress) + openAIVideo.CreatedAt = originTask.CreatedAt + openAIVideo.CompletedAt = originTask.UpdatedAt + + if len(viduResp.Creations) > 0 && viduResp.Creations[0].URL != "" { + openAIVideo.SetMetadata("url", viduResp.Creations[0].URL) + } + + if viduResp.State == "failed" && viduResp.ErrCode != "" { + openAIVideo.Error = &dto.OpenAIVideoError{ + Message: viduResp.ErrCode, + Code: viduResp.ErrCode, + } + } + + jsonData, _ := common.Marshal(openAIVideo) + return jsonData, nil +} diff --git a/relay/channel/tencent/adaptor.go b/relay/channel/tencent/adaptor.go index ab96ecaa3..eb6985537 100644 --- a/relay/channel/tencent/adaptor.go +++ b/relay/channel/tencent/adaptor.go @@ -5,15 +5,16 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/relay/channel" - relaycommon "one-api/relay/common" - "one-api/types" "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/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/tencent/relay-tencent.go b/relay/channel/tencent/relay-tencent.go index f33a275c6..75bc6b71a 100644 --- a/relay/channel/tencent/relay-tencent.go +++ b/relay/channel/tencent/relay-tencent.go @@ -10,17 +10,18 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/types" "strconv" "strings" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/vertex/adaptor.go b/relay/channel/vertex/adaptor.go index a424cb1a4..94ca47657 100644 --- a/relay/channel/vertex/adaptor.go +++ b/relay/channel/vertex/adaptor.go @@ -6,18 +6,19 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/claude" - "one-api/relay/channel/gemini" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/relay/constant" - "one-api/setting/model_setting" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/claude" + "github.com/QuantumNous/new-api/relay/channel/gemini" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/setting/model_setting" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) @@ -37,6 +38,7 @@ var claudeModelMap = map[string]string{ "claude-sonnet-4-20250514": "claude-sonnet-4@20250514", "claude-opus-4-20250514": "claude-opus-4@20250514", "claude-opus-4-1-20250805": "claude-opus-4-1@20250805", + "claude-sonnet-4-5-20250929": "claude-sonnet-4-5@20250929", } const anthropicVersion = "vertex-2023-10-16" @@ -90,7 +92,43 @@ func (a *Adaptor) getRequestUrl(info *relaycommon.RelayInfo, modelName, suffix s } a.AccountCredentials = *adc - if a.RequestMode == RequestModeLlama { + if a.RequestMode == RequestModeGemini { + if region == "global" { + return fmt.Sprintf( + "https://aiplatform.googleapis.com/v1/projects/%s/locations/global/publishers/google/models/%s:%s", + adc.ProjectID, + modelName, + suffix, + ), nil + } else { + return fmt.Sprintf( + "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:%s", + region, + adc.ProjectID, + region, + modelName, + suffix, + ), nil + } + } else if a.RequestMode == RequestModeClaude { + if region == "global" { + return fmt.Sprintf( + "https://aiplatform.googleapis.com/v1/projects/%s/locations/global/publishers/anthropic/models/%s:%s", + adc.ProjectID, + modelName, + suffix, + ), nil + } else { + return fmt.Sprintf( + "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/anthropic/models/%s:%s", + region, + adc.ProjectID, + region, + modelName, + suffix, + ), nil + } + } else if a.RequestMode == RequestModeLlama { return fmt.Sprintf( "https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions", region, @@ -98,42 +136,33 @@ func (a *Adaptor) getRequestUrl(info *relaycommon.RelayInfo, modelName, suffix s region, ), nil } - - if region == "global" { - return fmt.Sprintf( - "https://aiplatform.googleapis.com/v1/projects/%s/locations/global/publishers/google/models/%s:%s", - adc.ProjectID, - modelName, - suffix, - ), nil - } else { - return fmt.Sprintf( - "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:%s", - region, - adc.ProjectID, - region, - modelName, - suffix, - ), nil - } } else { + var keyPrefix string + if strings.HasSuffix(suffix, "?alt=sse") { + keyPrefix = "&" + } else { + keyPrefix = "?" + } if region == "global" { return fmt.Sprintf( - "https://aiplatform.googleapis.com/v1/publishers/google/models/%s:%s?key=%s", + "https://aiplatform.googleapis.com/v1/publishers/google/models/%s:%s%skey=%s", modelName, suffix, + keyPrefix, info.ApiKey, ), nil } else { return fmt.Sprintf( - "https://%s-aiplatform.googleapis.com/v1/publishers/google/models/%s:%s?key=%s", + "https://%s-aiplatform.googleapis.com/v1/publishers/google/models/%s:%s%skey=%s", region, modelName, suffix, + keyPrefix, info.ApiKey, ), nil } } + return "", errors.New("unsupported request mode") } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { @@ -187,7 +216,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel } req.Set("Authorization", "Bearer "+accessToken) } - if a.AccountCredentials.ProjectID != "" { + if a.AccountCredentials.ProjectID != "" { req.Set("x-goog-user-project", a.AccountCredentials.ProjectID) } return nil diff --git a/relay/channel/vertex/dto.go b/relay/channel/vertex/dto.go index 4a571612b..68044ff32 100644 --- a/relay/channel/vertex/dto.go +++ b/relay/channel/vertex/dto.go @@ -1,7 +1,7 @@ package vertex import ( - "one-api/dto" + "github.com/QuantumNous/new-api/dto" ) type VertexAIClaudeRequest struct { diff --git a/relay/channel/vertex/relay-vertex.go b/relay/channel/vertex/relay-vertex.go index f0b84906a..c5103a977 100644 --- a/relay/channel/vertex/relay-vertex.go +++ b/relay/channel/vertex/relay-vertex.go @@ -1,6 +1,6 @@ package vertex -import "one-api/common" +import "github.com/QuantumNous/new-api/common" func GetModelRegion(other string, localModelName string) string { // if other is json string diff --git a/relay/channel/vertex/service_account.go b/relay/channel/vertex/service_account.go index f90d5454d..96ec6b28f 100644 --- a/relay/channel/vertex/service_account.go +++ b/relay/channel/vertex/service_account.go @@ -8,12 +8,13 @@ import ( "errors" "net/http" "net/url" - relaycommon "one-api/relay/common" - "one-api/service" "strings" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + "github.com/bytedance/gopkg/cache/asynccache" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v5" "fmt" "time" diff --git a/relay/channel/volcengine/adaptor.go b/relay/channel/volcengine/adaptor.go index 21d6e1705..c5d9e5dd6 100644 --- a/relay/channel/volcengine/adaptor.go +++ b/relay/channel/volcengine/adaptor.go @@ -9,16 +9,17 @@ import ( "mime/multipart" "net/http" "net/textproto" - channelconstant "one-api/constant" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/relay/constant" - "one-api/types" "path/filepath" "strings" + channelconstant "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) @@ -36,8 +37,57 @@ func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayIn } func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { - //TODO implement me - return nil, errors.New("not implemented") + if info.RelayMode != constant.RelayModeAudioSpeech { + return nil, errors.New("unsupported audio relay mode") + } + + appID, token, err := parseVolcengineAuth(info.ApiKey) + if err != nil { + return nil, err + } + + voiceType := mapVoiceType(request.Voice) + speedRatio := request.Speed + encoding := mapEncoding(request.ResponseFormat) + + c.Set("response_format", encoding) + + volcRequest := VolcengineTTSRequest{ + App: VolcengineTTSApp{ + AppID: appID, + Token: token, + Cluster: "volcano_tts", + }, + User: VolcengineTTSUser{ + UID: "openai_relay_user", + }, + Audio: VolcengineTTSAudio{ + VoiceType: voiceType, + Encoding: encoding, + SpeedRatio: speedRatio, + Rate: 24000, + }, + Request: VolcengineTTSReqInfo{ + ReqID: generateRequestID(), + Text: request.Input, + Operation: "query", + Model: info.OriginModelName, + }, + } + + // 同步扩展字段的厂商自定义metadata + if len(request.Metadata) > 0 { + if err = json.Unmarshal(request.Metadata, &volcRequest); err != nil { + return nil, fmt.Errorf("error unmarshalling metadata to volcengine request: %w", err) + } + } + + jsonData, err := json.Marshal(volcRequest) + if err != nil { + return nil, fmt.Errorf("error marshalling volcengine request: %w", err) + } + + return bytes.NewReader(jsonData), nil } func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { @@ -189,33 +239,56 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) { } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { - // 支持自定义域名,如果未设置则使用默认域名 baseUrl := info.ChannelBaseUrl if baseUrl == "" { baseUrl = channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeVolcEngine] } - switch info.RelayMode { - case constant.RelayModeChatCompletions: + switch info.RelayFormat { + case types.RelayFormatClaude: if strings.HasPrefix(info.UpstreamModelName, "bot") { return fmt.Sprintf("%s/api/v3/bots/chat/completions", baseUrl), nil } return fmt.Sprintf("%s/api/v3/chat/completions", baseUrl), nil - case constant.RelayModeEmbeddings: - return fmt.Sprintf("%s/api/v3/embeddings", baseUrl), nil - case constant.RelayModeImagesGenerations: - return fmt.Sprintf("%s/api/v3/images/generations", baseUrl), nil - case constant.RelayModeImagesEdits: - return fmt.Sprintf("%s/api/v3/images/edits", baseUrl), nil - case constant.RelayModeRerank: - return fmt.Sprintf("%s/api/v3/rerank", baseUrl), nil default: + switch info.RelayMode { + case constant.RelayModeChatCompletions: + if strings.HasPrefix(info.UpstreamModelName, "bot") { + return fmt.Sprintf("%s/api/v3/bots/chat/completions", baseUrl), nil + } + return fmt.Sprintf("%s/api/v3/chat/completions", baseUrl), nil + case constant.RelayModeEmbeddings: + return fmt.Sprintf("%s/api/v3/embeddings", baseUrl), nil + case constant.RelayModeImagesGenerations: + return fmt.Sprintf("%s/api/v3/images/generations", baseUrl), nil + case constant.RelayModeImagesEdits: + return fmt.Sprintf("%s/api/v3/images/edits", baseUrl), nil + case constant.RelayModeRerank: + return fmt.Sprintf("%s/api/v3/rerank", baseUrl), nil + case constant.RelayModeAudioSpeech: + // 只有当 baseUrl 是火山默认的官方Url时才改为官方的的TTS接口,否则走透传的New接口 + if baseUrl == channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeVolcEngine] { + return "https://openspeech.bytedance.com/api/v1/tts", nil + } + return fmt.Sprintf("%s/v1/audio/speech", baseUrl), nil + default: + } } return "", fmt.Errorf("unsupported relay mode: %d", info.RelayMode) } func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { channel.SetupApiRequestHeader(info, c, req) + + if info.RelayMode == constant.RelayModeAudioSpeech { + parts := strings.Split(info.ApiKey, "|") + if len(parts) == 2 { + req.Set("Authorization", "Bearer;"+parts[1]) + } + req.Set("Content-Type", "application/json") + return nil + } + req.Set("Authorization", "Bearer "+info.ApiKey) return nil } @@ -251,6 +324,11 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request } func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { + if info.RelayMode == constant.RelayModeAudioSpeech { + encoding := mapEncoding(c.GetString("response_format")) + return handleTTSResponse(c, resp, info, encoding) + } + adaptor := openai.Adaptor{} usage, err = adaptor.DoResponse(c, resp, info) return diff --git a/relay/channel/volcengine/tts.go b/relay/channel/volcengine/tts.go new file mode 100644 index 000000000..328512845 --- /dev/null +++ b/relay/channel/volcengine/tts.go @@ -0,0 +1,194 @@ +package volcengine + +import ( + "encoding/base64" + "encoding/json" + "errors" + "io" + "net/http" + "strings" + + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type VolcengineTTSRequest struct { + App VolcengineTTSApp `json:"app"` + User VolcengineTTSUser `json:"user"` + Audio VolcengineTTSAudio `json:"audio"` + Request VolcengineTTSReqInfo `json:"request"` +} + +type VolcengineTTSApp struct { + AppID string `json:"appid"` + Token string `json:"token"` + Cluster string `json:"cluster"` +} + +type VolcengineTTSUser struct { + UID string `json:"uid"` +} + +type VolcengineTTSAudio struct { + VoiceType string `json:"voice_type"` + Encoding string `json:"encoding"` + SpeedRatio float64 `json:"speed_ratio"` + Rate int `json:"rate"` + Bitrate int `json:"bitrate,omitempty"` + LoudnessRatio float64 `json:"loudness_ratio,omitempty"` + EnableEmotion bool `json:"enable_emotion,omitempty"` + Emotion string `json:"emotion,omitempty"` + EmotionScale float64 `json:"emotion_scale,omitempty"` + ExplicitLanguage string `json:"explicit_language,omitempty"` + ContextLanguage string `json:"context_language,omitempty"` +} + +type VolcengineTTSReqInfo struct { + ReqID string `json:"reqid"` + Text string `json:"text"` + Operation string `json:"operation"` + Model string `json:"model,omitempty"` + TextType string `json:"text_type,omitempty"` + SilenceDuration float64 `json:"silence_duration,omitempty"` + WithTimestamp interface{} `json:"with_timestamp,omitempty"` + ExtraParam *VolcengineTTSExtraParam `json:"extra_param,omitempty"` +} + +type VolcengineTTSExtraParam struct { + DisableMarkdownFilter bool `json:"disable_markdown_filter,omitempty"` + EnableLatexTn bool `json:"enable_latex_tn,omitempty"` + MuteCutThreshold string `json:"mute_cut_threshold,omitempty"` + MuteCutRemainMs string `json:"mute_cut_remain_ms,omitempty"` + DisableEmojiFilter bool `json:"disable_emoji_filter,omitempty"` + UnsupportedCharRatioThresh float64 `json:"unsupported_char_ratio_thresh,omitempty"` + AigcWatermark bool `json:"aigc_watermark,omitempty"` + CacheConfig *VolcengineTTSCacheConfig `json:"cache_config,omitempty"` +} + +type VolcengineTTSCacheConfig struct { + TextType int `json:"text_type,omitempty"` + UseCache bool `json:"use_cache,omitempty"` +} + +type VolcengineTTSResponse struct { + ReqID string `json:"reqid"` + Code int `json:"code"` + Message string `json:"message"` + Sequence int `json:"sequence"` + Data string `json:"data"` + Addition *VolcengineTTSAdditionInfo `json:"addition,omitempty"` +} + +type VolcengineTTSAdditionInfo struct { + Duration string `json:"duration"` +} + +var openAIToVolcengineVoiceMap = map[string]string{ + "alloy": "zh_male_M392_conversation_wvae_bigtts", + "echo": "zh_male_wenhao_mars_bigtts", + "fable": "zh_female_tianmei_mars_bigtts", + "onyx": "zh_male_zhibei_mars_bigtts", + "nova": "zh_female_shuangkuaisisi_mars_bigtts", + "shimmer": "zh_female_cancan_mars_bigtts", +} + +var responseFormatToEncodingMap = map[string]string{ + "mp3": "mp3", + "opus": "ogg_opus", + "aac": "mp3", + "flac": "mp3", + "wav": "wav", + "pcm": "pcm", +} + +func parseVolcengineAuth(apiKey string) (appID, token string, err error) { + parts := strings.Split(apiKey, "|") + if len(parts) != 2 { + return "", "", errors.New("invalid api key format, expected: appid|access_token") + } + return parts[0], parts[1], nil +} + +func mapVoiceType(openAIVoice string) string { + if voice, ok := openAIToVolcengineVoiceMap[openAIVoice]; ok { + return voice + } + return openAIVoice +} + +func mapEncoding(responseFormat string) string { + if encoding, ok := responseFormatToEncodingMap[responseFormat]; ok { + return encoding + } + return "mp3" +} + +func getContentTypeByEncoding(encoding string) string { + contentTypeMap := map[string]string{ + "mp3": "audio/mpeg", + "ogg_opus": "audio/ogg", + "wav": "audio/wav", + "pcm": "audio/pcm", + } + if ct, ok := contentTypeMap[encoding]; ok { + return ct + } + return "application/octet-stream" +} + +func handleTTSResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, encoding string) (usage any, err *types.NewAPIError) { + body, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return nil, types.NewErrorWithStatusCode( + errors.New("failed to read volcengine response"), + types.ErrorCodeReadResponseBodyFailed, + http.StatusInternalServerError, + ) + } + defer resp.Body.Close() + + var volcResp VolcengineTTSResponse + if unmarshalErr := json.Unmarshal(body, &volcResp); unmarshalErr != nil { + return nil, types.NewErrorWithStatusCode( + errors.New("failed to parse volcengine response"), + types.ErrorCodeBadResponseBody, + http.StatusInternalServerError, + ) + } + + if volcResp.Code != 3000 { + return nil, types.NewErrorWithStatusCode( + errors.New(volcResp.Message), + types.ErrorCodeBadResponse, + http.StatusBadRequest, + ) + } + + audioData, decodeErr := base64.StdEncoding.DecodeString(volcResp.Data) + if decodeErr != nil { + return nil, types.NewErrorWithStatusCode( + errors.New("failed to decode audio data"), + types.ErrorCodeBadResponseBody, + http.StatusInternalServerError, + ) + } + + contentType := getContentTypeByEncoding(encoding) + c.Header("Content-Type", contentType) + c.Data(http.StatusOK, contentType, audioData) + + usage = &dto.Usage{ + PromptTokens: info.PromptTokens, + CompletionTokens: 0, + TotalTokens: info.PromptTokens, + } + + return usage, nil +} + +func generateRequestID() string { + return uuid.New().String() +} diff --git a/relay/channel/xai/adaptor.go b/relay/channel/xai/adaptor.go index d5671ab2f..b130723e7 100644 --- a/relay/channel/xai/adaptor.go +++ b/relay/channel/xai/adaptor.go @@ -4,14 +4,15 @@ import ( "errors" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/types" "strings" - "one-api/relay/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" + + "github.com/QuantumNous/new-api/relay/constant" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/xai/dto.go b/relay/channel/xai/dto.go index 107a980a8..371d62a43 100644 --- a/relay/channel/xai/dto.go +++ b/relay/channel/xai/dto.go @@ -1,6 +1,6 @@ package xai -import "one-api/dto" +import "github.com/QuantumNous/new-api/dto" // ChatCompletionResponse represents the response from XAI chat completion API type ChatCompletionResponse struct { diff --git a/relay/channel/xai/text.go b/relay/channel/xai/text.go index 5cae9c0ae..752d40c18 100644 --- a/relay/channel/xai/text.go +++ b/relay/channel/xai/text.go @@ -4,15 +4,16 @@ import ( "encoding/json" "io" "net/http" - "one-api/common" - "one-api/dto" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/xunfei/adaptor.go b/relay/channel/xunfei/adaptor.go index 7ee76f1ad..686b0cbd2 100644 --- a/relay/channel/xunfei/adaptor.go +++ b/relay/channel/xunfei/adaptor.go @@ -4,12 +4,13 @@ import ( "errors" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - relaycommon "one-api/relay/common" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/channel/xunfei/dto.go b/relay/channel/xunfei/dto.go index c169e5f79..71a40f2d0 100644 --- a/relay/channel/xunfei/dto.go +++ b/relay/channel/xunfei/dto.go @@ -1,6 +1,6 @@ package xunfei -import "one-api/dto" +import "github.com/QuantumNous/new-api/dto" type XunfeiMessage struct { Role string `json:"role"` diff --git a/relay/channel/xunfei/relay-xunfei.go b/relay/channel/xunfei/relay-xunfei.go index 9d5c190fe..b8fbd2958 100644 --- a/relay/channel/xunfei/relay-xunfei.go +++ b/relay/channel/xunfei/relay-xunfei.go @@ -8,14 +8,15 @@ import ( "fmt" "io" "net/url" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/relay/helper" - "one-api/types" "strings" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" "github.com/gorilla/websocket" ) @@ -207,10 +208,6 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap return nil, nil, err } - defer func() { - conn.Close() - }() - data := requestOpenAI2Xunfei(textRequest, appId, domain) err = conn.WriteJSON(data) if err != nil { @@ -220,6 +217,9 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap dataChan := make(chan XunfeiChatResponse) stopChan := make(chan bool) go func() { + defer func() { + conn.Close() + }() for { _, msg, err := conn.ReadMessage() if err != nil { diff --git a/relay/channel/zhipu/adaptor.go b/relay/channel/zhipu/adaptor.go index bd27c90b0..23016fd3b 100644 --- a/relay/channel/zhipu/adaptor.go +++ b/relay/channel/zhipu/adaptor.go @@ -5,10 +5,11 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - relaycommon "one-api/relay/common" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/zhipu/dto.go b/relay/channel/zhipu/dto.go index 2682dd3a4..5ca91362d 100644 --- a/relay/channel/zhipu/dto.go +++ b/relay/channel/zhipu/dto.go @@ -1,8 +1,9 @@ package zhipu import ( - "one-api/dto" "time" + + "github.com/QuantumNous/new-api/dto" ) type ZhipuMessage struct { diff --git a/relay/channel/zhipu/relay-zhipu.go b/relay/channel/zhipu/relay-zhipu.go index 8eb0dcc13..964dff082 100644 --- a/relay/channel/zhipu/relay-zhipu.go +++ b/relay/channel/zhipu/relay-zhipu.go @@ -5,19 +5,20 @@ import ( "encoding/json" "io" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/types" "strings" "sync" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v5" ) // https://open.bigmodel.cn/doc/api#chatglm_std diff --git a/relay/channel/zhipu_4v/adaptor.go b/relay/channel/zhipu_4v/adaptor.go index 37c0c3521..575da7cbf 100644 --- a/relay/channel/zhipu_4v/adaptor.go +++ b/relay/channel/zhipu_4v/adaptor.go @@ -5,13 +5,14 @@ import ( "fmt" "io" "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/claude" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" - relayconstant "one-api/relay/constant" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/claude" + "github.com/QuantumNous/new-api/relay/channel/openai" + relaycommon "github.com/QuantumNous/new-api/relay/common" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/channel/zhipu_4v/dto.go b/relay/channel/zhipu_4v/dto.go index 4d867679c..e5edd0ddf 100644 --- a/relay/channel/zhipu_4v/dto.go +++ b/relay/channel/zhipu_4v/dto.go @@ -1,8 +1,9 @@ package zhipu_4v import ( - "one-api/dto" "time" + + "github.com/QuantumNous/new-api/dto" ) // type ZhipuMessage struct { diff --git a/relay/channel/zhipu_4v/relay-zhipu_v4.go b/relay/channel/zhipu_4v/relay-zhipu_v4.go index aec87dd5d..53e94e14b 100644 --- a/relay/channel/zhipu_4v/relay-zhipu_v4.go +++ b/relay/channel/zhipu_4v/relay-zhipu_v4.go @@ -1,8 +1,9 @@ package zhipu_4v import ( - "one-api/dto" "strings" + + "github.com/QuantumNous/new-api/dto" ) func requestOpenAI2Zhipu(request dto.GeneralOpenAIRequest) *dto.GeneralOpenAIRequest { diff --git a/relay/claude_handler.go b/relay/claude_handler.go index 59d12abe4..c71a5ca2b 100644 --- a/relay/claude_handler.go +++ b/relay/claude_handler.go @@ -5,16 +5,17 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/setting/model_setting" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/setting/model_setting" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) @@ -112,6 +113,12 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) } + // remove disabled fields for Claude API + jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings) + if err != nil { + return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) + } + // apply param override if len(info.ParamOverride) > 0 { jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride) diff --git a/relay/common/override.go b/relay/common/override.go index 212cf7b47..c86d8b34c 100644 --- a/relay/common/override.go +++ b/relay/common/override.go @@ -3,11 +3,12 @@ package common import ( "encoding/json" "fmt" - "github.com/tidwall/gjson" - "github.com/tidwall/sjson" "regexp" "strconv" "strings" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" ) type ConditionOperation struct { diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 99925dc5d..a23e863b6 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -3,14 +3,15 @@ package common import ( "errors" "fmt" - "one-api/common" - "one-api/constant" - "one-api/dto" - relayconstant "one-api/relay/constant" - "one-api/types" "strings" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" "github.com/gorilla/websocket" ) @@ -105,7 +106,8 @@ type RelayInfo struct { UserQuota int RelayFormat types.RelayFormat SendResponseCount int - FinalPreConsumedQuota int // 最终预消耗的配额 + FinalPreConsumedQuota int // 最终预消耗的配额 + IsClaudeBetaQuery bool // /v1/messages?beta=true PriceData types.PriceData @@ -260,6 +262,7 @@ var streamSupportedChannels = map[int]bool{ constant.ChannelTypeXai: true, constant.ChannelTypeDeepSeek: true, constant.ChannelTypeBaiduV2: true, + constant.ChannelTypeZhipu_v4: true, } func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo { @@ -279,6 +282,9 @@ func GenRelayInfoClaude(c *gin.Context, request dto.Request) *RelayInfo { info.ClaudeConvertInfo = &ClaudeConvertInfo{ LastMessagesType: LastMessageTypeNone, } + if c.Query("beta") == "true" { + info.IsClaudeBetaQuery = true + } return info } @@ -496,10 +502,59 @@ func (t TaskSubmitReq) HasImage() bool { } type TaskInfo struct { - Code int `json:"code"` - TaskID string `json:"task_id"` - Status string `json:"status"` - Reason string `json:"reason,omitempty"` - Url string `json:"url,omitempty"` - Progress string `json:"progress,omitempty"` + Code int `json:"code"` + TaskID string `json:"task_id"` + Status string `json:"status"` + Reason string `json:"reason,omitempty"` + Url string `json:"url,omitempty"` + Progress string `json:"progress,omitempty"` + CompletionTokens int `json:"completion_tokens,omitempty"` // 用于按倍率计费 + TotalTokens int `json:"total_tokens,omitempty"` // 用于按倍率计费 +} + +func FailTaskInfo(reason string) *TaskInfo { + return &TaskInfo{ + Status: "FAILURE", + Reason: reason, + } +} + +// RemoveDisabledFields 从请求 JSON 数据中移除渠道设置中禁用的字段 +// service_tier: 服务层级字段,可能导致额外计费(OpenAI、Claude、Responses API 支持) +// store: 数据存储授权字段,涉及用户隐私(仅 OpenAI、Responses API 支持,默认允许透传,禁用后可能导致 Codex 无法使用) +// safety_identifier: 安全标识符,用于向 OpenAI 报告违规用户(仅 OpenAI 支持,涉及用户隐私) +func RemoveDisabledFields(jsonData []byte, channelOtherSettings dto.ChannelOtherSettings) ([]byte, error) { + var data map[string]interface{} + if err := common.Unmarshal(jsonData, &data); err != nil { + common.SysError("RemoveDisabledFields Unmarshal error :" + err.Error()) + return jsonData, nil + } + + // 默认移除 service_tier,除非明确允许(避免额外计费风险) + if !channelOtherSettings.AllowServiceTier { + if _, exists := data["service_tier"]; exists { + delete(data, "service_tier") + } + } + + // 默认允许 store 透传,除非明确禁用(禁用可能影响 Codex 使用) + if channelOtherSettings.DisableStore { + if _, exists := data["store"]; exists { + delete(data, "store") + } + } + + // 默认移除 safety_identifier,除非明确允许(保护用户隐私,避免向 OpenAI 报告用户信息) + if !channelOtherSettings.AllowSafetyIdentifier { + if _, exists := data["safety_identifier"]; exists { + delete(data, "safety_identifier") + } + } + + jsonDataAfter, err := common.Marshal(data) + if err != nil { + common.SysError("RemoveDisabledFields Marshal error :" + err.Error()) + return jsonData, nil + } + return jsonDataAfter, nil } diff --git a/relay/common/relay_utils.go b/relay/common/relay_utils.go index 3a721b479..b38baf13a 100644 --- a/relay/common/relay_utils.go +++ b/relay/common/relay_utils.go @@ -3,12 +3,15 @@ package common import ( "fmt" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" + "strconv" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/gin-gonic/gin" + "github.com/samber/lo" ) type HasPrompt interface { @@ -52,7 +55,7 @@ func createTaskError(err error, code string, statusCode int, localError bool) *d } } -func storeTaskRequest(c *gin.Context, info *RelayInfo, action string, requestObj interface{}) { +func storeTaskRequest(c *gin.Context, info *RelayInfo, action string, requestObj TaskSubmitReq) { info.Action = action c.Set("task_request", requestObj) } @@ -64,9 +67,167 @@ func validatePrompt(prompt string) *dto.TaskError { return nil } -func ValidateBasicTaskRequest(c *gin.Context, info *RelayInfo, action string) *dto.TaskError { +func validateMultipartTaskRequest(c *gin.Context, info *RelayInfo, action string) (TaskSubmitReq, error) { var req TaskSubmitReq - if err := common.UnmarshalBodyReusable(c, &req); err != nil { + if _, err := c.MultipartForm(); err != nil { + return req, err + } + + formData := c.Request.PostForm + req = TaskSubmitReq{ + Prompt: formData.Get("prompt"), + Model: formData.Get("model"), + Mode: formData.Get("mode"), + Image: formData.Get("image"), + Size: formData.Get("size"), + Metadata: make(map[string]interface{}), + } + + if durationStr := formData.Get("seconds"); durationStr != "" { + if duration, err := strconv.Atoi(durationStr); err == nil { + req.Duration = duration + } + } + + if images := formData["images"]; len(images) > 0 { + req.Images = images + } + + for key, values := range formData { + if len(values) > 0 && !isKnownTaskField(key) { + if intVal, err := strconv.Atoi(values[0]); err == nil { + req.Metadata[key] = intVal + } else if floatVal, err := strconv.ParseFloat(values[0], 64); err == nil { + req.Metadata[key] = floatVal + } else { + req.Metadata[key] = values[0] + } + } + } + return req, nil +} + +func ValidateMultipartDirect(c *gin.Context, info *RelayInfo) *dto.TaskError { + contentType := c.GetHeader("Content-Type") + var prompt string + var model string + var seconds int + var size string + var hasInputReference bool + + if strings.HasPrefix(contentType, "multipart/form-data") { + form, err := common.ParseMultipartFormReusable(c) + if err != nil { + return createTaskError(err, "invalid_multipart_form", http.StatusBadRequest, true) + } + defer form.RemoveAll() + + prompts, ok := form.Value["prompt"] + if !ok || len(prompts) == 0 { + return createTaskError(fmt.Errorf("prompt field is required"), "missing_prompt", http.StatusBadRequest, true) + } + prompt = prompts[0] + + if _, ok := form.Value["model"]; !ok { + return createTaskError(fmt.Errorf("model field is required"), "missing_model", http.StatusBadRequest, true) + } + model = form.Value["model"][0] + + if _, ok := form.File["input_reference"]; ok { + hasInputReference = true + } + + if ss, ok := form.Value["seconds"]; ok { + sInt := common.String2Int(ss[0]) + if sInt > seconds { + seconds = common.String2Int(ss[0]) + } + } + + if sz, ok := form.Value["size"]; ok { + size = sz[0] + } + } else { + var req TaskSubmitReq + if err := common.UnmarshalBodyReusable(c, &req); err != nil { + return createTaskError(err, "invalid_json", http.StatusBadRequest, true) + } + + prompt = req.Prompt + model = req.Model + seconds = req.Duration + + if strings.TrimSpace(req.Model) == "" { + return createTaskError(fmt.Errorf("model field is required"), "missing_model", http.StatusBadRequest, true) + } + + if req.HasImage() { + hasInputReference = true + } + } + + if taskErr := validatePrompt(prompt); taskErr != nil { + return taskErr + } + + action := constant.TaskActionTextGenerate + if hasInputReference { + action = constant.TaskActionGenerate + } + if strings.HasPrefix(model, "sora-2") { + + if size == "" { + size = "720x1280" + } + + if seconds <= 0 { + seconds = 4 + } + + if model == "sora-2" && !lo.Contains([]string{"720x1280", "1280x720"}, size) { + return createTaskError(fmt.Errorf("sora-2 size is invalid"), "invalid_size", http.StatusBadRequest, true) + } + if model == "sora-2-pro" && !lo.Contains([]string{"720x1280", "1280x720", "1792x1024", "1024x1792"}, size) { + return createTaskError(fmt.Errorf("sora-2 size is invalid"), "invalid_size", http.StatusBadRequest, true) + } + info.PriceData.OtherRatios = map[string]float64{ + "seconds": float64(seconds), + "size": 1, + } + if lo.Contains([]string{"1792x1024", "1024x1792"}, size) { + info.PriceData.OtherRatios["size"] = 1.666667 + } + } + + info.Action = action + + return nil +} + +func isKnownTaskField(field string) bool { + knownFields := map[string]bool{ + "prompt": true, + "model": true, + "mode": true, + "image": true, + "images": true, + "size": true, + "duration": true, + "input_reference": true, // Sora 特有字段 + } + return knownFields[field] +} + +func ValidateBasicTaskRequest(c *gin.Context, info *RelayInfo, action string) *dto.TaskError { + var err error + contentType := c.GetHeader("Content-Type") + var req TaskSubmitReq + if strings.HasPrefix(contentType, "multipart/form-data") { + req, err = validateMultipartTaskRequest(c, info, action) + if err != nil { + return createTaskError(err, "invalid_multipart_form", http.StatusBadRequest, true) + } + } else if err := common.UnmarshalBodyReusable(c, &req); err != nil { return createTaskError(err, "invalid_request", http.StatusBadRequest, true) } diff --git a/relay/common_handler/rerank.go b/relay/common_handler/rerank.go index 05dbfa6d7..daf005df4 100644 --- a/relay/common_handler/rerank.go +++ b/relay/common_handler/rerank.go @@ -3,13 +3,14 @@ package common_handler import ( "io" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/relay/channel/xinference" - relaycommon "one-api/relay/common" - "one-api/service" - "one-api/types" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel/xinference" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/compatible_handler.go b/relay/compatible_handler.go index 38b820f72..1975eb423 100644 --- a/relay/compatible_handler.go +++ b/relay/compatible_handler.go @@ -5,20 +5,21 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/logger" - "one-api/model" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/setting/model_setting" - "one-api/setting/operation_setting" - "one-api/types" "strings" "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" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/setting/model_setting" + "github.com/QuantumNous/new-api/setting/operation_setting" + "github.com/QuantumNous/new-api/types" + "github.com/shopspring/decimal" "github.com/gin-gonic/gin" @@ -135,6 +136,12 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types return types.NewError(err, types.ErrorCodeJsonMarshalFailed, types.ErrOptionWithSkipRetry()) } + // remove disabled fields for OpenAI API + jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings) + if err != nil { + return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) + } + // apply param override if len(info.ParamOverride) > 0 { jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride) diff --git a/relay/embedding_handler.go b/relay/embedding_handler.go index 3d8962bb4..9bb76df03 100644 --- a/relay/embedding_handler.go +++ b/relay/embedding_handler.go @@ -5,12 +5,14 @@ import ( "encoding/json" "fmt" "net/http" - "one-api/common" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/types" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/logger" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) @@ -47,6 +49,7 @@ func EmbeddingHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError * if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) } + logger.LogDebug(c, fmt.Sprintf("converted embedding request body: %s", string(jsonData))) requestBody := bytes.NewBuffer(jsonData) statusCodeMappingStr := c.GetString("status_code_mapping") resp, err := adaptor.DoRequest(c, info, requestBody) diff --git a/relay/gemini_handler.go b/relay/gemini_handler.go index 1410da606..b3eb7f336 100644 --- a/relay/gemini_handler.go +++ b/relay/gemini_handler.go @@ -5,18 +5,19 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/logger" - "one-api/relay/channel/gemini" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/setting/model_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/relay/channel/gemini" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/setting/model_setting" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) @@ -239,6 +240,8 @@ func GeminiEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo) (newAPI return types.NewError(err, types.ErrorCodeChannelModelMappedError, types.ErrOptionWithSkipRetry()) } + req.SetModelName("models/" + info.UpstreamModelName) + adaptor := GetAdaptor(info.ApiType) if adaptor == nil { return types.NewError(fmt.Errorf("invalid api type: %d", info.ApiType), types.ErrorCodeInvalidApiType, types.ErrOptionWithSkipRetry()) @@ -263,6 +266,7 @@ func GeminiEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo) (newAPI return types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid, types.ErrOptionWithSkipRetry()) } } + logger.LogDebug(c, "Gemini embedding request body: "+string(jsonData)) requestBody = bytes.NewReader(jsonData) resp, err := adaptor.DoRequest(c, info, requestBody) diff --git a/relay/helper/common.go b/relay/helper/common.go index 381147ae5..3bb1c80c9 100644 --- a/relay/helper/common.go +++ b/relay/helper/common.go @@ -4,10 +4,11 @@ import ( "errors" "fmt" "net/http" - "one-api/common" - "one-api/dto" - "one-api/logger" - "one-api/types" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/logger" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" diff --git a/relay/helper/model_mapped.go b/relay/helper/model_mapped.go index 5b64cd8b3..821158fae 100644 --- a/relay/helper/model_mapped.go +++ b/relay/helper/model_mapped.go @@ -4,9 +4,10 @@ import ( "encoding/json" "errors" "fmt" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/common" "github.com/gin-gonic/gin" - "one-api/dto" - "one-api/relay/common" ) func ModelMappedHelper(c *gin.Context, info *common.RelayInfo, request dto.Request) error { diff --git a/relay/helper/price.go b/relay/helper/price.go index c23c068b3..dfbc58640 100644 --- a/relay/helper/price.go +++ b/relay/helper/price.go @@ -2,10 +2,12 @@ package helper import ( "fmt" - "one-api/common" - relaycommon "one-api/relay/common" - "one-api/setting/ratio_setting" - "one-api/types" + + "github.com/QuantumNous/new-api/common" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/setting/operation_setting" + "github.com/QuantumNous/new-api/setting/ratio_setting" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) @@ -54,6 +56,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens var cacheCreationRatio float64 var audioRatio float64 var audioCompletionRatio float64 + var freeModel bool if !usePrice { preConsumedTokens := common.Max(promptTokens, common.PreConsumedQuota) if meta.MaxTokens != 0 { @@ -86,18 +89,35 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio) } + // check if free model pre-consume is disabled + if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume { + // if model price or ratio is 0, do not pre-consume quota + if usePrice { + if modelPrice == 0 { + preConsumedQuota = 0 + freeModel = true + } + } else { + if modelRatio == 0 { + preConsumedQuota = 0 + freeModel = true + } + } + } + priceData := types.PriceData{ - ModelPrice: modelPrice, - ModelRatio: modelRatio, - CompletionRatio: completionRatio, - GroupRatioInfo: groupRatioInfo, - UsePrice: usePrice, - CacheRatio: cacheRatio, - ImageRatio: imageRatio, - AudioRatio: audioRatio, - AudioCompletionRatio: audioCompletionRatio, - CacheCreationRatio: cacheCreationRatio, - ShouldPreConsumedQuota: preConsumedQuota, + FreeModel: freeModel, + ModelPrice: modelPrice, + ModelRatio: modelRatio, + CompletionRatio: completionRatio, + GroupRatioInfo: groupRatioInfo, + UsePrice: usePrice, + CacheRatio: cacheRatio, + ImageRatio: imageRatio, + AudioRatio: audioRatio, + AudioCompletionRatio: audioCompletionRatio, + CacheCreationRatio: cacheCreationRatio, + QuotaToPreConsume: preConsumedQuota, } if common.DebugEnabled { @@ -114,7 +134,7 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) types. modelPrice, success := ratio_setting.GetModelPrice(info.OriginModelName, true) // 如果没有配置价格,则使用默认价格 if !success { - defaultPrice, ok := ratio_setting.GetDefaultModelRatioMap()[info.OriginModelName] + defaultPrice, ok := ratio_setting.GetDefaultModelPriceMap()[info.OriginModelName] if !ok { modelPrice = 0.1 } else { diff --git a/relay/helper/stream_scanner.go b/relay/helper/stream_scanner.go index 725d178cc..9044a1650 100644 --- a/relay/helper/stream_scanner.go +++ b/relay/helper/stream_scanner.go @@ -6,15 +6,16 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/constant" - "one-api/logger" - relaycommon "one-api/relay/common" - "one-api/setting/operation_setting" "strings" "sync" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/logger" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/setting/operation_setting" + "github.com/bytedance/gopkg/util/gopool" "github.com/gin-gonic/gin" diff --git a/relay/helper/valid_request.go b/relay/helper/valid_request.go index f4a290ec6..f17b4f5b8 100644 --- a/relay/helper/valid_request.go +++ b/relay/helper/valid_request.go @@ -4,13 +4,14 @@ import ( "errors" "fmt" "math" - "one-api/common" - "one-api/dto" - "one-api/logger" - relayconstant "one-api/relay/constant" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/logger" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) @@ -21,8 +22,10 @@ func GetAndValidateRequest(c *gin.Context, format types.RelayFormat) (request dt case types.RelayFormatOpenAI: request, err = GetAndValidateTextRequest(c, relayMode) case types.RelayFormatGemini: - if strings.Contains(c.Request.URL.Path, ":embedContent") || strings.Contains(c.Request.URL.Path, ":batchEmbedContents") { + if strings.Contains(c.Request.URL.Path, ":embedContent") { request, err = GetAndValidateGeminiEmbeddingRequest(c) + } else if strings.Contains(c.Request.URL.Path, ":batchEmbedContents") { + request, err = GetAndValidateGeminiBatchEmbeddingRequest(c) } else { request, err = GetAndValidateGeminiRequest(c) } @@ -275,7 +278,9 @@ func GetAndValidateTextRequest(c *gin.Context, relayMode int) (*dto.GeneralOpenA return nil, errors.New("field prompt is required") } case relayconstant.RelayModeChatCompletions: - if len(textRequest.Messages) == 0 { + // For FIM (Fill-in-the-middle) requests with prefix/suffix, messages is optional + // It will be filled by provider-specific adaptors if needed (e.g., SiliconFlow)。Or it is allowed by model vendor(s) (e.g., DeepSeek) + if len(textRequest.Messages) == 0 && textRequest.Prefix == nil && textRequest.Suffix == nil { return nil, errors.New("field messages is required") } case relayconstant.RelayModeEmbeddings: @@ -297,7 +302,7 @@ func GetAndValidateGeminiRequest(c *gin.Context) (*dto.GeminiChatRequest, error) if err != nil { return nil, err } - if len(request.Contents) == 0 { + if len(request.Contents) == 0 && len(request.Requests) == 0 { return nil, errors.New("contents is required") } @@ -316,3 +321,12 @@ func GetAndValidateGeminiEmbeddingRequest(c *gin.Context) (*dto.GeminiEmbeddingR } return request, nil } + +func GetAndValidateGeminiBatchEmbeddingRequest(c *gin.Context) (*dto.GeminiBatchEmbeddingRequest, error) { + request := &dto.GeminiBatchEmbeddingRequest{} + err := common.UnmarshalBodyReusable(c, request) + if err != nil { + return nil, err + } + return request, nil +} diff --git a/relay/image_handler.go b/relay/image_handler.go index 9c873d47f..afb9446a1 100644 --- a/relay/image_handler.go +++ b/relay/image_handler.go @@ -5,16 +5,17 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/dto" - "one-api/logger" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/setting/model_setting" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/logger" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/setting/model_setting" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/relay/mjproxy_handler.go b/relay/mjproxy_handler.go index ec8dfc6b2..8916ab181 100644 --- a/relay/mjproxy_handler.go +++ b/relay/mjproxy_handler.go @@ -7,20 +7,21 @@ import ( "io" "log" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/model" - relaycommon "one-api/relay/common" - relayconstant "one-api/relay/constant" - "one-api/relay/helper" - "one-api/service" - "one-api/setting" - "one-api/setting/system_setting" "strconv" "strings" "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" + 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/setting/system_setting" + "github.com/gin-gonic/gin" ) @@ -217,7 +218,7 @@ func RelaySwapFace(c *gin.Context, info *relaycommon.RelayInfo) *dto.MidjourneyR tokenName := c.GetString("token_name") logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %s", priceData.ModelPrice, priceData.GroupRatioInfo.GroupRatio, constant.MjActionSwapFace) - other := service.GenerateMjOtherInfo(priceData) + other := service.GenerateMjOtherInfo(info, priceData) model.RecordConsumeLog(c, info.UserId, model.RecordConsumeLogParams{ ChannelId: info.ChannelId, ModelName: modelName, @@ -517,7 +518,7 @@ func RelayMidjourneySubmit(c *gin.Context, relayInfo *relaycommon.RelayInfo) *dt } tokenName := c.GetString("token_name") logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %s,ID %s", priceData.ModelPrice, priceData.GroupRatioInfo.GroupRatio, midjRequest.Action, midjResponse.Result) - other := service.GenerateMjOtherInfo(priceData) + other := service.GenerateMjOtherInfo(relayInfo, priceData) model.RecordConsumeLog(c, relayInfo.UserId, model.RecordConsumeLogParams{ ChannelId: relayInfo.ChannelId, ModelName: modelName, diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index 0c271210b..736a58223 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -1,43 +1,47 @@ package relay import ( - "one-api/constant" - "one-api/relay/channel" - "one-api/relay/channel/ali" - "one-api/relay/channel/aws" - "one-api/relay/channel/baidu" - "one-api/relay/channel/baidu_v2" - "one-api/relay/channel/claude" - "one-api/relay/channel/cloudflare" - "one-api/relay/channel/cohere" - "one-api/relay/channel/coze" - "one-api/relay/channel/deepseek" - "one-api/relay/channel/dify" - "one-api/relay/channel/gemini" - "one-api/relay/channel/jimeng" - "one-api/relay/channel/jina" - "one-api/relay/channel/mistral" - "one-api/relay/channel/mokaai" - "one-api/relay/channel/moonshot" - "one-api/relay/channel/ollama" - "one-api/relay/channel/openai" - "one-api/relay/channel/palm" - "one-api/relay/channel/perplexity" - "one-api/relay/channel/siliconflow" - taskjimeng "one-api/relay/channel/task/jimeng" - "one-api/relay/channel/task/kling" - "one-api/relay/channel/task/suno" - taskvertex "one-api/relay/channel/task/vertex" - taskVidu "one-api/relay/channel/task/vidu" - "one-api/relay/channel/tencent" - "one-api/relay/channel/vertex" - "one-api/relay/channel/volcengine" - "one-api/relay/channel/xai" - "one-api/relay/channel/xunfei" - "one-api/relay/channel/zhipu" - "one-api/relay/channel/zhipu_4v" "strconv" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/relay/channel" + "github.com/QuantumNous/new-api/relay/channel/ali" + "github.com/QuantumNous/new-api/relay/channel/aws" + "github.com/QuantumNous/new-api/relay/channel/baidu" + "github.com/QuantumNous/new-api/relay/channel/baidu_v2" + "github.com/QuantumNous/new-api/relay/channel/claude" + "github.com/QuantumNous/new-api/relay/channel/cloudflare" + "github.com/QuantumNous/new-api/relay/channel/cohere" + "github.com/QuantumNous/new-api/relay/channel/coze" + "github.com/QuantumNous/new-api/relay/channel/deepseek" + "github.com/QuantumNous/new-api/relay/channel/dify" + "github.com/QuantumNous/new-api/relay/channel/gemini" + "github.com/QuantumNous/new-api/relay/channel/jimeng" + "github.com/QuantumNous/new-api/relay/channel/jina" + "github.com/QuantumNous/new-api/relay/channel/minimax" + "github.com/QuantumNous/new-api/relay/channel/mistral" + "github.com/QuantumNous/new-api/relay/channel/mokaai" + "github.com/QuantumNous/new-api/relay/channel/moonshot" + "github.com/QuantumNous/new-api/relay/channel/ollama" + "github.com/QuantumNous/new-api/relay/channel/openai" + "github.com/QuantumNous/new-api/relay/channel/palm" + "github.com/QuantumNous/new-api/relay/channel/perplexity" + "github.com/QuantumNous/new-api/relay/channel/siliconflow" + "github.com/QuantumNous/new-api/relay/channel/submodel" + taskdoubao "github.com/QuantumNous/new-api/relay/channel/task/doubao" + taskjimeng "github.com/QuantumNous/new-api/relay/channel/task/jimeng" + "github.com/QuantumNous/new-api/relay/channel/task/kling" + tasksora "github.com/QuantumNous/new-api/relay/channel/task/sora" + "github.com/QuantumNous/new-api/relay/channel/task/suno" + taskvertex "github.com/QuantumNous/new-api/relay/channel/task/vertex" + taskVidu "github.com/QuantumNous/new-api/relay/channel/task/vidu" + "github.com/QuantumNous/new-api/relay/channel/tencent" + "github.com/QuantumNous/new-api/relay/channel/vertex" + "github.com/QuantumNous/new-api/relay/channel/volcengine" + "github.com/QuantumNous/new-api/relay/channel/xai" + "github.com/QuantumNous/new-api/relay/channel/xunfei" + "github.com/QuantumNous/new-api/relay/channel/zhipu" + "github.com/QuantumNous/new-api/relay/channel/zhipu_4v" "github.com/gin-gonic/gin" ) @@ -103,6 +107,10 @@ func GetAdaptor(apiType int) channel.Adaptor { return &jimeng.Adaptor{} case constant.APITypeMoonshot: return &moonshot.Adaptor{} // Moonshot uses Claude API + case constant.APITypeSubmodel: + return &submodel.Adaptor{} + case constant.APITypeMiniMax: + return &minimax.Adaptor{} } return nil } @@ -132,6 +140,10 @@ func GetTaskAdaptor(platform constant.TaskPlatform) channel.TaskAdaptor { return &taskvertex.TaskAdaptor{} case constant.ChannelTypeVidu: return &taskVidu.TaskAdaptor{} + case constant.ChannelTypeDoubaoVideo: + return &taskdoubao.TaskAdaptor{} + case constant.ChannelTypeSora, constant.ChannelTypeOpenAI: + return &tasksora.TaskAdaptor{} } } return nil diff --git a/relay/relay_task.go b/relay/relay_task.go index 9cb8cd5c8..deb38d559 100644 --- a/relay/relay_task.go +++ b/relay/relay_task.go @@ -7,17 +7,19 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/model" - relaycommon "one-api/relay/common" - relayconstant "one-api/relay/constant" - "one-api/service" - "one-api/setting/ratio_setting" "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/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/setting/ratio_setting" + "github.com/gin-gonic/gin" ) @@ -53,7 +55,7 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto. } modelPrice, success := ratio_setting.GetModelPrice(modelName, true) if !success { - defaultPrice, ok := ratio_setting.GetDefaultModelRatioMap()[modelName] + defaultPrice, ok := ratio_setting.GetDefaultModelPriceMap()[modelName] if !ok { modelPrice = 0.1 } else { @@ -70,6 +72,17 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto. } else { ratio = modelPrice * groupRatio } + // FIXME: 临时修补,支持任务仅按次计费 + if !common.StringsContains(constant.TaskPricePatches, modelName) { + if len(info.PriceData.OtherRatios) > 0 { + for _, ra := range info.PriceData.OtherRatios { + if 1.0 != ra { + ratio *= ra + } + } + } + } + println(fmt.Sprintf("model: %s, model_price: %.4f, group: %s, group_ratio: %.4f, final_ratio: %.4f", modelName, modelPrice, info.UsingGroup, groupRatio, ratio)) userQuota, err := model.GetUserQuota(info.UserId, false) if err != nil { taskErr = service.TaskErrorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError) @@ -138,12 +151,31 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto. } if quota != 0 { tokenName := c.GetString("token_name") - gRatio := groupRatio - if hasUserGroupRatio { - gRatio = userGroupRatio + //gRatio := groupRatio + //if hasUserGroupRatio { + // gRatio = userGroupRatio + //} + logContent := fmt.Sprintf("操作 %s", info.Action) + // FIXME: 临时修补,支持任务仅按次计费 + if common.StringsContains(constant.TaskPricePatches, modelName) { + logContent = fmt.Sprintf("%s,按次计费", logContent) + } else { + if len(info.PriceData.OtherRatios) > 0 { + var contents []string + for key, ra := range info.PriceData.OtherRatios { + if 1.0 != ra { + contents = append(contents, fmt.Sprintf("%s: %.2f", key, ra)) + } + } + if len(contents) > 0 { + logContent = fmt.Sprintf("%s, 计算参数:%s", logContent, strings.Join(contents, ", ")) + } + } } - logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %s", modelPrice, gRatio, info.Action) other := make(map[string]interface{}) + if c != nil && c.Request != nil && c.Request.URL != nil { + other["request_path"] = c.Request.URL.Path + } other["model_price"] = modelPrice other["group_ratio"] = groupRatio if hasUserGroupRatio { @@ -362,11 +394,34 @@ func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *d } }() - if len(respBody) == 0 { - respBody, err = json.Marshal(dto.TaskResponse[any]{ - Code: "success", - Data: TaskModel2Dto(originTask), - }) + if len(respBody) != 0 { + return + } + + if strings.HasPrefix(c.Request.RequestURI, "/v1/videos/") { + adaptor := GetTaskAdaptor(originTask.Platform) + if adaptor == nil { + taskResp = service.TaskErrorWrapperLocal(fmt.Errorf("invalid channel id: %d", originTask.ChannelId), "invalid_channel_id", http.StatusBadRequest) + return + } + if converter, ok := adaptor.(channel.OpenAIVideoConverter); ok { + openAIVideoData, err := converter.ConvertToOpenAIVideo(originTask) + if err != nil { + taskResp = service.TaskErrorWrapper(err, "convert_to_openai_video_failed", http.StatusInternalServerError) + return + } + respBody = openAIVideoData + return + } + taskResp = service.TaskErrorWrapperLocal(errors.New(fmt.Sprintf("not_implemented:%s", originTask.Platform)), "not_implemented", http.StatusNotImplemented) + return + } + respBody, err = json.Marshal(dto.TaskResponse[any]{ + Code: "success", + Data: TaskModel2Dto(originTask), + }) + if err != nil { + taskResp = service.TaskErrorWrapper(err, "marshal_response_failed", http.StatusInternalServerError) } return } diff --git a/relay/rerank_handler.go b/relay/rerank_handler.go index 46d2e25f6..06aebbd1c 100644 --- a/relay/rerank_handler.go +++ b/relay/rerank_handler.go @@ -5,13 +5,14 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/setting/model_setting" - "one-api/types" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/setting/model_setting" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/relay/responses_handler.go b/relay/responses_handler.go index 0c57a303f..8087e2391 100644 --- a/relay/responses_handler.go +++ b/relay/responses_handler.go @@ -5,15 +5,16 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/setting/model_setting" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/relay/helper" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/setting/model_setting" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) @@ -56,6 +57,13 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError * if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) } + + // remove disabled fields for OpenAI Responses API + jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings) + if err != nil { + return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) + } + // apply param override if len(info.ParamOverride) > 0 { jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride) diff --git a/relay/websocket.go b/relay/websocket.go index 2d313154c..57a51895b 100644 --- a/relay/websocket.go +++ b/relay/websocket.go @@ -2,10 +2,11 @@ package relay import ( "fmt" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/service" - "one-api/types" + + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" diff --git a/router/api-router.go b/router/api-router.go index b96ccf3ea..d8d5857fc 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -1,8 +1,8 @@ package router import ( - "one-api/controller" - "one-api/middleware" + "github.com/QuantumNous/new-api/controller" + "github.com/QuantumNous/new-api/middleware" "github.com/gin-contrib/gzip" "github.com/gin-gonic/gin" @@ -20,6 +20,8 @@ func SetApiRouter(router *gin.Engine) { apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels) apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus) apiRouter.GET("/notice", controller.GetNotice) + apiRouter.GET("/user-agreement", controller.GetUserAgreement) + apiRouter.GET("/privacy-policy", controller.GetPrivacyPolicy) apiRouter.GET("/about", controller.GetAbout) //apiRouter.GET("/midjourney", controller.GetMidjourney) apiRouter.GET("/home_page_content", controller.GetHomePageContent) @@ -41,11 +43,17 @@ func SetApiRouter(router *gin.Engine) { apiRouter.POST("/stripe/webhook", controller.StripeWebhook) apiRouter.POST("/creem/webhook", controller.CreemWebhook) + // Universal secure verification routes + apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.UniversalVerify) + apiRouter.GET("/verify/status", middleware.UserAuth(), controller.GetVerificationStatus) + userRoute := apiRouter.Group("/user") { userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register) userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login) userRoute.POST("/login/2fa", middleware.CriticalRateLimit(), controller.Verify2FALogin) + userRoute.POST("/passkey/login/begin", middleware.CriticalRateLimit(), controller.PasskeyLoginBegin) + userRoute.POST("/passkey/login/finish", middleware.CriticalRateLimit(), controller.PasskeyLoginFinish) //userRoute.POST("/tokenlog", middleware.CriticalRateLimit(), controller.TokenLog) userRoute.GET("/logout", controller.Logout) userRoute.GET("/epay/notify", controller.EpayNotify) @@ -60,8 +68,15 @@ func SetApiRouter(router *gin.Engine) { selfRoute.PUT("/self", controller.UpdateSelf) selfRoute.DELETE("/self", controller.DeleteSelf) selfRoute.GET("/token", controller.GenerateAccessToken) + selfRoute.GET("/passkey", controller.PasskeyStatus) + selfRoute.POST("/passkey/register/begin", controller.PasskeyRegisterBegin) + selfRoute.POST("/passkey/register/finish", controller.PasskeyRegisterFinish) + selfRoute.POST("/passkey/verify/begin", controller.PasskeyVerifyBegin) + selfRoute.POST("/passkey/verify/finish", controller.PasskeyVerifyFinish) + selfRoute.DELETE("/passkey", controller.PasskeyDelete) selfRoute.GET("/aff", controller.GetAffCode) selfRoute.GET("/topup/info", controller.GetTopUpInfo) + selfRoute.GET("/topup/self", controller.GetUserTopUps) selfRoute.POST("/topup", middleware.CriticalRateLimit(), controller.TopUp) selfRoute.POST("/pay", middleware.CriticalRateLimit(), controller.RequestEpay) selfRoute.POST("/amount", controller.RequestAmount) @@ -83,12 +98,15 @@ func SetApiRouter(router *gin.Engine) { adminRoute.Use(middleware.AdminAuth()) { adminRoute.GET("/", controller.GetAllUsers) + adminRoute.GET("/topup", controller.GetAllTopUps) + adminRoute.POST("/topup/complete", controller.AdminCompleteTopUp) adminRoute.GET("/search", controller.SearchUsers) adminRoute.GET("/:id", controller.GetUser) adminRoute.POST("/", controller.CreateUser) adminRoute.POST("/manage", controller.ManageUser) adminRoute.PUT("/", controller.UpdateUser) adminRoute.DELETE("/:id", controller.DeleteUser) + adminRoute.DELETE("/:id/reset_passkey", controller.AdminResetPasskey) // Admin 2FA routes adminRoute.GET("/2fa/stats", controller.Admin2FAStats) @@ -117,7 +135,7 @@ func SetApiRouter(router *gin.Engine) { channelRoute.GET("/models", controller.ChannelListModels) channelRoute.GET("/models_enabled", controller.EnabledListModels) channelRoute.GET("/:id", controller.GetChannel) - channelRoute.POST("/:id/key", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.GetChannelKey) + channelRoute.POST("/:id/key", middleware.CriticalRateLimit(), middleware.DisableCache(), middleware.SecureVerificationRequired(), controller.GetChannelKey) channelRoute.GET("/test", controller.TestAllChannels) channelRoute.GET("/test/:id", controller.TestChannel) channelRoute.GET("/update_balance", controller.UpdateAllChannelsBalance) diff --git a/router/dashboard.go b/router/dashboard.go index 940006790..17132dfb2 100644 --- a/router/dashboard.go +++ b/router/dashboard.go @@ -1,10 +1,10 @@ package router import ( + "github.com/QuantumNous/new-api/controller" + "github.com/QuantumNous/new-api/middleware" "github.com/gin-contrib/gzip" "github.com/gin-gonic/gin" - "one-api/controller" - "one-api/middleware" ) func SetDashboardRouter(router *gin.Engine) { diff --git a/router/main.go b/router/main.go index 235764270..45b3080f2 100644 --- a/router/main.go +++ b/router/main.go @@ -4,10 +4,11 @@ import ( "embed" "fmt" "net/http" - "one-api/common" "os" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/gin-gonic/gin" ) diff --git a/router/relay-router.go b/router/relay-router.go index e0f05e97b..267459e2c 100644 --- a/router/relay-router.go +++ b/router/relay-router.go @@ -1,11 +1,11 @@ package router import ( - "one-api/constant" - "one-api/controller" - "one-api/middleware" - "one-api/relay" - "one-api/types" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/controller" + "github.com/QuantumNous/new-api/middleware" + "github.com/QuantumNous/new-api/relay" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) diff --git a/router/video-router.go b/router/video-router.go index bcc05eae9..72459fe18 100644 --- a/router/video-router.go +++ b/router/video-router.go @@ -1,19 +1,26 @@ package router import ( - "one-api/controller" - "one-api/middleware" + "github.com/QuantumNous/new-api/controller" + "github.com/QuantumNous/new-api/middleware" "github.com/gin-gonic/gin" ) func SetVideoRouter(router *gin.Engine) { videoV1Router := router.Group("/v1") + videoV1Router.GET("/videos/:task_id/content", controller.VideoProxy) videoV1Router.Use(middleware.TokenAuth(), middleware.Distribute()) { videoV1Router.POST("/video/generations", controller.RelayTask) videoV1Router.GET("/video/generations/:task_id", controller.RelayTask) } + // openai compatible API video routes + // docs: https://platform.openai.com/docs/api-reference/videos/create + { + videoV1Router.POST("/videos", controller.RelayTask) + videoV1Router.GET("/videos/:task_id", controller.RelayTask) + } klingV1Router := router.Group("/kling/v1") klingV1Router.Use(middleware.KlingRequestConvert(), middleware.TokenAuth(), middleware.Distribute()) diff --git a/router/web-router.go b/router/web-router.go index 57cd61ac0..b053a3e63 100644 --- a/router/web-router.go +++ b/router/web-router.go @@ -2,14 +2,15 @@ package router import ( "embed" + "net/http" + "strings" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/controller" + "github.com/QuantumNous/new-api/middleware" "github.com/gin-contrib/gzip" "github.com/gin-contrib/static" "github.com/gin-gonic/gin" - "net/http" - "one-api/common" - "one-api/controller" - "one-api/middleware" - "strings" ) func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { diff --git a/service/channel.go b/service/channel.go index 6ddc8e9ec..8f8a35726 100644 --- a/service/channel.go +++ b/service/channel.go @@ -3,13 +3,14 @@ package service import ( "fmt" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/model" - "one-api/setting/operation_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/model" + "github.com/QuantumNous/new-api/setting/operation_setting" + "github.com/QuantumNous/new-api/types" ) func formatNotifyType(channelId int, status int) string { @@ -75,6 +76,8 @@ func ShouldDisableChannel(channelType int, err *types.NewAPIError) bool { return true case "pre_consume_token_quota_failed": return true + case "Arrearage": + return true } switch oaiErr.Type { case "insufficient_quota": diff --git a/service/convert.go b/service/convert.go index b232ca396..975ab2d0b 100644 --- a/service/convert.go +++ b/service/convert.go @@ -3,12 +3,13 @@ package service import ( "encoding/json" "fmt" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/relay/channel/openrouter" - relaycommon "one-api/relay/common" "strings" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel/openrouter" + relaycommon "github.com/QuantumNous/new-api/relay/common" ) func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.RelayInfo) (*dto.GeneralOpenAIRequest, error) { @@ -351,7 +352,7 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon Type: "content_block_start", ContentBlock: &dto.ClaudeMediaMessage{ Type: "thinking", - Thinking: "", + Thinking: common.GetPointer[string](""), }, }) } @@ -359,7 +360,7 @@ func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamRespon // text delta claudeResponse.Delta = &dto.ClaudeMediaMessage{ Type: "thinking_delta", - Thinking: reasoning, + Thinking: &reasoning, } } else { if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeText { @@ -636,9 +637,6 @@ func extractTextFromGeminiParts(parts []dto.GeminiPart) string { func ResponseOpenAI2Gemini(openAIResponse *dto.OpenAITextResponse, info *relaycommon.RelayInfo) *dto.GeminiChatResponse { geminiResponse := &dto.GeminiChatResponse{ Candidates: make([]dto.GeminiChatCandidate, 0, len(openAIResponse.Choices)), - PromptFeedback: dto.GeminiChatPromptFeedback{ - SafetyRatings: []dto.GeminiChatSafetyRating{}, - }, UsageMetadata: dto.GeminiUsageMetadata{ PromptTokenCount: openAIResponse.PromptTokens, CandidatesTokenCount: openAIResponse.CompletionTokens, @@ -735,9 +733,6 @@ func StreamResponseOpenAI2Gemini(openAIResponse *dto.ChatCompletionsStreamRespon geminiResponse := &dto.GeminiChatResponse{ Candidates: make([]dto.GeminiChatCandidate, 0, len(openAIResponse.Choices)), - PromptFeedback: dto.GeminiChatPromptFeedback{ - SafetyRatings: []dto.GeminiChatSafetyRating{}, - }, UsageMetadata: dto.GeminiUsageMetadata{ PromptTokenCount: info.PromptTokens, CandidatesTokenCount: 0, // 流式响应中可能没有完整的 usage 信息 diff --git a/service/download.go b/service/download.go index 036c43af8..752d8c65b 100644 --- a/service/download.go +++ b/service/download.go @@ -5,9 +5,10 @@ import ( "encoding/json" "fmt" "net/http" - "one-api/common" - "one-api/setting/system_setting" "strings" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/setting/system_setting" ) // WorkerRequest Worker请求的数据结构 @@ -45,7 +46,7 @@ func DoWorkerRequest(req *WorkerRequest) (*http.Response, error) { return nil, fmt.Errorf("failed to marshal worker payload: %v", err) } - return http.Post(workerUrl, "application/json", bytes.NewBuffer(workerPayload)) + return GetHttpClient().Post(workerUrl, "application/json", bytes.NewBuffer(workerPayload)) } func DoDownloadRequest(originUrl string, reason ...string) (resp *http.Response, err error) { @@ -64,6 +65,6 @@ func DoDownloadRequest(originUrl string, reason ...string) (resp *http.Response, } common.SysLog(fmt.Sprintf("downloading from origin: %s, reason: %s", common.MaskSensitiveInfo(originUrl), strings.Join(reason, ", "))) - return http.Get(originUrl) + return GetHttpClient().Get(originUrl) } } diff --git a/service/epay.go b/service/epay.go index 48b84dd58..bfe14371e 100644 --- a/service/epay.go +++ b/service/epay.go @@ -1,8 +1,8 @@ package service import ( - "one-api/setting/operation_setting" - "one-api/setting/system_setting" + "github.com/QuantumNous/new-api/setting/operation_setting" + "github.com/QuantumNous/new-api/setting/system_setting" ) func GetCallbackAddress() string { diff --git a/service/error.go b/service/error.go index 5c3bddd6e..070335ec6 100644 --- a/service/error.go +++ b/service/error.go @@ -6,12 +6,13 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/dto" - "one-api/logger" - "one-api/types" "strconv" "strings" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/logger" + "github.com/QuantumNous/new-api/types" ) func MidjourneyErrorWrapper(code int, desc string) *dto.MidjourneyResponse { @@ -141,7 +142,8 @@ func TaskErrorWrapper(err error, code string, statusCode int) *dto.TaskError { lowerText := strings.ToLower(text) if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") { common.SysLog(fmt.Sprintf("error: %s", text)) - text = "请求上游地址失败" + //text = "请求上游地址失败" + text = common.MaskSensitiveInfo(text) } //避免暴露内部错误 taskError := &dto.TaskError{ diff --git a/service/file_decoder.go b/service/file_decoder.go index 99fdc3f9a..4ca139794 100644 --- a/service/file_decoder.go +++ b/service/file_decoder.go @@ -10,12 +10,13 @@ import ( _ "image/png" "io" "net/http" - "one-api/common" - "one-api/constant" - "one-api/logger" - "one-api/types" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/logger" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" ) diff --git a/service/http.go b/service/http.go index 357a2e788..7bd54c4ac 100644 --- a/service/http.go +++ b/service/http.go @@ -5,8 +5,9 @@ import ( "fmt" "io" "net/http" - "one-api/common" - "one-api/logger" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/logger" "github.com/gin-gonic/gin" ) diff --git a/service/http_client.go b/service/http_client.go index b191ddd78..8bc044544 100644 --- a/service/http_client.go +++ b/service/http_client.go @@ -6,20 +6,42 @@ import ( "net" "net/http" "net/url" - "one-api/common" + "sync" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/setting/system_setting" + "golang.org/x/net/proxy" ) -var httpClient *http.Client +var ( + httpClient *http.Client + proxyClientLock sync.Mutex + proxyClients = make(map[string]*http.Client) +) + +func checkRedirect(req *http.Request, via []*http.Request) error { + fetchSetting := system_setting.GetFetchSetting() + urlStr := req.URL.String() + if err := common.ValidateURLWithFetchSetting(urlStr, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil { + return fmt.Errorf("redirect to %s blocked: %v", urlStr, err) + } + if len(via) >= 10 { + return fmt.Errorf("stopped after 10 redirects") + } + return nil +} func InitHttpClient() { if common.RelayTimeout == 0 { - httpClient = &http.Client{} + httpClient = &http.Client{ + CheckRedirect: checkRedirect, + } } else { httpClient = &http.Client{ - Timeout: time.Duration(common.RelayTimeout) * time.Second, + Timeout: time.Duration(common.RelayTimeout) * time.Second, + CheckRedirect: checkRedirect, } } } @@ -28,12 +50,31 @@ func GetHttpClient() *http.Client { return httpClient } +// ResetProxyClientCache 清空代理客户端缓存,确保下次使用时重新初始化 +func ResetProxyClientCache() { + proxyClientLock.Lock() + defer proxyClientLock.Unlock() + for _, client := range proxyClients { + if transport, ok := client.Transport.(*http.Transport); ok && transport != nil { + transport.CloseIdleConnections() + } + } + proxyClients = make(map[string]*http.Client) +} + // NewProxyHttpClient 创建支持代理的 HTTP 客户端 func NewProxyHttpClient(proxyURL string) (*http.Client, error) { if proxyURL == "" { return http.DefaultClient, nil } + proxyClientLock.Lock() + if client, ok := proxyClients[proxyURL]; ok { + proxyClientLock.Unlock() + return client, nil + } + proxyClientLock.Unlock() + parsedURL, err := url.Parse(proxyURL) if err != nil { return nil, err @@ -41,11 +82,17 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) { switch parsedURL.Scheme { case "http", "https": - return &http.Client{ + client := &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyURL(parsedURL), }, - }, nil + CheckRedirect: checkRedirect, + } + client.Timeout = time.Duration(common.RelayTimeout) * time.Second + proxyClientLock.Lock() + proxyClients[proxyURL] = client + proxyClientLock.Unlock() + return client, nil case "socks5", "socks5h": // 获取认证信息 @@ -67,15 +114,21 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) { return nil, err } - return &http.Client{ + client := &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return dialer.Dial(network, addr) }, }, - }, nil + CheckRedirect: checkRedirect, + } + client.Timeout = time.Duration(common.RelayTimeout) * time.Second + proxyClientLock.Lock() + proxyClients[proxyURL] = client + proxyClientLock.Unlock() + return client, nil default: - return nil, fmt.Errorf("unsupported proxy scheme: %s", parsedURL.Scheme) + return nil, fmt.Errorf("unsupported proxy scheme: %s, must be http, https, socks5 or socks5h", parsedURL.Scheme) } } diff --git a/service/image.go b/service/image.go index 453d8dd1c..72ab892d9 100644 --- a/service/image.go +++ b/service/image.go @@ -8,10 +8,11 @@ import ( "image" "io" "net/http" - "one-api/common" - "one-api/constant" "strings" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "golang.org/x/image/webp" ) diff --git a/service/log_info_generate.go b/service/log_info_generate.go index 7a609c9f5..95a88dfbc 100644 --- a/service/log_info_generate.go +++ b/service/log_info_generate.go @@ -1,15 +1,36 @@ package service import ( - "one-api/common" - "one-api/constant" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/types" + "strings" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" "github.com/gin-gonic/gin" ) +func appendRequestPath(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, other map[string]interface{}) { + if other == nil { + return + } + if ctx != nil && ctx.Request != nil && ctx.Request.URL != nil { + if path := ctx.Request.URL.Path; path != "" { + other["request_path"] = path + return + } + } + if relayInfo != nil && relayInfo.RequestURLPath != "" { + path := relayInfo.RequestURLPath + if idx := strings.Index(path, "?"); idx != -1 { + path = path[:idx] + } + other["request_path"] = path + } +} + func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio, groupRatio, completionRatio float64, cacheTokens int, cacheRatio float64, modelPrice float64, userGroupRatio float64) map[string]interface{} { other := make(map[string]interface{}) @@ -42,6 +63,7 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m adminInfo["multi_key_index"] = common.GetContextKeyInt(ctx, constant.ContextKeyChannelMultiKeyIndex) } other["admin_info"] = adminInfo + appendRequestPath(ctx, relayInfo, other) return other } @@ -78,12 +100,13 @@ func GenerateClaudeOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, return info } -func GenerateMjOtherInfo(priceData types.PerCallPriceData) map[string]interface{} { +func GenerateMjOtherInfo(relayInfo *relaycommon.RelayInfo, priceData types.PerCallPriceData) map[string]interface{} { other := make(map[string]interface{}) other["model_price"] = priceData.ModelPrice other["group_ratio"] = priceData.GroupRatioInfo.GroupRatio if priceData.GroupRatioInfo.HasSpecialRatio { other["user_group_ratio"] = priceData.GroupRatioInfo.GroupSpecialRatio } + appendRequestPath(nil, relayInfo, other) return other } diff --git a/service/midjourney.go b/service/midjourney.go index 916d02d0b..9b2eb5ca7 100644 --- a/service/midjourney.go +++ b/service/midjourney.go @@ -6,15 +6,16 @@ import ( "io" "log" "net/http" - "one-api/common" - "one-api/constant" - "one-api/dto" - relayconstant "one-api/relay/constant" - "one-api/setting" "strconv" "strings" "time" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/setting" + "github.com/gin-gonic/gin" ) diff --git a/service/notify-limit.go b/service/notify-limit.go index 309ea54d2..cad5d7bc1 100644 --- a/service/notify-limit.go +++ b/service/notify-limit.go @@ -2,12 +2,13 @@ package service import ( "fmt" - "github.com/bytedance/gopkg/util/gopool" - "one-api/common" - "one-api/constant" "strconv" "sync" "time" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/bytedance/gopkg/util/gopool" ) // notifyLimitStore is used for in-memory rate limiting when Redis is disabled diff --git a/service/passkey/service.go b/service/passkey/service.go new file mode 100644 index 000000000..4d29d1aef --- /dev/null +++ b/service/passkey/service.go @@ -0,0 +1,177 @@ +package passkey + +import ( + "errors" + "fmt" + "net" + "net/http" + "net/url" + "strings" + "time" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/setting/system_setting" + + "github.com/go-webauthn/webauthn/protocol" + webauthn "github.com/go-webauthn/webauthn/webauthn" +) + +const ( + RegistrationSessionKey = "passkey_registration_session" + LoginSessionKey = "passkey_login_session" + VerifySessionKey = "passkey_verify_session" +) + +// BuildWebAuthn constructs a WebAuthn instance using the current passkey settings and request context. +func BuildWebAuthn(r *http.Request) (*webauthn.WebAuthn, error) { + settings := system_setting.GetPasskeySettings() + if settings == nil { + return nil, errors.New("未找到 Passkey 设置") + } + + displayName := strings.TrimSpace(settings.RPDisplayName) + if displayName == "" { + displayName = common.SystemName + } + + origins, err := resolveOrigins(r, settings) + if err != nil { + return nil, err + } + + rpID, err := resolveRPID(r, settings, origins) + if err != nil { + return nil, err + } + + selection := protocol.AuthenticatorSelection{ + ResidentKey: protocol.ResidentKeyRequirementRequired, + RequireResidentKey: protocol.ResidentKeyRequired(), + UserVerification: protocol.UserVerificationRequirement(settings.UserVerification), + } + if selection.UserVerification == "" { + selection.UserVerification = protocol.VerificationPreferred + } + if attachment := strings.TrimSpace(settings.AttachmentPreference); attachment != "" { + selection.AuthenticatorAttachment = protocol.AuthenticatorAttachment(attachment) + } + + config := &webauthn.Config{ + RPID: rpID, + RPDisplayName: displayName, + RPOrigins: origins, + AuthenticatorSelection: selection, + Debug: common.DebugEnabled, + Timeouts: webauthn.TimeoutsConfig{ + Login: webauthn.TimeoutConfig{ + Enforce: true, + Timeout: 2 * time.Minute, + TimeoutUVD: 2 * time.Minute, + }, + Registration: webauthn.TimeoutConfig{ + Enforce: true, + Timeout: 2 * time.Minute, + TimeoutUVD: 2 * time.Minute, + }, + }, + } + + return webauthn.New(config) +} + +func resolveOrigins(r *http.Request, settings *system_setting.PasskeySettings) ([]string, error) { + originsStr := strings.TrimSpace(settings.Origins) + if originsStr != "" { + originList := strings.Split(originsStr, ",") + origins := make([]string, 0, len(originList)) + for _, origin := range originList { + trimmed := strings.TrimSpace(origin) + if trimmed == "" { + continue + } + if !settings.AllowInsecureOrigin && strings.HasPrefix(strings.ToLower(trimmed), "http://") { + return nil, fmt.Errorf("Passkey 不允许使用不安全的 Origin: %s", trimmed) + } + origins = append(origins, trimmed) + } + if len(origins) == 0 { + // 如果配置了Origins但过滤后为空,使用自动推导 + goto autoDetect + } + return origins, nil + } + +autoDetect: + scheme := detectScheme(r) + if scheme == "http" && !settings.AllowInsecureOrigin && r.Host != "localhost" && r.Host != "127.0.0.1" && !strings.HasPrefix(r.Host, "127.0.0.1:") && !strings.HasPrefix(r.Host, "localhost:") { + return nil, fmt.Errorf("Passkey 仅支持 HTTPS,当前访问: %s://%s,请在 Passkey 设置中允许不安全 Origin 或配置 HTTPS", scheme, r.Host) + } + // 优先使用请求的完整Host(包含端口) + host := r.Host + + // 如果无法从请求获取Host,尝试从ServerAddress获取 + if host == "" && system_setting.ServerAddress != "" { + if parsed, err := url.Parse(system_setting.ServerAddress); err == nil && parsed.Host != "" { + host = parsed.Host + if scheme == "" && parsed.Scheme != "" { + scheme = parsed.Scheme + } + } + } + if host == "" { + return nil, fmt.Errorf("无法确定 Passkey 的 Origin,请在系统设置或 Passkey 设置中指定。当前 Host: '%s', ServerAddress: '%s'", r.Host, system_setting.ServerAddress) + } + if scheme == "" { + scheme = "https" + } + origin := fmt.Sprintf("%s://%s", scheme, host) + return []string{origin}, nil +} + +func resolveRPID(r *http.Request, settings *system_setting.PasskeySettings, origins []string) (string, error) { + rpID := strings.TrimSpace(settings.RPID) + if rpID != "" { + return hostWithoutPort(rpID), nil + } + if len(origins) == 0 { + return "", errors.New("Passkey 未配置 Origin,无法推导 RPID") + } + parsed, err := url.Parse(origins[0]) + if err != nil { + return "", fmt.Errorf("无法解析 Passkey Origin: %w", err) + } + return hostWithoutPort(parsed.Host), nil +} + +func hostWithoutPort(host string) string { + host = strings.TrimSpace(host) + if host == "" { + return "" + } + if strings.Contains(host, ":") { + if host, _, err := net.SplitHostPort(host); err == nil { + return host + } + } + return host +} + +func detectScheme(r *http.Request) string { + if r == nil { + return "" + } + if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" { + parts := strings.Split(proto, ",") + return strings.ToLower(strings.TrimSpace(parts[0])) + } + if r.TLS != nil { + return "https" + } + if r.URL != nil && r.URL.Scheme != "" { + return strings.ToLower(r.URL.Scheme) + } + if r.Header.Get("X-Forwarded-Protocol") != "" { + return strings.ToLower(strings.TrimSpace(r.Header.Get("X-Forwarded-Protocol"))) + } + return "http" +} diff --git a/service/passkey/session.go b/service/passkey/session.go new file mode 100644 index 000000000..15e619326 --- /dev/null +++ b/service/passkey/session.go @@ -0,0 +1,50 @@ +package passkey + +import ( + "encoding/json" + "errors" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + webauthn "github.com/go-webauthn/webauthn/webauthn" +) + +var errSessionNotFound = errors.New("Passkey 会话不存在或已过期") + +func SaveSessionData(c *gin.Context, key string, data *webauthn.SessionData) error { + session := sessions.Default(c) + if data == nil { + session.Delete(key) + return session.Save() + } + payload, err := json.Marshal(data) + if err != nil { + return err + } + session.Set(key, string(payload)) + return session.Save() +} + +func PopSessionData(c *gin.Context, key string) (*webauthn.SessionData, error) { + session := sessions.Default(c) + raw := session.Get(key) + if raw == nil { + return nil, errSessionNotFound + } + session.Delete(key) + _ = session.Save() + var data webauthn.SessionData + switch value := raw.(type) { + case string: + if err := json.Unmarshal([]byte(value), &data); err != nil { + return nil, err + } + case []byte: + if err := json.Unmarshal(value, &data); err != nil { + return nil, err + } + default: + return nil, errors.New("Passkey 会话格式无效") + } + return &data, nil +} diff --git a/service/passkey/user.go b/service/passkey/user.go new file mode 100644 index 000000000..2ec248a9d --- /dev/null +++ b/service/passkey/user.go @@ -0,0 +1,71 @@ +package passkey + +import ( + "fmt" + "strconv" + "strings" + + "github.com/QuantumNous/new-api/model" + + webauthn "github.com/go-webauthn/webauthn/webauthn" +) + +type WebAuthnUser struct { + user *model.User + credential *model.PasskeyCredential +} + +func NewWebAuthnUser(user *model.User, credential *model.PasskeyCredential) *WebAuthnUser { + return &WebAuthnUser{user: user, credential: credential} +} + +func (u *WebAuthnUser) WebAuthnID() []byte { + if u == nil || u.user == nil { + return nil + } + return []byte(strconv.Itoa(u.user.Id)) +} + +func (u *WebAuthnUser) WebAuthnName() string { + if u == nil || u.user == nil { + return "" + } + name := strings.TrimSpace(u.user.Username) + if name == "" { + return fmt.Sprintf("user-%d", u.user.Id) + } + return name +} + +func (u *WebAuthnUser) WebAuthnDisplayName() string { + if u == nil || u.user == nil { + return "" + } + display := strings.TrimSpace(u.user.DisplayName) + if display != "" { + return display + } + return u.WebAuthnName() +} + +func (u *WebAuthnUser) WebAuthnCredentials() []webauthn.Credential { + if u == nil || u.credential == nil { + return nil + } + cred := u.credential.ToWebAuthnCredential() + return []webauthn.Credential{cred} +} + +func (u *WebAuthnUser) ModelUser() *model.User { + if u == nil { + return nil + } + return u.user +} + +func (u *WebAuthnUser) PasskeyCredential() *model.PasskeyCredential { + if u == nil { + return nil + } + return u.credential +} diff --git a/service/pre_consume_quota.go b/service/pre_consume_quota.go index 0cf53513b..995a3f971 100644 --- a/service/pre_consume_quota.go +++ b/service/pre_consume_quota.go @@ -3,11 +3,12 @@ package service import ( "fmt" "net/http" - "one-api/common" - "one-api/logger" - "one-api/model" - relaycommon "one-api/relay/common" - "one-api/types" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/logger" + "github.com/QuantumNous/new-api/model" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" "github.com/bytedance/gopkg/util/gopool" "github.com/gin-gonic/gin" diff --git a/service/quota.go b/service/quota.go index 12017e11e..495e379d4 100644 --- a/service/quota.go +++ b/service/quota.go @@ -5,18 +5,19 @@ import ( "fmt" "log" "math" - "one-api/common" - "one-api/constant" - "one-api/dto" - "one-api/logger" - "one-api/model" - relaycommon "one-api/relay/common" - "one-api/setting/ratio_setting" - "one-api/setting/system_setting" - "one-api/types" "strings" "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" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/setting/ratio_setting" + "github.com/QuantumNous/new-api/setting/system_setting" + "github.com/QuantumNous/new-api/types" + "github.com/bytedance/gopkg/util/gopool" "github.com/gin-gonic/gin" @@ -549,8 +550,11 @@ func checkAndSendQuotaNotify(relayInfo *relaycommon.RelayInfo, quota int, preCon // Bark推送使用简短文本,不支持HTML content = "{{value}},剩余额度:{{value}},请及时充值" values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota)} + } else if notifyType == dto.NotifyTypeGotify { + content = "{{value}},当前剩余额度为 {{value}},请及时充值。" + values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota)} } else { - // 默认内容格式,适用于Email和Webhook + // 默认内容格式,适用于Email和Webhook(支持HTML) content = "{{value}},当前剩余额度为 {{value}},为了不影响您的使用,请及时充值。
充值链接:{{value}}" values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota), topUpLink, topUpLink} } diff --git a/service/sensitive.go b/service/sensitive.go index ed033daac..3c7809980 100644 --- a/service/sensitive.go +++ b/service/sensitive.go @@ -2,9 +2,10 @@ package service import ( "errors" - "one-api/dto" - "one-api/setting" "strings" + + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/setting" ) func CheckSensitiveMessages(messages []dto.Message) ([]string, error) { diff --git a/service/task.go b/service/task.go index c2501fe28..b33ef29c5 100644 --- a/service/task.go +++ b/service/task.go @@ -1,8 +1,9 @@ package service import ( - "one-api/constant" "strings" + + "github.com/QuantumNous/new-api/constant" ) func CoverTaskActionToModelName(platform constant.TaskPlatform, action string) string { diff --git a/service/token_counter.go b/service/token_counter.go index be5c2e80c..87ef3b3ec 100644 --- a/service/token_counter.go +++ b/service/token_counter.go @@ -10,15 +10,16 @@ import ( _ "image/png" "log" "math" - "one-api/common" - "one-api/constant" - "one-api/dto" - relaycommon "one-api/relay/common" - "one-api/types" "strings" "sync" "unicode/utf8" + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/types" + "github.com/gin-gonic/gin" "github.com/tiktoken-go/tokenizer" "github.com/tiktoken-go/tokenizer/codec" diff --git a/service/usage_helpr.go b/service/usage_helpr.go index ca9c08305..719b04831 100644 --- a/service/usage_helpr.go +++ b/service/usage_helpr.go @@ -1,7 +1,7 @@ package service import ( - "one-api/dto" + "github.com/QuantumNous/new-api/dto" ) //func GetPromptTokens(textRequest dto.GeneralOpenAIRequest, relayMode int) (int, error) { diff --git a/service/user_notify.go b/service/user_notify.go index fba12d9db..cecf46cad 100644 --- a/service/user_notify.go +++ b/service/user_notify.go @@ -1,14 +1,17 @@ package service import ( + "bytes" + "encoding/json" "fmt" "net/http" "net/url" - "one-api/common" - "one-api/dto" - "one-api/model" - "one-api/setting/system_setting" "strings" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/setting/system_setting" ) func NotifyRootUser(t string, subject string, content string) { @@ -37,13 +40,16 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data switch notifyType { case dto.NotifyTypeEmail: - // check setting email - userEmail = userSetting.NotificationEmail - if userEmail == "" { + // 优先使用设置中的通知邮箱,如果为空则使用用户的默认邮箱 + emailToUse := userSetting.NotificationEmail + if emailToUse == "" { + emailToUse = userEmail + } + if emailToUse == "" { common.SysLog(fmt.Sprintf("user %d has no email, skip sending email", userId)) return nil } - return sendEmailNotify(userEmail, data) + return sendEmailNotify(emailToUse, data) case dto.NotifyTypeWebhook: webhookURLStr := userSetting.WebhookUrl if webhookURLStr == "" { @@ -61,6 +67,14 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data return nil } return sendBarkNotify(barkURL, data) + case dto.NotifyTypeGotify: + gotifyUrl := userSetting.GotifyUrl + gotifyToken := userSetting.GotifyToken + if gotifyUrl == "" || gotifyToken == "" { + common.SysLog(fmt.Sprintf("user %d has no gotify url or token, skip sending gotify", userId)) + return nil + } + return sendGotifyNotify(gotifyUrl, gotifyToken, userSetting.GotifyPriority, data) } return nil } @@ -144,3 +158,98 @@ func sendBarkNotify(barkURL string, data dto.Notify) error { return nil } + +func sendGotifyNotify(gotifyUrl string, gotifyToken string, priority int, data dto.Notify) error { + // 处理占位符 + content := data.Content + for _, value := range data.Values { + content = strings.Replace(content, dto.ContentValueParam, fmt.Sprintf("%v", value), 1) + } + + // 构建完整的 Gotify API URL + // 确保 URL 以 /message 结尾 + finalURL := strings.TrimSuffix(gotifyUrl, "/") + "/message?token=" + url.QueryEscape(gotifyToken) + + // Gotify优先级范围0-10,如果超出范围则使用默认值5 + if priority < 0 || priority > 10 { + priority = 5 + } + + // 构建 JSON payload + type GotifyMessage struct { + Title string `json:"title"` + Message string `json:"message"` + Priority int `json:"priority"` + } + + payload := GotifyMessage{ + Title: data.Title, + Message: content, + Priority: priority, + } + + // 序列化为 JSON + payloadBytes, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("failed to marshal gotify payload: %v", err) + } + + var req *http.Request + var resp *http.Response + + if system_setting.EnableWorker() { + // 使用worker发送请求 + workerReq := &WorkerRequest{ + URL: finalURL, + Key: system_setting.WorkerValidKey, + Method: http.MethodPost, + Headers: map[string]string{ + "Content-Type": "application/json; charset=utf-8", + "User-Agent": "OneAPI-Gotify-Notify/1.0", + }, + Body: payloadBytes, + } + + resp, err = DoWorkerRequest(workerReq) + if err != nil { + return fmt.Errorf("failed to send gotify request through worker: %v", err) + } + defer resp.Body.Close() + + // 检查响应状态 + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("gotify request failed with status code: %d", resp.StatusCode) + } + } else { + // SSRF防护:验证Gotify URL(非Worker模式) + fetchSetting := system_setting.GetFetchSetting() + if err := common.ValidateURLWithFetchSetting(finalURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil { + return fmt.Errorf("request reject: %v", err) + } + + // 直接发送请求 + req, err = http.NewRequest(http.MethodPost, finalURL, bytes.NewBuffer(payloadBytes)) + if err != nil { + return fmt.Errorf("failed to create gotify request: %v", err) + } + + // 设置请求头 + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("User-Agent", "NewAPI-Gotify-Notify/1.0") + + // 发送请求 + client := GetHttpClient() + resp, err = client.Do(req) + if err != nil { + return fmt.Errorf("failed to send gotify request: %v", err) + } + defer resp.Body.Close() + + // 检查响应状态 + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("gotify request failed with status code: %d", resp.StatusCode) + } + } + + return nil +} diff --git a/service/webhook.go b/service/webhook.go index c678b8634..bab8842c8 100644 --- a/service/webhook.go +++ b/service/webhook.go @@ -8,10 +8,11 @@ import ( "encoding/json" "fmt" "net/http" - "one-api/common" - "one-api/dto" - "one-api/setting/system_setting" "time" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/setting/system_setting" ) // WebhookPayload webhook 通知的负载数据 diff --git a/setting/chat.go b/setting/chat.go index bd1e26e30..9be08268a 100644 --- a/setting/chat.go +++ b/setting/chat.go @@ -2,7 +2,8 @@ package setting import ( "encoding/json" - "one-api/common" + + "github.com/QuantumNous/new-api/common" ) var Chats = []map[string]string{ diff --git a/setting/config/config.go b/setting/config/config.go index 3af51b146..3b97c9554 100644 --- a/setting/config/config.go +++ b/setting/config/config.go @@ -2,11 +2,12 @@ package config import ( "encoding/json" - "one-api/common" "reflect" "strconv" "strings" "sync" + + "github.com/QuantumNous/new-api/common" ) // ConfigManager 统一管理所有配置 diff --git a/setting/console_setting/config.go b/setting/console_setting/config.go index 8cfcd0ed6..144e95c49 100644 --- a/setting/console_setting/config.go +++ b/setting/console_setting/config.go @@ -1,6 +1,6 @@ package console_setting -import "one-api/setting/config" +import "github.com/QuantumNous/new-api/setting/config" type ConsoleSetting struct { ApiInfo string `json:"api_info"` // 控制台 API 信息 (JSON 数组字符串) diff --git a/setting/model_setting/claude.go b/setting/model_setting/claude.go index 049831821..1be0e3e40 100644 --- a/setting/model_setting/claude.go +++ b/setting/model_setting/claude.go @@ -2,7 +2,8 @@ package model_setting import ( "net/http" - "one-api/setting/config" + + "github.com/QuantumNous/new-api/setting/config" ) //var claudeHeadersSettings = map[string][]string{} diff --git a/setting/model_setting/gemini.go b/setting/model_setting/gemini.go index f132fec88..4856482ed 100644 --- a/setting/model_setting/gemini.go +++ b/setting/model_setting/gemini.go @@ -1,7 +1,7 @@ package model_setting import ( - "one-api/setting/config" + "github.com/QuantumNous/new-api/setting/config" ) // GeminiSettings 定义Gemini模型的配置 diff --git a/setting/model_setting/global.go b/setting/model_setting/global.go index de2851bb5..afde02144 100644 --- a/setting/model_setting/global.go +++ b/setting/model_setting/global.go @@ -1,7 +1,7 @@ package model_setting import ( - "one-api/setting/config" + "github.com/QuantumNous/new-api/setting/config" ) type GlobalSettings struct { diff --git a/setting/operation_setting/general_setting.go b/setting/operation_setting/general_setting.go index ae0c436ec..b4a3ccccd 100644 --- a/setting/operation_setting/general_setting.go +++ b/setting/operation_setting/general_setting.go @@ -1,18 +1,35 @@ package operation_setting -import "one-api/setting/config" +import "github.com/QuantumNous/new-api/setting/config" + +// 额度展示类型 +const ( + QuotaDisplayTypeUSD = "USD" + QuotaDisplayTypeCNY = "CNY" + QuotaDisplayTypeTokens = "TOKENS" + QuotaDisplayTypeCustom = "CUSTOM" +) type GeneralSetting struct { DocsLink string `json:"docs_link"` PingIntervalEnabled bool `json:"ping_interval_enabled"` PingIntervalSeconds int `json:"ping_interval_seconds"` + // 当前站点额度展示类型:USD / CNY / TOKENS + QuotaDisplayType string `json:"quota_display_type"` + // 自定义货币符号,用于 CUSTOM 展示类型 + CustomCurrencySymbol string `json:"custom_currency_symbol"` + // 自定义货币与美元汇率(1 USD = X Custom) + CustomCurrencyExchangeRate float64 `json:"custom_currency_exchange_rate"` } // 默认配置 var generalSetting = GeneralSetting{ - DocsLink: "https://docs.newapi.pro", - PingIntervalEnabled: false, - PingIntervalSeconds: 60, + DocsLink: "https://docs.newapi.pro", + PingIntervalEnabled: false, + PingIntervalSeconds: 60, + QuotaDisplayType: QuotaDisplayTypeUSD, + CustomCurrencySymbol: "¤", + CustomCurrencyExchangeRate: 1.0, } func init() { @@ -23,3 +40,52 @@ func init() { func GetGeneralSetting() *GeneralSetting { return &generalSetting } + +// IsCurrencyDisplay 是否以货币形式展示(美元或人民币) +func IsCurrencyDisplay() bool { + return generalSetting.QuotaDisplayType != QuotaDisplayTypeTokens +} + +// IsCNYDisplay 是否以人民币展示 +func IsCNYDisplay() bool { + return generalSetting.QuotaDisplayType == QuotaDisplayTypeCNY +} + +// GetQuotaDisplayType 返回额度展示类型 +func GetQuotaDisplayType() string { + return generalSetting.QuotaDisplayType +} + +// GetCurrencySymbol 返回当前展示类型对应符号 +func GetCurrencySymbol() string { + switch generalSetting.QuotaDisplayType { + case QuotaDisplayTypeUSD: + return "$" + case QuotaDisplayTypeCNY: + return "¥" + case QuotaDisplayTypeCustom: + if generalSetting.CustomCurrencySymbol != "" { + return generalSetting.CustomCurrencySymbol + } + return "¤" + default: + return "" + } +} + +// GetUsdToCurrencyRate 返回 1 USD = X 的 X(TOKENS 不适用) +func GetUsdToCurrencyRate(usdToCny float64) float64 { + switch generalSetting.QuotaDisplayType { + case QuotaDisplayTypeUSD: + return 1 + case QuotaDisplayTypeCNY: + return usdToCny + case QuotaDisplayTypeCustom: + if generalSetting.CustomCurrencyExchangeRate > 0 { + return generalSetting.CustomCurrencyExchangeRate + } + return 1 + default: + return 1 + } +} diff --git a/setting/operation_setting/monitor_setting.go b/setting/operation_setting/monitor_setting.go index 1d0bbec40..eed58d9c1 100644 --- a/setting/operation_setting/monitor_setting.go +++ b/setting/operation_setting/monitor_setting.go @@ -1,9 +1,10 @@ package operation_setting import ( - "one-api/setting/config" "os" "strconv" + + "github.com/QuantumNous/new-api/setting/config" ) type MonitorSetting struct { diff --git a/setting/operation_setting/payment_setting.go b/setting/operation_setting/payment_setting.go index c8df039cf..84162f4e5 100644 --- a/setting/operation_setting/payment_setting.go +++ b/setting/operation_setting/payment_setting.go @@ -1,6 +1,6 @@ package operation_setting -import "one-api/setting/config" +import "github.com/QuantumNous/new-api/setting/config" type PaymentSetting struct { AmountOptions []int `json:"amount_options"` diff --git a/setting/operation_setting/payment_setting_old.go b/setting/operation_setting/payment_setting_old.go index a6313179e..d34b6f0b8 100644 --- a/setting/operation_setting/payment_setting_old.go +++ b/setting/operation_setting/payment_setting_old.go @@ -6,7 +6,7 @@ This file is the old version of the payment settings file. If you need to add ne package operation_setting import ( - "one-api/common" + "github.com/QuantumNous/new-api/common" ) var PayAddress = "" diff --git a/setting/operation_setting/quota_setting.go b/setting/operation_setting/quota_setting.go new file mode 100644 index 000000000..dcf0501a9 --- /dev/null +++ b/setting/operation_setting/quota_setting.go @@ -0,0 +1,21 @@ +package operation_setting + +import "github.com/QuantumNous/new-api/setting/config" + +type QuotaSetting struct { + EnableFreeModelPreConsume bool `json:"enable_free_model_pre_consume"` // 是否对免费模型启用预消耗 +} + +// 默认配置 +var quotaSetting = QuotaSetting{ + EnableFreeModelPreConsume: true, +} + +func init() { + // 注册到全局配置管理器 + config.GlobalConfig.Register("quota_setting", "aSetting) +} + +func GetQuotaSetting() *QuotaSetting { + return "aSetting +} diff --git a/setting/operation_setting/tools.go b/setting/operation_setting/tools.go index 5b89d6fec..adb76bfc0 100644 --- a/setting/operation_setting/tools.go +++ b/setting/operation_setting/tools.go @@ -29,6 +29,7 @@ const ( Gemini25FlashLitePreviewInputAudioPrice = 0.50 Gemini25FlashNativeAudioInputAudioPrice = 3.00 Gemini20FlashInputAudioPrice = 0.70 + GeminiRoboticsER15InputAudioPrice = 1.00 ) const ( @@ -74,6 +75,8 @@ func GetGeminiInputAudioPricePerMillionTokens(modelName string) float64 { return Gemini25FlashProductionInputAudioPrice } else if strings.HasPrefix(modelName, "gemini-2.0-flash") { return Gemini20FlashInputAudioPrice + } else if strings.HasPrefix(modelName, "gemini-robotics-er-1.5") { + return GeminiRoboticsER15InputAudioPrice } return 0 } diff --git a/setting/payment_stripe.go b/setting/payment_stripe.go index 80d877dfa..d97120c85 100644 --- a/setting/payment_stripe.go +++ b/setting/payment_stripe.go @@ -5,3 +5,4 @@ var StripeWebhookSecret = "" var StripePriceId = "" var StripeUnitPrice = 8.0 var StripeMinTopUp = 1 +var StripePromotionCodesEnabled = false diff --git a/setting/rate_limit.go b/setting/rate_limit.go index 141463e14..413f3958d 100644 --- a/setting/rate_limit.go +++ b/setting/rate_limit.go @@ -4,8 +4,9 @@ import ( "encoding/json" "fmt" "math" - "one-api/common" "sync" + + "github.com/QuantumNous/new-api/common" ) var ModelRequestRateLimitEnabled = false diff --git a/setting/ratio_setting/cache_ratio.go b/setting/ratio_setting/cache_ratio.go index 5993cdeeb..dca381797 100644 --- a/setting/ratio_setting/cache_ratio.go +++ b/setting/ratio_setting/cache_ratio.go @@ -2,8 +2,9 @@ package ratio_setting import ( "encoding/json" - "one-api/common" "sync" + + "github.com/QuantumNous/new-api/common" ) var defaultCacheRatio = map[string]float64{ @@ -52,6 +53,8 @@ var defaultCacheRatio = map[string]float64{ "claude-opus-4-20250514-thinking": 0.1, "claude-opus-4-1-20250805": 0.1, "claude-opus-4-1-20250805-thinking": 0.1, + "claude-sonnet-4-5-20250929": 0.1, + "claude-sonnet-4-5-20250929-thinking": 0.1, } var defaultCreateCacheRatio = map[string]float64{ @@ -69,6 +72,8 @@ var defaultCreateCacheRatio = map[string]float64{ "claude-opus-4-20250514-thinking": 1.25, "claude-opus-4-1-20250805": 1.25, "claude-opus-4-1-20250805-thinking": 1.25, + "claude-sonnet-4-5-20250929": 1.25, + "claude-sonnet-4-5-20250929-thinking": 1.25, } //var defaultCreateCacheRatio = map[string]float64{} diff --git a/setting/ratio_setting/group_ratio.go b/setting/ratio_setting/group_ratio.go index c42553da0..cf85b66e9 100644 --- a/setting/ratio_setting/group_ratio.go +++ b/setting/ratio_setting/group_ratio.go @@ -3,8 +3,9 @@ package ratio_setting import ( "encoding/json" "errors" - "one-api/common" "sync" + + "github.com/QuantumNous/new-api/common" ) var groupRatio = map[string]float64{ diff --git a/setting/ratio_setting/model_ratio.go b/setting/ratio_setting/model_ratio.go index 362c6fa1a..0ff4035bd 100644 --- a/setting/ratio_setting/model_ratio.go +++ b/setting/ratio_setting/model_ratio.go @@ -2,10 +2,11 @@ package ratio_setting import ( "encoding/json" - "one-api/common" - "one-api/setting/operation_setting" "strings" "sync" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/setting/operation_setting" ) // from songquanpeng/one-api @@ -141,6 +142,7 @@ var defaultModelRatio = map[string]float64{ "claude-3-7-sonnet-20250219": 1.5, "claude-3-7-sonnet-20250219-thinking": 1.5, "claude-sonnet-4-20250514": 1.5, + "claude-sonnet-4-5-20250929": 1.5, "claude-3-opus-20240229": 7.5, // $15 / 1M tokens "claude-opus-4-20250514": 7.5, "claude-opus-4-1-20250805": 7.5, @@ -178,6 +180,7 @@ var defaultModelRatio = map[string]float64{ "gemini-2.5-flash-lite-preview-thinking-*": 0.05, "gemini-2.5-flash-lite-preview-06-17": 0.05, "gemini-2.5-flash": 0.15, + "gemini-robotics-er-1.5-preview": 0.15, "gemini-embedding-001": 0.075, "text-embedding-004": 0.001, "chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens @@ -251,6 +254,17 @@ var defaultModelRatio = map[string]float64{ "grok-vision-beta": 2.5, "grok-3-fast-beta": 2.5, "grok-3-mini-fast-beta": 0.3, + // submodel + "NousResearch/Hermes-4-405B-FP8": 0.8, + "Qwen/Qwen3-235B-A22B-Thinking-2507": 0.6, + "Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": 0.8, + "Qwen/Qwen3-235B-A22B-Instruct-2507": 0.3, + "zai-org/GLM-4.5-FP8": 0.8, + "openai/gpt-oss-120b": 0.5, + "deepseek-ai/DeepSeek-R1-0528": 0.8, + "deepseek-ai/DeepSeek-R1": 0.8, + "deepseek-ai/DeepSeek-V3-0324": 0.8, + "deepseek-ai/DeepSeek-V3.1": 0.8, } var defaultModelPrice = map[string]float64{ @@ -277,6 +291,8 @@ var defaultModelPrice = map[string]float64{ "mj_upscale": 0.05, "swap_face": 0.05, "mj_upload": 0.05, + "sora-2": 0.3, + "sora-2-pro": 0.5, } var defaultAudioRatio = map[string]float64{ @@ -439,6 +455,10 @@ func GetDefaultModelRatioMap() map[string]float64 { return defaultModelRatio } +func GetDefaultModelPriceMap() map[string]float64 { + return defaultModelPrice +} + func GetDefaultImageRatioMap() map[string]float64 { return defaultImageRatio } @@ -501,7 +521,6 @@ func GetCompletionRatio(name string) float64 { } func getHardcodedCompletionModelRatio(name string) (float64, bool) { - lowercaseName := strings.ToLower(name) isReservedModel := strings.HasSuffix(name, "-all") || strings.HasSuffix(name, "-gizmo-*") if isReservedModel { @@ -576,6 +595,8 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) { return 4, false } return 2.5 / 0.3, false + } else if strings.HasPrefix(name, "gemini-robotics-er-1.5") { + return 2.5 / 0.3, false } return 4, false } @@ -594,9 +615,6 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) { } } // hint 只给官方上4倍率,由于开源模型供应商自行定价,不对其进行补全倍率进行强制对齐 - if lowercaseName == "deepseek-chat" || lowercaseName == "deepseek-reasoner" { - return 4, true - } if strings.HasPrefix(name, "ERNIE-Speed-") { return 2, true } else if strings.HasPrefix(name, "ERNIE-Lite-") { diff --git a/setting/system_setting/fetch_setting.go b/setting/system_setting/fetch_setting.go index c41b930af..078696195 100644 --- a/setting/system_setting/fetch_setting.go +++ b/setting/system_setting/fetch_setting.go @@ -1,6 +1,6 @@ package system_setting -import "one-api/setting/config" +import "github.com/QuantumNous/new-api/setting/config" type FetchSetting struct { EnableSSRFProtection bool `json:"enable_ssrf_protection"` // 是否启用SSRF防护 diff --git a/setting/system_setting/legal.go b/setting/system_setting/legal.go new file mode 100644 index 000000000..cc84d4085 --- /dev/null +++ b/setting/system_setting/legal.go @@ -0,0 +1,21 @@ +package system_setting + +import "github.com/QuantumNous/new-api/setting/config" + +type LegalSettings struct { + UserAgreement string `json:"user_agreement"` + PrivacyPolicy string `json:"privacy_policy"` +} + +var defaultLegalSettings = LegalSettings{ + UserAgreement: "", + PrivacyPolicy: "", +} + +func init() { + config.GlobalConfig.Register("legal", &defaultLegalSettings) +} + +func GetLegalSettings() *LegalSettings { + return &defaultLegalSettings +} diff --git a/setting/system_setting/oidc.go b/setting/system_setting/oidc.go index aed52ae09..307d3b4a4 100644 --- a/setting/system_setting/oidc.go +++ b/setting/system_setting/oidc.go @@ -1,6 +1,6 @@ package system_setting -import "one-api/setting/config" +import "github.com/QuantumNous/new-api/setting/config" type OIDCSettings struct { Enabled bool `json:"enabled"` diff --git a/setting/system_setting/passkey.go b/setting/system_setting/passkey.go new file mode 100644 index 000000000..41855898c --- /dev/null +++ b/setting/system_setting/passkey.go @@ -0,0 +1,50 @@ +package system_setting + +import ( + "net/url" + "strings" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/setting/config" +) + +type PasskeySettings struct { + Enabled bool `json:"enabled"` + RPDisplayName string `json:"rp_display_name"` + RPID string `json:"rp_id"` + Origins string `json:"origins"` + AllowInsecureOrigin bool `json:"allow_insecure_origin"` + UserVerification string `json:"user_verification"` + AttachmentPreference string `json:"attachment_preference"` +} + +var defaultPasskeySettings = PasskeySettings{ + Enabled: false, + RPDisplayName: common.SystemName, + RPID: "", + Origins: "", + AllowInsecureOrigin: false, + UserVerification: "preferred", + AttachmentPreference: "", +} + +func init() { + config.GlobalConfig.Register("passkey", &defaultPasskeySettings) +} + +func GetPasskeySettings() *PasskeySettings { + if defaultPasskeySettings.RPID == "" && ServerAddress != "" { + // 从ServerAddress提取域名作为RPID + // ServerAddress可能是 "https://newapi.pro" 这种格式 + serverAddr := strings.TrimSpace(ServerAddress) + if parsed, err := url.Parse(serverAddr); err == nil && parsed.Host != "" { + defaultPasskeySettings.RPID = parsed.Host + } else { + defaultPasskeySettings.RPID = serverAddr + } + } + if defaultPasskeySettings.Origins == "" || defaultPasskeySettings.Origins == "[]" { + defaultPasskeySettings.Origins = ServerAddress + } + return &defaultPasskeySettings +} diff --git a/setting/user_usable_group.go b/setting/user_usable_group.go index 57e4beecf..a6cdbf94d 100644 --- a/setting/user_usable_group.go +++ b/setting/user_usable_group.go @@ -2,8 +2,9 @@ package setting import ( "encoding/json" - "one-api/common" "sync" + + "github.com/QuantumNous/new-api/common" ) var userUsableGroups = map[string]string{ diff --git a/types/error.go b/types/error.go index a42e84385..9c12034e1 100644 --- a/types/error.go +++ b/types/error.go @@ -4,8 +4,9 @@ import ( "errors" "fmt" "net/http" - "one-api/common" "strings" + + "github.com/QuantumNous/new-api/common" ) type OpenAIError struct { @@ -61,6 +62,9 @@ const ( ErrorCodeConvertRequestFailed ErrorCode = "convert_request_failed" ErrorCodeAccessDenied ErrorCode = "access_denied" + // request error + ErrorCodeBadRequestBody ErrorCode = "bad_request_body" + // response error ErrorCodeReadResponseBodyFailed ErrorCode = "read_response_body_failed" ErrorCodeBadResponseStatusCode ErrorCode = "bad_response_status_code" @@ -69,6 +73,7 @@ const ( ErrorCodeEmptyResponse ErrorCode = "empty_response" ErrorCodeAwsInvokeError ErrorCode = "aws_invoke_error" ErrorCodeModelNotFound ErrorCode = "model_not_found" + ErrorCodePromptBlocked ErrorCode = "prompt_blocked" // sql error ErrorCodeQueryDataError ErrorCode = "query_data_error" @@ -159,6 +164,9 @@ func (e *NewAPIError) ToOpenAIError() OpenAIError { if e.errorCode != ErrorCodeCountTokenFailed { result.Message = common.MaskSensitiveInfo(result.Message) } + if result.Message == "" { + result.Message = string(e.errorType) + } return result } @@ -185,6 +193,9 @@ func (e *NewAPIError) ToClaudeError() ClaudeError { if e.errorCode != ErrorCodeCountTokenFailed { result.Message = common.MaskSensitiveInfo(result.Message) } + if result.Message == "" { + result.Message = string(e.errorType) + } return result } diff --git a/types/price_data.go b/types/price_data.go index ec7fcdfe9..8f6297408 100644 --- a/types/price_data.go +++ b/types/price_data.go @@ -9,17 +9,19 @@ type GroupRatioInfo struct { } type PriceData struct { - ModelPrice float64 - ModelRatio float64 - CompletionRatio float64 - CacheRatio float64 - CacheCreationRatio float64 - ImageRatio float64 - AudioRatio float64 - AudioCompletionRatio float64 - UsePrice bool - ShouldPreConsumedQuota int - GroupRatioInfo GroupRatioInfo + FreeModel bool + ModelPrice float64 + ModelRatio float64 + CompletionRatio float64 + CacheRatio float64 + CacheCreationRatio float64 + ImageRatio float64 + AudioRatio float64 + AudioCompletionRatio float64 + OtherRatios map[string]float64 + UsePrice bool + QuotaToPreConsume int // 预消耗额度 + GroupRatioInfo GroupRatioInfo } type PerCallPriceData struct { @@ -29,5 +31,5 @@ type PerCallPriceData struct { } func (p PriceData) ToSetting() string { - return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.ShouldPreConsumedQuota, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio) + return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, QuotaToPreConsume: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.QuotaToPreConsume, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio) } diff --git a/web/bun.lock b/web/bun.lock index 53467aa5e..3d4a2dfa0 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -10,7 +10,7 @@ "@visactor/react-vchart": "~1.8.8", "@visactor/vchart": "~1.8.8", "@visactor/vchart-semi-theme": "~1.8.8", - "axios": "^0.27.2", + "axios": "1.12.0", "clsx": "^2.1.1", "country-flag-icons": "^1.5.19", "dayjs": "^1.11.11", @@ -687,7 +687,7 @@ "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="], - "axios": ["axios@0.27.2", "", { "dependencies": { "follow-redirects": "^1.14.9", "form-data": "^4.0.0" } }, "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ=="], + "axios": ["axios@1.12.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg=="], "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="], @@ -713,6 +713,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], @@ -895,6 +897,8 @@ "dompurify": ["dompurify@3.2.6", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], "electron-to-chromium": ["electron-to-chromium@1.5.157", "", {}, "sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w=="], @@ -907,6 +911,14 @@ "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], @@ -995,7 +1007,7 @@ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - "form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="], + "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], @@ -1019,6 +1031,10 @@ "geojson-linestring-dissolve": ["geojson-linestring-dissolve@0.0.1", "", {}, "sha512-Y8I2/Ea28R/Xeki7msBcpMvJL2TaPfaPKP8xqueJfQ9/jEhps+iOJxOR2XCBGgVb12Z6XnDb1CMbaPfLepsLaw=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-stdin": ["get-stdin@6.0.0", "", {}, "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g=="], "get-value": ["get-value@2.0.6", "", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="], @@ -1031,6 +1047,8 @@ "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], @@ -1039,6 +1057,10 @@ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], @@ -1229,6 +1251,8 @@ "marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], @@ -1491,6 +1515,8 @@ "protocol-buffers-schema": ["protocol-buffers-schema@3.6.0", "", {}, "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="], + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "qrcode.react": ["qrcode.react@4.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA=="], @@ -1505,7 +1531,7 @@ "rc-checkbox": ["rc-checkbox@3.5.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", "rc-util": "^5.25.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg=="], - "rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="], + "rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="], "rc-dialog": ["rc-dialog@9.6.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", "classnames": "^2.2.6", "rc-motion": "^2.3.0", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg=="], @@ -1949,6 +1975,8 @@ "@lobehub/ui/lucide-react": ["lucide-react@0.484.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-oZy8coK9kZzvqhSgfbGkPtTgyjpBvs3ukLgDPv14dSOZtBtboryWF5o8i3qen7QbGg7JhiJBz5mK1p8YoMZTLQ=="], + "@lobehub/ui/rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="], + "@radix-ui/react-dismissable-layer/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="], "@radix-ui/react-popper/@floating-ui/react-dom": ["@floating-ui/react-dom@0.7.2", "", { "dependencies": { "@floating-ui/dom": "^0.5.3", "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg=="], @@ -1965,8 +1993,6 @@ "@visactor/vrender-kits/roughjs": ["roughjs@4.5.2", "", { "dependencies": { "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-2xSlLDKdsWyFxrveYWk9YQ/Y9UfK38EAMRNkYkMqYBJvPX8abCa9PN0x3w02H8Oa6/0bcZICJU+U95VumPqseg=="], - "antd/rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="], - "antd/scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], diff --git a/web/i18next.config.js b/web/i18next.config.js new file mode 100644 index 000000000..ca6b4a5f3 --- /dev/null +++ b/web/i18next.config.js @@ -0,0 +1,95 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import { defineConfig } from 'i18next-cli'; + +/** @type {import('i18next-cli').I18nextToolkitConfig} */ +export default defineConfig({ + locales: [ + "zh", + "en", + "fr", + "ru" + ], + extract: { + input: [ + "src/**/*.{js,jsx,ts,tsx}" + ], + ignore: [ + "src/i18n/**/*" + ], + output: "src/i18n/locales/{{language}}.json", + ignoredAttributes: [ + "accept", + "align", + "aria-label", + "autoComplete", + "className", + "clipRule", + "color", + "crossOrigin", + "data-index", + "data-name", + "data-testid", + "data-type", + "defaultActiveKey", + "direction", + "editorType", + "field", + "fill", + "fillRule", + "height", + "hoverStyle", + "htmlType", + "id", + "itemKey", + "key", + "keyPrefix", + "layout", + "margin", + "maxHeight", + "mode", + "name", + "overflow", + "placement", + "position", + "rel", + "role", + "rowKey", + "searchPosition", + "selectedStyle", + "shape", + "size", + "style", + "theme", + "trigger", + "uploadTrigger", + "validateStatus", + "value", + "viewBox", + "width" + ], + sort: true, + disablePlurals: false, + removeUnusedKeys: false, + nsSeparator: false, + keySeparator: false, + mergeNamespaces: true + } +}); \ No newline at end of file diff --git a/web/index.html b/web/index.html index 09d87ae1a..b1bcf1713 100644 --- a/web/index.html +++ b/web/index.html @@ -10,6 +10,8 @@ content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用" /> New API + + diff --git a/web/jsconfig.json b/web/jsconfig.json index ced4d0543..170a7cb4c 100644 --- a/web/jsconfig.json +++ b/web/jsconfig.json @@ -6,4 +6,4 @@ } }, "include": ["src/**/*"] -} \ No newline at end of file +} diff --git a/web/package.json b/web/package.json index f014d84b9..5063c6073 100644 --- a/web/package.json +++ b/web/package.json @@ -10,7 +10,7 @@ "@visactor/react-vchart": "~1.8.8", "@visactor/vchart": "~1.8.8", "@visactor/vchart-semi-theme": "~1.8.8", - "axios": "^0.27.2", + "axios": "1.12.0", "clsx": "^2.1.1", "country-flag-icons": "^1.5.19", "dayjs": "^1.11.11", @@ -49,7 +49,11 @@ "lint:fix": "prettier . --write", "eslint": "bunx eslint \"**/*.{js,jsx}\" --cache", "eslint:fix": "bunx eslint \"**/*.{js,jsx}\" --fix --cache", - "preview": "vite preview" + "preview": "vite preview", + "i18n:extract": "bunx i18next-cli extract", + "i18n:status": "bunx i18next-cli status", + "i18n:sync": "bunx i18next-cli sync", + "i18n:lint": "bunx i18next-cli lint" }, "eslintConfig": { "extends": [ @@ -81,7 +85,8 @@ "prettier": "^3.0.0", "tailwindcss": "^3", "typescript": "4.4.2", - "vite": "^5.2.0" + "vite": "^5.2.0", + "i18next-cli": "^1.10.3" }, "prettier": { "singleQuote": true, diff --git a/web/src/App.jsx b/web/src/App.jsx index 635742f91..06e364897 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -51,6 +51,8 @@ import SetupCheck from './components/layout/SetupCheck'; const Home = lazy(() => import('./pages/Home')); const Dashboard = lazy(() => import('./pages/Dashboard')); const About = lazy(() => import('./pages/About')); +const UserAgreement = lazy(() => import('./pages/UserAgreement')); +const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy')); function App() { const location = useLocation(); @@ -301,6 +303,22 @@ function App() { } /> + } key={location.pathname}> + + + } + /> + } key={location.pathname}> + + + } + /> { useState(false); const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false); const [showTwoFA, setShowTwoFA] = useState(false); + const [passkeySupported, setPasskeySupported] = useState(false); + const [passkeyLoading, setPasskeyLoading] = useState(false); + const [agreedToTerms, setAgreedToTerms] = useState(false); + const [hasUserAgreement, setHasUserAgreement] = useState(false); + const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false); const logo = getLogo(); const systemName = getSystemName(); @@ -93,8 +106,18 @@ const LoginForm = () => { setTurnstileEnabled(true); setTurnstileSiteKey(status.turnstile_site_key); } + + // 从 status 获取用户协议和隐私政策的启用状态 + setHasUserAgreement(status.user_agreement_enabled || false); + setHasPrivacyPolicy(status.privacy_policy_enabled || false); }, [status]); + useEffect(() => { + isPasskeySupported() + .then(setPasskeySupported) + .catch(() => setPasskeySupported(false)); + }, []); + useEffect(() => { if (searchParams.get('expired')) { showError(t('未登录或登录已过期,请重新登录')); @@ -102,6 +125,10 @@ const LoginForm = () => { }, []); const onWeChatLoginClicked = () => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } setWechatLoading(true); setShowWeChatLoginModal(true); setWechatLoading(false); @@ -141,6 +168,10 @@ const LoginForm = () => { } async function handleSubmit(e) { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } if (turnstileEnabled && turnstileToken === '') { showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); return; @@ -192,6 +223,10 @@ const LoginForm = () => { // 添加Telegram登录处理函数 const onTelegramLoginClicked = async (response) => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } const fields = [ 'id', 'first_name', @@ -228,6 +263,10 @@ const LoginForm = () => { // 包装的GitHub登录点击处理 const handleGitHubClick = () => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } setGithubLoading(true); try { onGitHubOAuthClicked(status.github_client_id); @@ -239,6 +278,10 @@ const LoginForm = () => { // 包装的OIDC登录点击处理 const handleOIDCClick = () => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } setOidcLoading(true); try { onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id); @@ -250,6 +293,10 @@ const LoginForm = () => { // 包装的LinuxDO登录点击处理 const handleLinuxDOClick = () => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } setLinuxdoLoading(true); try { onLinuxDOOAuthClicked(status.linuxdo_client_id); @@ -266,6 +313,66 @@ const LoginForm = () => { setEmailLoginLoading(false); }; + const handlePasskeyLogin = async () => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } + if (!passkeySupported) { + showInfo('当前环境无法使用 Passkey 登录'); + return; + } + if (!window.PublicKeyCredential) { + showInfo('当前浏览器不支持 Passkey'); + return; + } + + setPasskeyLoading(true); + try { + const beginRes = await API.post('/api/user/passkey/login/begin'); + const { success, message, data } = beginRes.data; + if (!success) { + showError(message || '无法发起 Passkey 登录'); + return; + } + + const publicKeyOptions = prepareCredentialRequestOptions( + data?.options || data?.publicKey || data, + ); + const assertion = await navigator.credentials.get({ + publicKey: publicKeyOptions, + }); + const payload = buildAssertionResult(assertion); + if (!payload) { + showError('Passkey 验证失败,请重试'); + return; + } + + const finishRes = await API.post( + '/api/user/passkey/login/finish', + payload, + ); + const finish = finishRes.data; + if (finish.success) { + userDispatch({ type: 'login', payload: finish.data }); + setUserData(finish.data); + updateAPI(); + showSuccess('登录成功!'); + navigate('/console'); + } else { + showError(finish.message || 'Passkey 登录失败,请重试'); + } + } catch (error) { + if (error?.name === 'AbortError') { + showInfo('已取消 Passkey 登录'); + } else { + showError('Passkey 登录失败,请重试'); + } + } finally { + setPasskeyLoading(false); + } + }; + // 包装的重置密码点击处理 const handleResetPasswordClick = () => { setResetPasswordLoading(true); @@ -385,6 +492,19 @@ const LoginForm = () => {
)} + {status.passkey_login && passkeySupported && ( + + )} + {t('或')} @@ -401,6 +521,44 @@ const LoginForm = () => {
+ {(hasUserAgreement || hasPrivacyPolicy) && ( +
+ setAgreedToTerms(e.target.checked)} + > + + {t('我已阅读并同意')} + {hasUserAgreement && ( + <> + + {t('用户协议')} + + + )} + {hasUserAgreement && hasPrivacyPolicy && t('和')} + {hasPrivacyPolicy && ( + <> + + {t('隐私政策')} + + + )} + + +
+ )} + {!status.self_use_mode_enabled && (
@@ -437,6 +595,18 @@ const LoginForm = () => {
+ {status.passkey_login && passkeySupported && ( + + )}
{ prefix={} /> + {(hasUserAgreement || hasPrivacyPolicy) && ( +
+ setAgreedToTerms(e.target.checked)} + > + + {t('我已阅读并同意')} + {hasUserAgreement && ( + <> + + {t('用户协议')} + + + )} + {hasUserAgreement && hasPrivacyPolicy && t('和')} + {hasPrivacyPolicy && ( + <> + + {t('隐私政策')} + + + )} + + +
+ )} +
diff --git a/web/src/components/auth/RegisterForm.jsx b/web/src/components/auth/RegisterForm.jsx index 9c98bdc3a..436a7b6b8 100644 --- a/web/src/components/auth/RegisterForm.jsx +++ b/web/src/components/auth/RegisterForm.jsx @@ -30,7 +30,7 @@ import { setUserData, } from '../../helpers'; import Turnstile from 'react-turnstile'; -import { Button, Card, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui'; +import { Button, Card, Checkbox, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui'; import Title from '@douyinfe/semi-ui/lib/es/typography/title'; import Text from '@douyinfe/semi-ui/lib/es/typography/text'; import { @@ -82,6 +82,9 @@ const RegisterForm = () => { const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false); const [disableButton, setDisableButton] = useState(false); const [countdown, setCountdown] = useState(30); + const [agreedToTerms, setAgreedToTerms] = useState(false); + const [hasUserAgreement, setHasUserAgreement] = useState(false); + const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false); const logo = getLogo(); const systemName = getSystemName(); @@ -106,6 +109,10 @@ const RegisterForm = () => { setTurnstileEnabled(true); setTurnstileSiteKey(status.turnstile_site_key); } + + // 从 status 获取用户协议和隐私政策的启用状态 + setHasUserAgreement(status.user_agreement_enabled || false); + setHasPrivacyPolicy(status.privacy_policy_enabled || false); }, [status]); useEffect(() => { @@ -505,6 +512,44 @@ const RegisterForm = () => { )} + {(hasUserAgreement || hasPrivacyPolicy) && ( +
+ setAgreedToTerms(e.target.checked)} + > + + {t('我已阅读并同意')} + {hasUserAgreement && ( + <> + + {t('用户协议')} + + + )} + {hasUserAgreement && hasPrivacyPolicy && t('和')} + {hasPrivacyPolicy && ( + <> + + {t('隐私政策')} + + + )} + + +
+ )} +
diff --git a/web/src/components/common/DocumentRenderer/index.jsx b/web/src/components/common/DocumentRenderer/index.jsx new file mode 100644 index 000000000..383afc11d --- /dev/null +++ b/web/src/components/common/DocumentRenderer/index.jsx @@ -0,0 +1,243 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useEffect, useState } from 'react'; +import { API, showError } from '../../../helpers'; +import { Empty, Card, Spin, Typography } from '@douyinfe/semi-ui'; +const { Title } = Typography; +import { + IllustrationConstruction, + IllustrationConstructionDark, +} from '@douyinfe/semi-illustrations'; +import { useTranslation } from 'react-i18next'; +import MarkdownRenderer from '../markdown/MarkdownRenderer'; + +// 检查是否为 URL +const isUrl = (content) => { + try { + new URL(content.trim()); + return true; + } catch { + return false; + } +}; + +// 检查是否为 HTML 内容 +const isHtmlContent = (content) => { + if (!content || typeof content !== 'string') return false; + + // 检查是否包含HTML标签 + const htmlTagRegex = /<\/?[a-z][\s\S]*>/i; + return htmlTagRegex.test(content); +}; + +// 安全地渲染HTML内容 +const sanitizeHtml = (html) => { + // 创建一个临时元素来解析HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = html; + + // 提取样式 + const styles = Array.from(tempDiv.querySelectorAll('style')) + .map(style => style.innerHTML) + .join('\n'); + + // 提取body内容,如果没有body标签则使用全部内容 + const bodyContent = tempDiv.querySelector('body'); + const content = bodyContent ? bodyContent.innerHTML : html; + + return { content, styles }; +}; + +/** + * 通用文档渲染组件 + * @param {string} apiEndpoint - API 接口地址 + * @param {string} title - 文档标题 + * @param {string} cacheKey - 本地存储缓存键 + * @param {string} emptyMessage - 空内容时的提示消息 + */ +const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => { + const { t } = useTranslation(); + const [content, setContent] = useState(''); + const [loading, setLoading] = useState(true); + const [htmlStyles, setHtmlStyles] = useState(''); + const [processedHtmlContent, setProcessedHtmlContent] = useState(''); + + const loadContent = async () => { + // 先从缓存中获取 + const cachedContent = localStorage.getItem(cacheKey) || ''; + if (cachedContent) { + setContent(cachedContent); + processContent(cachedContent); + setLoading(false); + } + + try { + const res = await API.get(apiEndpoint); + const { success, message, data } = res.data; + if (success && data) { + setContent(data); + processContent(data); + localStorage.setItem(cacheKey, data); + } else { + if (!cachedContent) { + showError(message || emptyMessage); + setContent(''); + } + } + } catch (error) { + if (!cachedContent) { + showError(emptyMessage); + setContent(''); + } + } finally { + setLoading(false); + } + }; + + const processContent = (rawContent) => { + if (isHtmlContent(rawContent)) { + const { content: htmlContent, styles } = sanitizeHtml(rawContent); + setProcessedHtmlContent(htmlContent); + setHtmlStyles(styles); + } else { + setProcessedHtmlContent(''); + setHtmlStyles(''); + } + }; + + useEffect(() => { + loadContent(); + }, []); + + // 处理HTML样式注入 + useEffect(() => { + const styleId = `document-renderer-styles-${cacheKey}`; + + if (htmlStyles) { + let styleEl = document.getElementById(styleId); + if (!styleEl) { + styleEl = document.createElement('style'); + styleEl.id = styleId; + styleEl.type = 'text/css'; + document.head.appendChild(styleEl); + } + styleEl.innerHTML = htmlStyles; + } else { + const el = document.getElementById(styleId); + if (el) el.remove(); + } + + return () => { + const el = document.getElementById(styleId); + if (el) el.remove(); + }; + }, [htmlStyles, cacheKey]); + + // 显示加载状态 + if (loading) { + return ( +
+ +
+ ); + } + + // 如果没有内容,显示空状态 + if (!content || content.trim() === '') { + return ( +
+ } + darkModeImage={} + className='p-8' + /> +
+ ); + } + + // 如果是 URL,显示链接卡片 + if (isUrl(content)) { + return ( +
+ +
+ {title} +

+ {t('管理员设置了外部链接,点击下方按钮访问')} +

+ + {t('访问' + title)} + +
+
+
+ ); + } + + // 如果是 HTML 内容,直接渲染 + if (isHtmlContent(content)) { + const { content: htmlContent, styles } = sanitizeHtml(content); + + // 设置样式(如果有的话) + useEffect(() => { + if (styles && styles !== htmlStyles) { + setHtmlStyles(styles); + } + }, [content, styles, htmlStyles]); + + return ( +
+
+
+ {title} +
+
+
+
+ ); + } + + // 其他内容统一使用 Markdown 渲染器 + return ( +
+
+
+ {title} +
+ +
+
+
+
+ ); +}; + +export default DocumentRenderer; \ No newline at end of file diff --git a/web/src/components/common/examples/ChannelKeyViewExample.jsx b/web/src/components/common/examples/ChannelKeyViewExample.jsx new file mode 100644 index 000000000..1bb2998b2 --- /dev/null +++ b/web/src/components/common/examples/ChannelKeyViewExample.jsx @@ -0,0 +1,113 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, Modal } from '@douyinfe/semi-ui'; +import { useSecureVerification } from '../../../hooks/common/useSecureVerification'; +import { createApiCalls } from '../../../services/secureVerification'; +import SecureVerificationModal from '../modals/SecureVerificationModal'; +import ChannelKeyDisplay from '../ui/ChannelKeyDisplay'; + +/** + * 渠道密钥查看组件使用示例 + * 展示如何使用通用安全验证系统 + */ +const ChannelKeyViewExample = ({ channelId }) => { + const { t } = useTranslation(); + const [keyData, setKeyData] = useState(''); + const [showKeyModal, setShowKeyModal] = useState(false); + + // 使用通用安全验证 Hook + const { + isModalVisible, + verificationMethods, + verificationState, + startVerification, + executeVerification, + cancelVerification, + setVerificationCode, + switchVerificationMethod, + } = useSecureVerification({ + onSuccess: (result) => { + // 验证成功后处理结果 + if (result.success && result.data?.key) { + setKeyData(result.data.key); + setShowKeyModal(true); + } + }, + successMessage: t('密钥获取成功'), + }); + + // 开始查看密钥流程 + const handleViewKey = async () => { + const apiCall = createApiCalls.viewChannelKey(channelId); + + await startVerification(apiCall, { + title: t('查看渠道密钥'), + description: t('为了保护账户安全,请验证您的身份。'), + preferredMethod: 'passkey', // 可以指定首选验证方式 + }); + }; + + return ( + <> + {/* 查看密钥按钮 */} + + + {/* 安全验证模态框 */} + + + {/* 密钥显示模态框 */} + setShowKeyModal(false)} + footer={ + + } + width={700} + style={{ maxWidth: '90vw' }} + > + + + + ); +}; + +export default ChannelKeyViewExample; diff --git a/web/src/components/common/modals/SecureVerificationModal.jsx b/web/src/components/common/modals/SecureVerificationModal.jsx new file mode 100644 index 000000000..6c61c291d --- /dev/null +++ b/web/src/components/common/modals/SecureVerificationModal.jsx @@ -0,0 +1,322 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Modal, + Button, + Input, + Typography, + Tabs, + TabPane, + Space, + Spin, +} from '@douyinfe/semi-ui'; + +/** + * 通用安全验证模态框组件 + * 配合 useSecureVerification Hook 使用 + * @param {Object} props + * @param {boolean} props.visible - 是否显示模态框 + * @param {Object} props.verificationMethods - 可用的验证方式 + * @param {Object} props.verificationState - 当前验证状态 + * @param {Function} props.onVerify - 验证回调 + * @param {Function} props.onCancel - 取消回调 + * @param {Function} props.onCodeChange - 验证码变化回调 + * @param {Function} props.onMethodSwitch - 验证方式切换回调 + * @param {string} props.title - 模态框标题 + * @param {string} props.description - 验证描述文本 + */ +const SecureVerificationModal = ({ + visible, + verificationMethods, + verificationState, + onVerify, + onCancel, + onCodeChange, + onMethodSwitch, + title, + description, +}) => { + const { t } = useTranslation(); + const [isAnimating, setIsAnimating] = useState(false); + const [verifySuccess, setVerifySuccess] = useState(false); + + const { has2FA, hasPasskey, passkeySupported } = verificationMethods; + const { method, loading, code } = verificationState; + + useEffect(() => { + if (visible) { + setIsAnimating(true); + setVerifySuccess(false); + } else { + setIsAnimating(false); + } + }, [visible]); + + const handleKeyDown = (e) => { + if (e.key === 'Enter' && code.trim() && !loading && method === '2fa') { + onVerify(method, code); + } + if (e.key === 'Escape' && !loading) { + onCancel(); + } + }; + + // 如果用户没有启用任何验证方式 + if (visible && !has2FA && !hasPasskey) { + return ( + {t('确定')}} + width={500} + style={{ maxWidth: '90vw' }} + > +
+
+ + + +
+ + {t('需要安全验证')} + + + {t('您需要先启用两步验证或 Passkey 才能查看敏感信息。')} + +
+ + {t('请前往个人设置 → 安全设置进行配置。')} + +
+
+ ); + } + + return ( + +
+ {/* 描述信息 */} + {description && ( + + {description} + + )} + + {/* 验证方式选择 */} + + {has2FA && ( + +
+
+ + + + } + style={{ width: '100%' }} + /> +
+ + + {t('从认证器应用中获取验证码,或使用备用码')} + + +
+ + +
+
+
+ )} + + {hasPasskey && passkeySupported && ( + +
+
+
+ + + +
+ + {t('使用 Passkey 验证')} + + + {t('点击验证按钮,使用您的生物特征或安全密钥')} + +
+ +
+ + +
+
+
+ )} +
+
+
+ ); +}; + +export default SecureVerificationModal; diff --git a/web/src/components/common/modals/TwoFactorAuthModal.jsx b/web/src/components/common/modals/TwoFactorAuthModal.jsx index 2a9a8b25b..082e63d79 100644 --- a/web/src/components/common/modals/TwoFactorAuthModal.jsx +++ b/web/src/components/common/modals/TwoFactorAuthModal.jsx @@ -135,7 +135,9 @@ const TwoFactorAuthModal = ({ autoFocus /> - {t('支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。')} + {t( + '支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。', + )}
diff --git a/web/src/components/layout/Footer.jsx b/web/src/components/layout/Footer.jsx index 5c210fca8..c827a581b 100644 --- a/web/src/components/layout/Footer.jsx +++ b/web/src/components/layout/Footer.jsx @@ -142,14 +142,6 @@ const FooterBar = () => { > Midjourney-Proxy - - chatnio - {
@@ -200,15 +207,6 @@ const FooterBar = () => { > New API - & - - One API -
@@ -223,10 +221,23 @@ const FooterBar = () => { return (
{footer ? ( -
+
+
+
+ {t('设计与开发由')} + + New API + +
+
) : ( customFooter )} diff --git a/web/src/components/layout/PageLayout.jsx b/web/src/components/layout/PageLayout.jsx index f8cdfb0cb..6474501dd 100644 --- a/web/src/components/layout/PageLayout.jsx +++ b/web/src/components/layout/PageLayout.jsx @@ -48,9 +48,19 @@ const PageLayout = () => { const { i18n } = useTranslation(); const location = useLocation(); - const shouldHideFooter = - location.pathname.startsWith('/console') || - location.pathname === '/pricing'; + const cardProPages = [ + '/console/channel', + '/console/log', + '/console/redemption', + '/console/user', + '/console/token', + '/console/midjourney', + '/console/task', + '/console/models', + '/pricing', + ]; + + const shouldHideFooter = cardProPages.includes(location.pathname); const shouldInnerPadding = location.pathname.includes('/console') && diff --git a/web/src/components/layout/SiderBar.jsx b/web/src/components/layout/SiderBar.jsx index 793e48355..39d6d4489 100644 --- a/web/src/components/layout/SiderBar.jsx +++ b/web/src/components/layout/SiderBar.jsx @@ -58,7 +58,7 @@ const SiderBar = ({ onNavigate = () => {} }) => { loading: sidebarLoading, } = useSidebar(); - const showSkeleton = useMinimumLoadingTime(sidebarLoading); + const showSkeleton = useMinimumLoadingTime(sidebarLoading, 200); const [selectedKeys, setSelectedKeys] = useState(['home']); const [chatItems, setChatItems] = useState([]); diff --git a/web/src/components/layout/headerbar/LanguageSelector.jsx b/web/src/components/layout/headerbar/LanguageSelector.jsx index cbfd69b35..840e29628 100644 --- a/web/src/components/layout/headerbar/LanguageSelector.jsx +++ b/web/src/components/layout/headerbar/LanguageSelector.jsx @@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com import React from 'react'; import { Button, Dropdown } from '@douyinfe/semi-ui'; import { Languages } from 'lucide-react'; -import { CN, GB } from 'country-flag-icons/react/3x2'; +import { CN, GB, FR, RU } from 'country-flag-icons/react/3x2'; const LanguageSelector = ({ currentLang, onLanguageChange, t }) => { return ( @@ -42,12 +42,26 @@ const LanguageSelector = ({ currentLang, onLanguageChange, t }) => { English + onLanguageChange('fr')} + className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'fr' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`} + > + + Français + + onLanguageChange('ru')} + className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'ru' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`} + > + + Русский + } > + + + + diff --git a/web/src/components/settings/PaymentSetting.jsx b/web/src/components/settings/PaymentSetting.jsx index 90c0bac49..28cbf13b3 100644 --- a/web/src/components/settings/PaymentSetting.jsx +++ b/web/src/components/settings/PaymentSetting.jsx @@ -46,6 +46,7 @@ const PaymentSetting = () => { StripePriceId: '', StripeUnitPrice: 8.0, StripeMinTopUp: 1, + StripePromotionCodesEnabled: false, }); let [loading, setLoading] = useState(false); diff --git a/web/src/components/settings/PersonalSetting.jsx b/web/src/components/settings/PersonalSetting.jsx index 3ba8dcfd3..18d374801 100644 --- a/web/src/components/settings/PersonalSetting.jsx +++ b/web/src/components/settings/PersonalSetting.jsx @@ -19,7 +19,18 @@ For commercial licensing, please contact support@quantumnous.com import React, { useContext, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { API, copy, showError, showInfo, showSuccess } from '../../helpers'; +import { + API, + copy, + showError, + showInfo, + showSuccess, + setStatusData, + prepareCredentialCreationOptions, + buildRegistrationResult, + isPasskeySupported, + setUserData, +} from '../../helpers'; import { UserContext } from '../../context/User'; import { Modal } from '@douyinfe/semi-ui'; import { useTranslation } from 'react-i18next'; @@ -59,6 +70,10 @@ const PersonalSetting = () => { const [disableButton, setDisableButton] = useState(false); const [countdown, setCountdown] = useState(30); const [systemToken, setSystemToken] = useState(''); + const [passkeyStatus, setPasskeyStatus] = useState({ enabled: false }); + const [passkeyRegisterLoading, setPasskeyRegisterLoading] = useState(false); + const [passkeyDeleteLoading, setPasskeyDeleteLoading] = useState(false); + const [passkeySupported, setPasskeySupported] = useState(false); const [notificationSettings, setNotificationSettings] = useState({ warningType: 'email', warningThreshold: 100000, @@ -66,23 +81,52 @@ const PersonalSetting = () => { webhookSecret: '', notificationEmail: '', barkUrl: '', + gotifyUrl: '', + gotifyToken: '', + gotifyPriority: 5, acceptUnsetModelRatioModel: false, recordIpLog: false, }); useEffect(() => { - let status = localStorage.getItem('status'); - if (status) { - status = JSON.parse(status); - setStatus(status); - if (status.turnstile_check) { + let saved = localStorage.getItem('status'); + if (saved) { + const parsed = JSON.parse(saved); + setStatus(parsed); + if (parsed.turnstile_check) { setTurnstileEnabled(true); - setTurnstileSiteKey(status.turnstile_site_key); + setTurnstileSiteKey(parsed.turnstile_site_key); + } else { + setTurnstileEnabled(false); + setTurnstileSiteKey(''); } } - getUserData().then((res) => { - console.log(userState); - }); + // Always refresh status from server to avoid stale flags (e.g., admin just enabled OAuth) + (async () => { + try { + const res = await API.get('/api/status'); + const { success, data } = res.data; + if (success && data) { + setStatus(data); + setStatusData(data); + if (data.turnstile_check) { + setTurnstileEnabled(true); + setTurnstileSiteKey(data.turnstile_site_key); + } else { + setTurnstileEnabled(false); + setTurnstileSiteKey(''); + } + } + } catch (e) { + // ignore and keep local status + } + })(); + + getUserData(); + + isPasskeySupported() + .then(setPasskeySupported) + .catch(() => setPasskeySupported(false)); }, []); useEffect(() => { @@ -108,6 +152,10 @@ const PersonalSetting = () => { webhookSecret: settings.webhook_secret || '', notificationEmail: settings.notification_email || '', barkUrl: settings.bark_url || '', + gotifyUrl: settings.gotify_url || '', + gotifyToken: settings.gotify_token || '', + gotifyPriority: + settings.gotify_priority !== undefined ? settings.gotify_priority : 5, acceptUnsetModelRatioModel: settings.accept_unset_model_ratio_model || false, recordIpLog: settings.record_ip_log || false, @@ -131,11 +179,95 @@ const PersonalSetting = () => { } }; + const loadPasskeyStatus = async () => { + try { + const res = await API.get('/api/user/passkey'); + const { success, data, message } = res.data; + if (success) { + setPasskeyStatus({ + enabled: data?.enabled || false, + last_used_at: data?.last_used_at || null, + backup_eligible: data?.backup_eligible || false, + backup_state: data?.backup_state || false, + }); + } else { + showError(message); + } + } catch (error) { + // 忽略错误,保留默认状态 + } + }; + + const handleRegisterPasskey = async () => { + if (!passkeySupported || !window.PublicKeyCredential) { + showInfo(t('当前设备不支持 Passkey')); + return; + } + setPasskeyRegisterLoading(true); + try { + const beginRes = await API.post('/api/user/passkey/register/begin'); + const { success, message, data } = beginRes.data; + if (!success) { + showError(message || t('无法发起 Passkey 注册')); + return; + } + + const publicKey = prepareCredentialCreationOptions( + data?.options || data?.publicKey || data, + ); + const credential = await navigator.credentials.create({ publicKey }); + const payload = buildRegistrationResult(credential); + if (!payload) { + showError(t('Passkey 注册失败,请重试')); + return; + } + + const finishRes = await API.post( + '/api/user/passkey/register/finish', + payload, + ); + if (finishRes.data.success) { + showSuccess(t('Passkey 注册成功')); + await loadPasskeyStatus(); + } else { + showError(finishRes.data.message || t('Passkey 注册失败,请重试')); + } + } catch (error) { + if (error?.name === 'AbortError') { + showInfo(t('已取消 Passkey 注册')); + } else { + showError(t('Passkey 注册失败,请重试')); + } + } finally { + setPasskeyRegisterLoading(false); + } + }; + + const handleRemovePasskey = async () => { + setPasskeyDeleteLoading(true); + try { + const res = await API.delete('/api/user/passkey'); + const { success, message } = res.data; + if (success) { + showSuccess(t('Passkey 已解绑')); + await loadPasskeyStatus(); + } else { + showError(message || t('操作失败,请重试')); + } + } catch (error) { + showError(t('操作失败,请重试')); + } finally { + setPasskeyDeleteLoading(false); + } + }; + const getUserData = async () => { let res = await API.get(`/api/user/self`); const { success, message, data } = res.data; if (success) { userDispatch({ type: 'login', payload: data }); + setUserData(data); + await loadPasskeyStatus(); } else { showError(message); } @@ -286,6 +418,12 @@ const PersonalSetting = () => { webhook_secret: notificationSettings.webhookSecret, notification_email: notificationSettings.notificationEmail, bark_url: notificationSettings.barkUrl, + gotify_url: notificationSettings.gotifyUrl, + gotify_token: notificationSettings.gotifyToken, + gotify_priority: (() => { + const parsed = parseInt(notificationSettings.gotifyPriority); + return isNaN(parsed) ? 5 : parsed; + })(), accept_unset_model_ratio_model: notificationSettings.acceptUnsetModelRatioModel, record_ip_log: notificationSettings.recordIpLog, @@ -323,6 +461,12 @@ const PersonalSetting = () => { handleSystemTokenClick={handleSystemTokenClick} setShowChangePasswordModal={setShowChangePasswordModal} setShowAccountDeleteModal={setShowAccountDeleteModal} + passkeyStatus={passkeyStatus} + passkeySupported={passkeySupported} + passkeyRegisterLoading={passkeyRegisterLoading} + passkeyDeleteLoading={passkeyDeleteLoading} + onPasskeyRegister={handleRegisterPasskey} + onPasskeyDelete={handleRemovePasskey} /> {/* 右侧:其他设置 */} diff --git a/web/src/components/settings/SystemSetting.jsx b/web/src/components/settings/SystemSetting.jsx index f9a2c019d..4a53ecd7b 100644 --- a/web/src/components/settings/SystemSetting.jsx +++ b/web/src/components/settings/SystemSetting.jsx @@ -30,6 +30,7 @@ import { Spin, Card, Radio, + Select, } from '@douyinfe/semi-ui'; const { Text } = Typography; import { @@ -45,7 +46,6 @@ import { useTranslation } from 'react-i18next'; const SystemSetting = () => { const { t } = useTranslation(); let [inputs, setInputs] = useState({ - PasswordLoginEnabled: '', PasswordRegisterEnabled: '', EmailVerificationEnabled: '', @@ -77,6 +77,13 @@ const SystemSetting = () => { TurnstileSiteKey: '', TurnstileSecretKey: '', RegisterEnabled: '', + 'passkey.enabled': '', + 'passkey.rp_display_name': '', + 'passkey.rp_id': '', + 'passkey.origins': [], + 'passkey.allow_insecure_origin': '', + 'passkey.user_verification': 'preferred', + 'passkey.attachment_preference': '', EmailDomainRestrictionEnabled: '', EmailAliasRestrictionEnabled: '', SMTPSSLEnabled: '', @@ -173,9 +180,25 @@ const SystemSetting = () => { case 'SMTPSSLEnabled': case 'LinuxDOOAuthEnabled': case 'oidc.enabled': + case 'passkey.enabled': + case 'passkey.allow_insecure_origin': case 'WorkerAllowHttpImageRequestEnabled': item.value = toBoolean(item.value); break; + case 'passkey.origins': + // origins是逗号分隔的字符串,直接使用 + item.value = item.value || ''; + break; + case 'passkey.rp_display_name': + case 'passkey.rp_id': + case 'passkey.attachment_preference': + // 确保字符串字段不为null/undefined + item.value = item.value || ''; + break; + case 'passkey.user_verification': + // 确保有默认值 + item.value = item.value || 'preferred'; + break; case 'Price': case 'MinTopUp': item.value = parseFloat(item.value); @@ -188,7 +211,9 @@ const SystemSetting = () => { setInputs(newInputs); setOriginInputs(newInputs); // 同步模式布尔到本地状态 - if (typeof newInputs['fetch_setting.domain_filter_mode'] !== 'undefined') { + if ( + typeof newInputs['fetch_setting.domain_filter_mode'] !== 'undefined' + ) { setDomainFilterMode(!!newInputs['fetch_setting.domain_filter_mode']); } if (typeof newInputs['fetch_setting.ip_filter_mode'] !== 'undefined') { @@ -582,6 +607,45 @@ const SystemSetting = () => { } }; + const submitPasskeySettings = async () => { + // 使用formApi直接获取当前表单值 + const formValues = formApiRef.current?.getValues() || {}; + + const options = []; + + options.push({ + key: 'passkey.rp_display_name', + value: + formValues['passkey.rp_display_name'] || + inputs['passkey.rp_display_name'] || + '', + }); + options.push({ + key: 'passkey.rp_id', + value: formValues['passkey.rp_id'] || inputs['passkey.rp_id'] || '', + }); + options.push({ + key: 'passkey.user_verification', + value: + formValues['passkey.user_verification'] || + inputs['passkey.user_verification'] || + 'preferred', + }); + options.push({ + key: 'passkey.attachment_preference', + value: + formValues['passkey.attachment_preference'] || + inputs['passkey.attachment_preference'] || + '', + }); + options.push({ + key: 'passkey.origins', + value: formValues['passkey.origins'] || inputs['passkey.origins'] || '', + }); + + await updateOptions(options); + }; + const handleCheckboxChange = async (optionKey, event) => { const value = event.target.checked; @@ -641,8 +705,15 @@ const SystemSetting = () => { + - (支持{' '} + {t('仅支持')}{' '} { > new-api-worker - ) + {' '}{t('或其兼容new-api-worker格式的其他版本')} { noLabel extraText={t('SSRF防护开关详细说明')} onChange={(e) => - handleCheckboxChange('fetch_setting.enable_ssrf_protection', e) + handleCheckboxChange( + 'fetch_setting.enable_ssrf_protection', + e, + ) } > {t('启用SSRF防护(推荐开启以保护服务器安全)')} - + { noLabel extraText={t('私有IP访问详细说明')} onChange={(e) => - handleCheckboxChange('fetch_setting.allow_private_ip', e) + handleCheckboxChange( + 'fetch_setting.allow_private_ip', + e, + ) } > - {t('允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)')} + {t( + '允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)', + )} - + { noLabel extraText={t('域名IP过滤详细说明')} onChange={(e) => - handleCheckboxChange('fetch_setting.apply_ip_filter_for_domain', e) + handleCheckboxChange( + 'fetch_setting.apply_ip_filter_for_domain', + e, + ) } style={{ marginBottom: 8 }} > @@ -740,17 +822,23 @@ const SystemSetting = () => { {t(domainFilterMode ? '域名白名单' : '域名黑名单')} - - {t('支持通配符格式,如:example.com, *.api.example.com')} + + {t( + '支持通配符格式,如:example.com, *.api.example.com', + )} { - const selected = val && val.target ? val.target.value : val; + const selected = + val && val.target ? val.target.value : val; const isWhitelist = selected === 'whitelist'; setDomainFilterMode(isWhitelist); - setInputs(prev => ({ + setInputs((prev) => ({ ...prev, 'fetch_setting.domain_filter_mode': isWhitelist, })); @@ -765,9 +853,9 @@ const SystemSetting = () => { onChange={(value) => { setDomainList(value); // 触发Form的onChange事件 - setInputs(prev => ({ + setInputs((prev) => ({ ...prev, - 'fetch_setting.domain_list': value + 'fetch_setting.domain_list': value, })); }} placeholder={t('输入域名后回车,如:example.com')} @@ -784,17 +872,21 @@ const SystemSetting = () => { {t(ipFilterMode ? 'IP白名单' : 'IP黑名单')} - + {t('支持CIDR格式,如:8.8.8.8, 192.168.1.0/24')} { - const selected = val && val.target ? val.target.value : val; + const selected = + val && val.target ? val.target.value : val; const isWhitelist = selected === 'whitelist'; setIpFilterMode(isWhitelist); - setInputs(prev => ({ + setInputs((prev) => ({ ...prev, 'fetch_setting.ip_filter_mode': isWhitelist, })); @@ -809,9 +901,9 @@ const SystemSetting = () => { onChange={(value) => { setIpList(value); // 触发Form的onChange事件 - setInputs(prev => ({ + setInputs((prev) => ({ ...prev, - 'fetch_setting.ip_list': value + 'fetch_setting.ip_list': value, })); }} placeholder={t('输入IP地址后回车,如:8.8.8.8')} @@ -826,7 +918,10 @@ const SystemSetting = () => { > {t('允许的端口')} - + {t('支持单个端口和端口范围,如:80, 443, 8000-8999')} { onChange={(value) => { setAllowedPorts(value); // 触发Form的onChange事件 - setInputs(prev => ({ + setInputs((prev) => ({ ...prev, - 'fetch_setting.allowed_ports': value + 'fetch_setting.allowed_ports': value, })); }} placeholder={t('输入端口后回车,如:80 或 8000-8999')} style={{ width: '100%' }} /> - + {t('端口配置详细说明')} @@ -957,6 +1055,135 @@ const SystemSetting = () => { + + + {t('用以支持基于 WebAuthn 的无密码登录注册')} + + + + + handleCheckboxChange('passkey.enabled', e) + } + > + {t('允许通过 Passkey 登录 & 认证')} + + + + + + + + + + + + + + + + + + + + + + + handleCheckboxChange( + 'passkey.allow_insecure_origin', + e, + ) + } + > + {t('允许不安全的 Origin(HTTP)')} + + + + + + + + + + + + {t('用以防止恶意用户利用临时邮箱批量注册')} diff --git a/web/src/components/settings/personal/cards/AccountManagement.jsx b/web/src/components/settings/personal/cards/AccountManagement.jsx index 515a5c191..d54edb93a 100644 --- a/web/src/components/settings/personal/cards/AccountManagement.jsx +++ b/web/src/components/settings/personal/cards/AccountManagement.jsx @@ -28,6 +28,7 @@ import { Tabs, TabPane, Popover, + Modal, } from '@douyinfe/semi-ui'; import { IconMail, @@ -58,6 +59,12 @@ const AccountManagement = ({ handleSystemTokenClick, setShowChangePasswordModal, setShowAccountDeleteModal, + passkeyStatus, + passkeySupported, + passkeyRegisterLoading, + passkeyDeleteLoading, + onPasskeyRegister, + onPasskeyDelete, }) => { const renderAccountInfo = (accountId, label) => { if (!accountId || accountId === '') { @@ -83,6 +90,14 @@ const AccountManagement = ({ ); }; + const isBound = (accountId) => Boolean(accountId); + const [showTelegramBindModal, setShowTelegramBindModal] = + React.useState(false); + const passkeyEnabled = passkeyStatus?.enabled; + const lastUsedLabel = passkeyStatus?.last_used_at + ? new Date(passkeyStatus.last_used_at).toLocaleString() + : t('尚未使用'); + return ( {/* 卡片头部 */} @@ -142,7 +157,7 @@ const AccountManagement = ({ size='small' onClick={() => setShowEmailBindModal(true)} > - {userState.user && userState.user.email !== '' + {isBound(userState.user?.email) ? t('修改绑定') : t('绑定')} @@ -165,9 +180,11 @@ const AccountManagement = ({ {t('微信')}
- {userState.user && userState.user.wechat_id !== '' - ? t('已绑定') - : t('未绑定')} + {!status.wechat_login + ? t('未启用') + : isBound(userState.user?.wechat_id) + ? t('已绑定') + : t('未绑定')}
@@ -179,7 +196,7 @@ const AccountManagement = ({ disabled={!status.wechat_login} onClick={() => setShowWeChatBindModal(true)} > - {userState.user && userState.user.wechat_id !== '' + {isBound(userState.user?.wechat_id) ? t('修改绑定') : status.wechat_login ? t('绑定') @@ -220,7 +237,7 @@ const AccountManagement = ({ onGitHubOAuthClicked(status.github_client_id) } disabled={ - (userState.user && userState.user.github_id !== '') || + isBound(userState.user?.github_id) || !status.github_oauth } > @@ -264,8 +281,7 @@ const AccountManagement = ({ ) } disabled={ - (userState.user && userState.user.oidc_id !== '') || - !status.oidc_enabled + isBound(userState.user?.oidc_id) || !status.oidc_enabled } > {status.oidc_enabled ? t('绑定') : t('未启用')} @@ -298,26 +314,56 @@ const AccountManagement = ({
{status.telegram_oauth ? ( - userState.user.telegram_id !== '' ? ( - ) : ( -
- -
+ ) ) : ( - )}
+ setShowTelegramBindModal(false)} + footer={null} + > +
+ {t('点击下方按钮通过 Telegram 完成绑定')} +
+
+
+ +
+
+
{/* LinuxDO绑定 */} @@ -350,7 +396,7 @@ const AccountManagement = ({ onLinuxDOOAuthClicked(status.linuxdo_client_id) } disabled={ - (userState.user && userState.user.linux_do_id !== '') || + isBound(userState.user?.linux_do_id) || !status.linuxdo_oauth } > @@ -443,6 +489,77 @@ const AccountManagement = ({ + {/* Passkey 设置 */} + +
+
+
+ +
+
+ + {t('Passkey 登录')} + + + {passkeyEnabled + ? t('已启用 Passkey,无需密码即可登录') + : t('使用 Passkey 实现免密且更安全的登录体验')} + +
+
+ {t('最后使用时间')}:{lastUsedLabel} +
+ {/*{passkeyEnabled && (*/} + {/*
*/} + {/* {t('备份支持')}:*/} + {/* {passkeyStatus?.backup_eligible*/} + {/* ? t('支持备份')*/} + {/* : t('不支持')}*/} + {/* ,{t('备份状态')}:*/} + {/* {passkeyStatus?.backup_state ? t('已备份') : t('未备份')}*/} + {/*
*/} + {/*)}*/} + {!passkeySupported && ( +
+ {t('当前设备不支持 Passkey')} +
+ )} +
+
+
+ +
+
+ {/* 两步验证设置 */} diff --git a/web/src/components/settings/personal/cards/NotificationSettings.jsx b/web/src/components/settings/personal/cards/NotificationSettings.jsx index aad612d2c..c19084a51 100644 --- a/web/src/components/settings/personal/cards/NotificationSettings.jsx +++ b/web/src/components/settings/personal/cards/NotificationSettings.jsx @@ -400,6 +400,7 @@ const NotificationSettings = ({ {t('邮件通知')} {t('Webhook通知')} {t('Bark通知')} + {t('Gotify通知')} - Bark 官方文档 + Bark {t('官方文档')} + + + + + + )} + + {/* Gotify推送设置 */} + {notificationSettings.warningType === 'gotify' && ( + <> + handleFormChange('gotifyUrl', val)} + prefix={} + extraText={t( + '支持HTTP和HTTPS,填写Gotify服务器的完整URL地址', + )} + showClear + rules={[ + { + required: + notificationSettings.warningType === 'gotify', + message: t('请输入Gotify服务器地址'), + }, + { + pattern: /^https?:\/\/.+/, + message: t( + 'Gotify服务器地址必须以http://或https://开头', + ), + }, + ]} + /> + + handleFormChange('gotifyToken', val)} + prefix={} + extraText={t( + '在Gotify服务器创建应用后获得的令牌,用于发送通知', + )} + showClear + rules={[ + { + required: + notificationSettings.warningType === 'gotify', + message: t('请输入Gotify应用令牌'), + }, + ]} + /> + + + handleFormChange('gotifyPriority', val) + } + prefix={} + extraText={t('消息优先级,范围0-10,默认为5')} + style={{ width: '100%', maxWidth: '300px' }} + /> + +
+
+ {t('配置说明')} +
+
+
+ 1. {t('在Gotify服务器的应用管理中创建新应用')} +
+
+ 2.{' '} + {t( + '复制应用的令牌(Token)并填写到上方的应用令牌字段', + )} +
+
3. {t('填写Gotify服务器的完整URL地址')}
+
+ + {t('更多信息请参考')} + {' '} + + Gotify {t('官方文档')}
diff --git a/web/src/components/setup/components/steps/DatabaseStep.jsx b/web/src/components/setup/components/steps/DatabaseStep.jsx index 04dd76a3c..d8d1d4f9f 100644 --- a/web/src/components/setup/components/steps/DatabaseStep.jsx +++ b/web/src/components/setup/components/steps/DatabaseStep.jsx @@ -25,29 +25,55 @@ import { Banner } from '@douyinfe/semi-ui'; * 显示当前数据库类型和相关警告信息 */ const DatabaseStep = ({ setupStatus, renderNavigationButtons, t }) => { + // 检测是否在 Electron 环境中运行 + const isElectron = + typeof window !== 'undefined' && window.electron?.isElectron; + return ( <> {/* 数据库警告 */} {setupStatus.database_type === 'sqlite' && ( -

- {t( - '您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!', - )} -

-

- + isElectron ? ( +

+

{t( - '建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。', + '您的数据将安全地存储在本地计算机上。所有配置、用户信息和使用记录都会自动保存,关闭应用后不会丢失。', )} - -

-
+

+ {window.electron?.dataDir && ( +

+ {t('数据存储位置:')} +
+ + {window.electron.dataDir} + +

+ )} +

+ 💡 {t('提示:如需备份数据,只需复制上述目录即可')} +

+
+ ) : ( +
+

+ {t( + '您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!', + )} +

+

+ + {t( + '建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。', + )} + +

+
+ ) } className='!rounded-lg' fullMode={false} diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index dd620fe01..c9c284abc 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -56,8 +56,10 @@ import { } from '../../../../helpers'; import ModelSelectModal from './ModelSelectModal'; import JSONEditor from '../../../common/ui/JSONEditor'; -import TwoFactorAuthModal from '../../../common/modals/TwoFactorAuthModal'; +import SecureVerificationModal from '../../../common/modals/SecureVerificationModal'; import ChannelKeyDisplay from '../../../common/ui/ChannelKeyDisplay'; +import { useSecureVerification } from '../../../../hooks/common/useSecureVerification'; +import { createApiCalls } from '../../../../services/secureVerification'; import { IconSave, IconClose, @@ -66,6 +68,8 @@ import { IconCode, IconGlobe, IconBolt, + IconChevronUp, + IconChevronDown, } from '@douyinfe/semi-icons'; const { Text, Title } = Typography; @@ -87,22 +91,7 @@ const REGION_EXAMPLE = { // 支持并且已适配通过接口获取模型列表的渠道类型 const MODEL_FETCHABLE_TYPES = new Set([ - 1, - 4, - 14, - 34, - 17, - 26, - 24, - 47, - 25, - 20, - 23, - 31, - 35, - 40, - 42, - 48, + 1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 35, 40, 42, 48, 43, ]); function type2secretPrompt(type) { @@ -118,10 +107,12 @@ function type2secretPrompt(type) { return '按照如下格式输入:AppId|SecretId|SecretKey'; case 33: return '按照如下格式输入:Ak|Sk|Region'; + case 45: + return '请输入渠道对应的鉴权密钥, 豆包语音输入:AppId|AccessToken'; case 50: return '按照如下格式输入: AccessKey|SecretKey, 如果上游是New API,则直接输ApiKey'; case 51: - return '按照如下格式输入: Access Key ID|Secret Access Key'; + return '按照如下格式输入: AccessKey|SecretAccessKey'; default: return '请输入渠道对应的鉴权密钥'; } @@ -164,6 +155,14 @@ const EditChannelModal = (props) => { settings: '', // 仅 Vertex: 密钥格式(存入 settings.vertex_key_type) vertex_key_type: 'json', + // 仅 AWS: 密钥格式和区域(存入 settings.aws_key_type 和 settings.aws_region) + aws_key_type: 'ak_sk', + // 企业账户设置 + is_enterprise_account: false, + // 字段透传控制默认值 + allow_service_tier: false, + disable_store: false, // false = 允许透传(默认开启) + allow_safety_identifier: false, }; const [batch, setBatch] = useState(false); const [multiToSingle, setMultiToSingle] = useState(false); @@ -189,13 +188,11 @@ const EditChannelModal = (props) => { const [channelSearchValue, setChannelSearchValue] = useState(''); const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式 const [keyMode, setKeyMode] = useState('append'); // 密钥模式:replace(覆盖)或 append(追加) + const [isEnterpriseAccount, setIsEnterpriseAccount] = useState(false); // 是否为企业账户 - // 2FA验证查看密钥相关状态 - const [twoFAState, setTwoFAState] = useState({ + // 密钥显示状态 + const [keyDisplayState, setKeyDisplayState] = useState({ showModal: false, - code: '', - loading: false, - showKey: false, keyData: '', }); @@ -204,18 +201,63 @@ const EditChannelModal = (props) => { const [verifyCode, setVerifyCode] = useState(''); const [verifyLoading, setVerifyLoading] = useState(false); + // 表单块导航相关状态 + const formSectionRefs = useRef({ + basicInfo: null, + apiConfig: null, + modelConfig: null, + advancedSettings: null, + channelExtraSettings: null, + }); + const [currentSectionIndex, setCurrentSectionIndex] = useState(0); + const formSections = [ + 'basicInfo', + 'apiConfig', + 'modelConfig', + 'advancedSettings', + 'channelExtraSettings', + ]; + const formContainerRef = useRef(null); + // 2FA状态更新辅助函数 const updateTwoFAState = (updates) => { setTwoFAState((prev) => ({ ...prev, ...updates })); }; + // 使用通用安全验证 Hook + const { + isModalVisible, + verificationMethods, + verificationState, + withVerification, + executeVerification, + cancelVerification, + setVerificationCode, + switchVerificationMethod, + } = useSecureVerification({ + onSuccess: (result) => { + // 验证成功后显示密钥 + console.log('Verification success, result:', result); + if (result && result.success && result.data?.key) { + showSuccess(t('密钥获取成功')); + setKeyDisplayState({ + showModal: true, + keyData: result.data.key, + }); + } else if (result && result.key) { + // 直接返回了 key(没有包装在 data 中) + showSuccess(t('密钥获取成功')); + setKeyDisplayState({ + showModal: true, + keyData: result.key, + }); + } + }, + }); - // 重置2FA状态 - const resetTwoFAState = () => { - setTwoFAState({ + // 重置密钥显示状态 + const resetKeyDisplayState = () => { + setKeyDisplayState({ showModal: false, - code: '', - loading: false, - showKey: false, keyData: '', }); }; @@ -227,6 +269,43 @@ const EditChannelModal = (props) => { setVerifyLoading(false); }; + // 表单导航功能 + const scrollToSection = (sectionKey) => { + const sectionElement = formSectionRefs.current[sectionKey]; + if (sectionElement) { + sectionElement.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'nearest', + }); + } + }; + + const navigateToSection = (direction) => { + const availableSections = formSections.filter((section) => { + if (section === 'apiConfig') { + return showApiConfigCard; + } + return true; + }); + + let newIndex; + if (direction === 'up') { + newIndex = + currentSectionIndex > 0 + ? currentSectionIndex - 1 + : availableSections.length - 1; + } else { + newIndex = + currentSectionIndex < availableSections.length - 1 + ? currentSectionIndex + 1 + : 0; + } + + setCurrentSectionIndex(newIndex); + scrollToSection(availableSections[newIndex]); + }; + // 渠道额外设置状态 const [channelSettings, setChannelSettings] = useState({ force_format: false, @@ -344,7 +423,10 @@ const EditChannelModal = (props) => { break; case 45: localModels = getChannelModels(value); - setInputs((prevInputs) => ({ ...prevInputs, base_url: 'https://ark.cn-beijing.volces.com' })); + setInputs((prevInputs) => ({ + ...prevInputs, + base_url: 'https://ark.cn-beijing.volces.com', + })); break; default: localModels = getChannelModels(value); @@ -437,15 +519,43 @@ const EditChannelModal = (props) => { parsedSettings.azure_responses_version || ''; // 读取 Vertex 密钥格式 data.vertex_key_type = parsedSettings.vertex_key_type || 'json'; + // 读取 AWS 密钥格式和区域 + data.aws_key_type = parsedSettings.aws_key_type || 'ak_sk'; + // 读取企业账户设置 + data.is_enterprise_account = + parsedSettings.openrouter_enterprise === true; + // 读取字段透传控制设置 + data.allow_service_tier = parsedSettings.allow_service_tier || false; + data.disable_store = parsedSettings.disable_store || false; + data.allow_safety_identifier = + parsedSettings.allow_safety_identifier || false; } catch (error) { console.error('解析其他设置失败:', error); data.azure_responses_version = ''; data.region = ''; data.vertex_key_type = 'json'; + data.aws_key_type = 'ak_sk'; + data.is_enterprise_account = false; + data.allow_service_tier = false; + data.disable_store = false; + data.allow_safety_identifier = false; } } else { // 兼容历史数据:老渠道没有 settings 时,默认按 json 展示 data.vertex_key_type = 'json'; + data.aws_key_type = 'ak_sk'; + data.is_enterprise_account = false; + data.allow_service_tier = false; + data.disable_store = false; + data.allow_safety_identifier = false; + } + + if ( + data.type === 45 && + (!data.base_url || + (typeof data.base_url === 'string' && data.base_url.trim() === '')) + ) { + data.base_url = 'https://ark.cn-beijing.volces.com'; } setInputs(data); @@ -457,6 +567,8 @@ const EditChannelModal = (props) => { } else { setAutoBan(true); } + // 同步企业账户状态 + setIsEnterpriseAccount(data.is_enterprise_account || false); setBasicModels(getChannelModels(data.type)); // 同步更新channelSettings状态显示 setChannelSettings({ @@ -585,42 +697,33 @@ const EditChannelModal = (props) => { } }; - // 使用TwoFactorAuthModal的验证函数 - const handleVerify2FA = async () => { - if (!verifyCode) { - showError(t('请输入验证码或备用码')); - return; - } - - setVerifyLoading(true); + // 查看渠道密钥(透明验证) + const handleShow2FAModal = async () => { try { - const res = await API.post(`/api/channel/${channelId}/key`, { - code: verifyCode, - }); - if (res.data.success) { - // 验证成功,显示密钥 - updateTwoFAState({ + // 使用 withVerification 包装,会自动处理需要验证的情况 + const result = await withVerification( + createApiCalls.viewChannelKey(channelId), + { + title: t('查看渠道密钥'), + description: t('为了保护账户安全,请验证您的身份。'), + preferredMethod: 'passkey', // 优先使用 Passkey + }, + ); + + // 如果直接返回了结果(已验证),显示密钥 + if (result && result.success && result.data?.key) { + showSuccess(t('密钥获取成功')); + setKeyDisplayState({ showModal: true, - showKey: true, - keyData: res.data.data.key, + keyData: result.data.key, }); - reset2FAVerifyState(); - showSuccess(t('验证成功')); - } else { - showError(res.data.message); } } catch (error) { - showError(t('获取密钥失败')); - } finally { - setVerifyLoading(false); + console.error('Failed to view channel key:', error); + showError(error.message || t('获取密钥失败')); } }; - // 显示2FA验证模态框 - 使用TwoFactorAuthModal - const handleShow2FAModal = () => { - setShow2FAVerifyModal(true); - }; - useEffect(() => { const modelMap = new Map(); @@ -696,6 +799,8 @@ const EditChannelModal = (props) => { fetchModelGroups(); // 重置手动输入模式状态 setUseManualInput(false); + // 重置导航状态 + setCurrentSectionIndex(0); } else { // 统一的模态框关闭重置逻辑 resetModalState(); @@ -716,16 +821,16 @@ const EditChannelModal = (props) => { }); // 重置密钥模式状态 setKeyMode('append'); + // 重置企业账户状态 + setIsEnterpriseAccount(false); // 清空表单中的key_mode字段 if (formApiRef.current) { formApiRef.current.setValue('key_mode', undefined); } // 重置本地输入,避免下次打开残留上一次的 JSON 字段值 setInputs(getInitValues()); - // 重置2FA状态 - resetTwoFAState(); - // 重置2FA验证状态 - reset2FAVerifyState(); + // 重置密钥显示状态 + resetKeyDisplayState(); }; const handleVertexUploadChange = ({ fileList }) => { @@ -826,7 +931,9 @@ const EditChannelModal = (props) => { delete localInputs.key; } } else { - localInputs.key = batch ? JSON.stringify(keys) : JSON.stringify(keys[0]); + localInputs.key = batch + ? JSON.stringify(keys) + : JSON.stringify(keys[0]); } } } @@ -846,7 +953,10 @@ const EditChannelModal = (props) => { showInfo(t('请至少选择一个模型!')); return; } - if (localInputs.type === 45 && (!localInputs.base_url || localInputs.base_url.trim() === '')) { + if ( + localInputs.type === 45 && + (!localInputs.base_url || localInputs.base_url.trim() === '') + ) { showInfo(t('请输入API地址!')); return; } @@ -879,6 +989,40 @@ const EditChannelModal = (props) => { }; localInputs.setting = JSON.stringify(channelExtraSettings); + // 处理 settings 字段(包括企业账户设置和字段透传控制) + let settings = {}; + if (localInputs.settings) { + try { + settings = JSON.parse(localInputs.settings); + } catch (error) { + console.error('解析settings失败:', error); + } + } + + // type === 20: 设置企业账户标识,无论是true还是false都要传到后端 + if (localInputs.type === 20) { + settings.openrouter_enterprise = + localInputs.is_enterprise_account === true; + } + + // type === 33 (AWS): 保存 aws_key_type 到 settings + if (localInputs.type === 33) { + settings.aws_key_type = localInputs.aws_key_type || 'ak_sk'; + } + + // type === 1 (OpenAI) 或 type === 14 (Claude): 设置字段透传控制(显式保存布尔值) + if (localInputs.type === 1 || localInputs.type === 14) { + settings.allow_service_tier = localInputs.allow_service_tier === true; + // 仅 OpenAI 渠道需要 store 和 safety_identifier + if (localInputs.type === 1) { + settings.disable_store = localInputs.disable_store === true; + settings.allow_safety_identifier = + localInputs.allow_safety_identifier === true; + } + } + + localInputs.settings = JSON.stringify(settings); + // 清理不需要发送到后端的字段 delete localInputs.force_format; delete localInputs.thinking_to_content; @@ -886,8 +1030,15 @@ const EditChannelModal = (props) => { delete localInputs.pass_through_body_enabled; delete localInputs.system_prompt; delete localInputs.system_prompt_override; + delete localInputs.is_enterprise_account; // 顶层的 vertex_key_type 不应发送给后端 delete localInputs.vertex_key_type; + // 顶层的 aws_key_type 不应发送给后端 + delete localInputs.aws_key_type; + // 清理字段透传控制的临时字段 + delete localInputs.allow_service_tier; + delete localInputs.disable_store; + delete localInputs.allow_safety_identifier; let res; localInputs.auto_ban = localInputs.auto_ban ? 1 : 0; @@ -927,6 +1078,56 @@ const EditChannelModal = (props) => { } }; + // 密钥去重函数 + const deduplicateKeys = () => { + const currentKey = formApiRef.current?.getValue('key') || inputs.key || ''; + + if (!currentKey.trim()) { + showInfo(t('请先输入密钥')); + return; + } + + // 按行分割密钥 + const keyLines = currentKey.split('\n'); + const beforeCount = keyLines.length; + + // 使用哈希表去重,保持原有顺序 + const keySet = new Set(); + const deduplicatedKeys = []; + + keyLines.forEach((line) => { + const trimmedLine = line.trim(); + if (trimmedLine && !keySet.has(trimmedLine)) { + keySet.add(trimmedLine); + deduplicatedKeys.push(trimmedLine); + } + }); + + const afterCount = deduplicatedKeys.length; + const deduplicatedKeyText = deduplicatedKeys.join('\n'); + + // 更新表单和状态 + if (formApiRef.current) { + formApiRef.current.setValue('key', deduplicatedKeyText); + } + handleInputChange('key', deduplicatedKeyText); + + // 显示去重结果 + const message = t( + '去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥', + { + before: beforeCount, + after: afterCount, + }, + ); + + if (beforeCount === afterCount) { + showInfo(t('未发现重复密钥')); + } else { + showSuccess(message); + } + }; + const addCustomModels = () => { if (customModel.trim() === '') return; const modelArray = customModel.split(',').map((model) => model.trim()); @@ -1022,24 +1223,41 @@ const EditChannelModal = (props) => { )} {batch && ( - { - setMultiToSingle((prev) => !prev); - setInputs((prev) => { - const newInputs = { ...prev }; - if (!multiToSingle) { - newInputs.multi_key_mode = multiKeyMode; - } else { - delete newInputs.multi_key_mode; - } - return newInputs; - }); - }} - > - {t('密钥聚合模式')} - + <> + { + setMultiToSingle((prev) => { + const nextValue = !prev; + setInputs((prevInputs) => { + const newInputs = { ...prevInputs }; + if (nextValue) { + newInputs.multi_key_mode = multiKeyMode; + } else { + delete newInputs.multi_key_mode; + } + return newInputs; + }); + return nextValue; + }); + }} + > + {t('密钥聚合模式')} + + + {inputs.type !== 41 && ( + + )} + )} ) : null; @@ -1136,7 +1354,41 @@ const EditChannelModal = (props) => { visible={props.visible} width={isMobile ? '100%' : 600} footer={ -
+
+
+
- )} - {batchExtra} -
- } - showClear - /> - ) - ) : ( - <> - {inputs.type === 41 && (inputs.vertex_key_type || 'json') === 'json' ? ( - <> - {!batch && ( -
- - {t('密钥输入方式')} - - - - - -
- )} - {batch && ( - - )} + {inputs.type === 20 && ( + { + setIsEnterpriseAccount(value); + handleInputChange('is_enterprise_account', value); + }} + extraText={t( + '企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选', + )} + initValue={inputs.is_enterprise_account} + /> + )} - {useManualInput && !batch ? ( - - handleInputChange('key', value) - } - extraText={ -
- - {t('请输入完整的 JSON 格式密钥内容')} - - {isEdit && - isMultiKeyChannel && - keyMode === 'append' && ( - - {t( - '追加模式:新密钥将添加到现有密钥列表的末尾', - )} - - )} - {isEdit && ( - - )} - {batchExtra} -
- } - autosize - showClear - /> - ) : ( - } - dragMainText={t('点击上传文件或拖拽文件到这里')} - dragSubText={t('仅支持 JSON 文件')} - style={{ marginTop: 10 }} - uploadTrigger='custom' - beforeUpload={() => false} - onChange={handleVertexUploadChange} - fileList={vertexFileList} - rules={ - isEdit - ? [] - : [ - { - required: true, - message: t('请上传密钥文件'), - }, - ] - } - extraText={batchExtra} - /> + handleInputChange('name', value)} + autoComplete='new-password' + /> + + {inputs.type === 33 && ( + <> + { + handleChannelOtherSettingsChange('aws_key_type', value); + }} + extraText={t( + 'AK/SK 模式:使用 AccessKey 和 SecretAccessKey;API Key 模式:使用 API Key', )} - - ) : ( - + + )} + + {inputs.type === 41 && ( + { + // 更新设置中的 vertex_key_type + handleChannelOtherSettingsChange( + 'vertex_key_type', + value, + ); + // 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件 + if (value === 'api_key') { + setBatch(false); + setUseManualInput(false); + setVertexKeys([]); + setVertexFileList([]); + if (formApiRef.current) { + formApiRef.current.setValue('vertex_files', []); + } + } + }} + extraText={ + inputs.vertex_key_type === 'api_key' + ? t('API Key 模式下不支持批量创建') + : t('JSON 模式支持手动输入或上传服务账号 JSON') + } + /> + )} + {batch ? ( + inputs.type === 41 && + (inputs.vertex_key_type || 'json') === 'json' ? ( + } + dragMainText={t('点击上传文件或拖拽文件到这里')} + dragSubText={t('仅支持 JSON 文件,支持多文件')} + style={{ marginTop: 10 }} + uploadTrigger='custom' + beforeUpload={() => false} + onChange={handleVertexUploadChange} + fileList={vertexFileList} + rules={ + isEdit + ? [] + : [ + { + required: true, + message: t('请上传密钥文件'), + }, + ] + } + extraText={batchExtra} + /> + ) : ( + handleInputChange('key', value)} extraText={ -
+
{isEdit && isMultiKeyChannel && keyMode === 'append' && ( @@ -1495,875 +1619,1202 @@ const EditChannelModal = (props) => { } showClear /> - )} - - )} + ) + ) : ( + <> + {inputs.type === 41 && + (inputs.vertex_key_type || 'json') === 'json' ? ( + <> + {!batch && ( +
+ + {t('密钥输入方式')} + + + + + +
+ )} - {isEdit && isMultiKeyChannel && ( - setKeyMode(value)} - extraText={ - - {keyMode === 'replace' - ? t('覆盖模式:将完全替换现有的所有密钥') - : t('追加模式:将新密钥添加到现有密钥列表末尾')} - - } - /> - )} - {batch && multiToSingle && ( - <> + {batch && ( + + )} + + {useManualInput && !batch ? ( + + handleInputChange('key', value) + } + extraText={ +
+ + {t('请输入完整的 JSON 格式密钥内容')} + + {isEdit && + isMultiKeyChannel && + keyMode === 'append' && ( + + {t( + '追加模式:新密钥将添加到现有密钥列表的末尾', + )} + + )} + {isEdit && ( + + )} + {batchExtra} +
+ } + autosize + showClear + /> + ) : ( + } + dragMainText={t('点击上传文件或拖拽文件到这里')} + dragSubText={t('仅支持 JSON 文件')} + style={{ marginTop: 10 }} + uploadTrigger='custom' + beforeUpload={() => false} + onChange={handleVertexUploadChange} + fileList={vertexFileList} + rules={ + isEdit + ? [] + : [ + { + required: true, + message: t('请上传密钥文件'), + }, + ] + } + extraText={batchExtra} + /> + )} + + ) : ( + + handleInputChange('key', value) + } + extraText={ +
+ {isEdit && + isMultiKeyChannel && + keyMode === 'append' && ( + + {t( + '追加模式:新密钥将添加到现有密钥列表的末尾', + )} + + )} + {isEdit && ( + + )} + {batchExtra} +
+ } + showClear + /> + )} + + )} + + {isEdit && isMultiKeyChannel && ( { - setMultiKeyMode(value); - handleInputChange('multi_key_mode', value); - }} + value={keyMode} + onChange={(value) => setKeyMode(value)} + extraText={ + + {keyMode === 'replace' + ? t('覆盖模式:将完全替换现有的所有密钥') + : t('追加模式:将新密钥添加到现有密钥列表末尾')} + + } /> - {inputs.multi_key_mode === 'polling' && ( - + { + setMultiKeyMode(value); + handleInputChange('multi_key_mode', value); + }} /> - )} - - )} + {inputs.multi_key_mode === 'polling' && ( + + )} + + )} - {inputs.type === 18 && ( - handleInputChange('other', value)} - showClear - /> - )} + {inputs.type === 18 && ( + handleInputChange('other', value)} + showClear + /> + )} - {inputs.type === 41 && ( - handleInputChange('other', value)} - rules={[{ required: true, message: t('请填写部署地区') }]} - template={REGION_EXAMPLE} - templateLabel={t('填入模板')} - editorType='region' - formApi={formApiRef.current} - extraText={t('设置默认地区和特定模型的专用地区')} - /> - )} + {inputs.type === 41 && ( + handleInputChange('other', value)} + rules={[ + { required: true, message: t('请填写部署地区') }, + ]} + template={REGION_EXAMPLE} + templateLabel={t('填入模板')} + editorType='region' + formApi={formApiRef.current} + extraText={t('设置默认地区和特定模型的专用地区')} + /> + )} - {inputs.type === 21 && ( - handleInputChange('other', value)} - showClear - /> - )} + {inputs.type === 21 && ( + handleInputChange('other', value)} + showClear + /> + )} - {inputs.type === 39 && ( - handleInputChange('other', value)} - showClear - /> - )} + {inputs.type === 39 && ( + handleInputChange('other', value)} + showClear + /> + )} - {inputs.type === 49 && ( - handleInputChange('other', value)} - showClear - /> - )} + {inputs.type === 49 && ( + handleInputChange('other', value)} + showClear + /> + )} - {inputs.type === 1 && ( - - handleInputChange('openai_organization', value) - } - /> - )} - + {inputs.type === 1 && ( + + handleInputChange('openai_organization', value) + } + /> + )} + +
{/* API Configuration Card */} {showApiConfigCard && ( - - {/* Header: API Config */} -
- - - -
- - {t('API 配置')} - -
- {t('API 地址和相关配置')} +
(formSectionRefs.current.apiConfig = el)}> + + {/* Header: API Config */} +
+ + + +
+ + {t('API 配置')} + +
+ {t('API 地址和相关配置')} +
-
- {inputs.type === 40 && ( - + {t('邀请链接')}: + + window.open( + 'https://cloud.siliconflow.cn/i/hij0YNTZ', + ) + } + > + https://cloud.siliconflow.cn/i/hij0YNTZ + +
+ } + className='!rounded-lg' + /> + )} + + {inputs.type === 3 && ( + <> +
- {t('邀请链接')}: - - window.open( - 'https://cloud.siliconflow.cn/i/hij0YNTZ', + + handleInputChange('base_url', value) + } + showClear + /> +
+
+ + handleInputChange('other', value) + } + showClear + /> +
+
+ + handleChannelOtherSettingsChange( + 'azure_responses_version', + value, ) } - > - https://cloud.siliconflow.cn/i/hij0YNTZ - + showClear + />
- } - className='!rounded-lg' - /> - )} + + )} - {inputs.type === 3 && ( - <> + {inputs.type === 8 && ( + <> + +
+ + handleInputChange('base_url', value) + } + showClear + /> +
+ + )} + + {inputs.type === 37 && ( -
- - handleInputChange('base_url', value) - } - showClear - /> -
-
- - handleInputChange('other', value) - } - showClear - /> -
-
- - handleChannelOtherSettingsChange( - 'azure_responses_version', - value, - ) - } - showClear - /> -
- - )} + )} - {inputs.type === 8 && ( - <> - -
- - handleInputChange('base_url', value) - } - showClear - /> -
- - )} - - {inputs.type === 37 && ( - + + handleInputChange('base_url', value) + } + showClear + extraText={t( + '对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写', + )} + /> +
)} - className='!rounded-lg' - /> - )} - {inputs.type !== 3 && - inputs.type !== 8 && - inputs.type !== 22 && - inputs.type !== 36 && - inputs.type !== 45 && ( + {inputs.type === 22 && (
handleInputChange('base_url', value) } showClear - extraText={t( - '对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写', - )} />
)} - {inputs.type === 22 && ( -
- - handleInputChange('base_url', value) - } - showClear - /> -
- )} - - {inputs.type === 36 && ( -
- - handleInputChange('base_url', value) - } - showClear - /> -
- )} - - {inputs.type === 45 && ( + {inputs.type === 36 && (
- - handleInputChange('base_url', value) - } - optionList={[ - { - value: 'https://ark.cn-beijing.volces.com', - label: 'https://ark.cn-beijing.volces.com' - }, - { - value: 'https://ark.ap-southeast.bytepluses.com', - label: 'https://ark.ap-southeast.bytepluses.com' - } - ]} - defaultValue='https://ark.cn-beijing.volces.com' + + handleInputChange('base_url', value) + } + showClear />
- )} - + )} + + {inputs.type === 45 && ( +
+ + handleInputChange('base_url', value) + } + optionList={[ + { + value: 'https://ark.cn-beijing.volces.com', + label: 'https://ark.cn-beijing.volces.com', + }, + { + value: + 'https://ark.ap-southeast.bytepluses.com', + label: + 'https://ark.ap-southeast.bytepluses.com', + }, + ]} + defaultValue='https://ark.cn-beijing.volces.com' + /> +
+ )} + +
)} {/* Model Configuration Card */} - - {/* Header: Model Config */} -
- - - -
- - {t('模型配置')} - -
- {t('模型选择和映射设置')} +
(formSectionRefs.current.modelConfig = el)}> + + {/* Header: Model Config */} +
+ + + +
+ + {t('模型配置')} + +
+ {t('模型选择和映射设置')} +
-
- handleInputChange('models', value)} - renderSelectedItem={(optionNode) => { - const modelName = String(optionNode?.value ?? ''); - return { - isRenderInTag: true, - content: ( - { - e.stopPropagation(); - const ok = await copy(modelName); - if (ok) { - showSuccess( - t('已复制:{{name}}', { name: modelName }), - ); - } else { + handleInputChange('models', value)} + renderSelectedItem={(optionNode) => { + const modelName = String(optionNode?.value ?? ''); + return { + isRenderInTag: true, + content: ( + { + e.stopPropagation(); + const ok = await copy(modelName); + if (ok) { + showSuccess( + t('已复制:{{name}}', { name: modelName }), + ); + } else { + showError(t('复制失败')); + } + }} + > + {optionNode.label || modelName} + + ), + }; + }} + extraText={ + + + + {MODEL_FETCHABLE_TYPES.has(inputs.type) && ( + + )} + + + {modelGroups && + modelGroups.length > 0 && + modelGroups.map((group) => ( + + ))} + + } + /> + + setCustomModel(value.trim())} + value={customModel} + suffix={ - - {MODEL_FETCHABLE_TYPES.has(inputs.type) && ( - - )} - - - {modelGroups && - modelGroups.length > 0 && - modelGroups.map((group) => ( - - ))} - - } - /> + } + /> - setCustomModel(value.trim())} - value={customModel} - suffix={ - - } - /> + + handleInputChange('test_model', value) + } + showClear + /> - handleInputChange('test_model', value)} - showClear - /> - - - handleInputChange('model_mapping', value) - } - template={MODEL_MAPPING_EXAMPLE} - templateLabel={t('填入模板')} - editorType='keyValue' - formApi={formApiRef.current} - extraText={t('键为请求中的模型名称,值为要替换的模型名称')} - /> - + + handleInputChange('model_mapping', value) + } + template={MODEL_MAPPING_EXAMPLE} + templateLabel={t('填入模板')} + editorType='keyValue' + formApi={formApiRef.current} + extraText={t( + '键为请求中的模型名称,值为要替换的模型名称', + )} + /> + +
{/* Advanced Settings Card */} - - {/* Header: Advanced Settings */} -
- - - -
- - {t('高级设置')} - -
- {t('渠道的高级配置选项')} +
(formSectionRefs.current.advancedSettings = el)} + > + + {/* Header: Advanced Settings */} +
+ + + +
+ + {t('高级设置')} + +
+ {t('渠道的高级配置选项')} +
-
- handleInputChange('groups', value)} - /> + handleInputChange('groups', value)} + /> - handleInputChange('tag', value)} - /> - handleInputChange('remark', value)} - /> + handleInputChange('tag', value)} + /> + handleInputChange('remark', value)} + /> - - - - handleInputChange('priority', value) - } - style={{ width: '100%' }} - /> - - - - handleInputChange('weight', value) - } - style={{ width: '100%' }} - /> - - - - setAutoBan(value)} - extraText={t( - '仅当自动禁用开启时有效,关闭后不会自动禁用该渠道', - )} - initValue={autoBan} - /> - - - handleInputChange('param_override', value) - } - extraText={ -
- - handleInputChange( - 'param_override', - JSON.stringify({ temperature: 0 }, null, 2), - ) + + + + handleInputChange('priority', value) } - > - {t('旧格式模板')} - - - handleInputChange( - 'param_override', - JSON.stringify( - { - operations: [ + style={{ width: '100%' }} + /> + + + + handleInputChange('weight', value) + } + style={{ width: '100%' }} + /> + + + + setAutoBan(value)} + extraText={t( + '仅当自动禁用开启时有效,关闭后不会自动禁用该渠道', + )} + initValue={autoBan} + /> + + + handleInputChange('param_override', value) + } + extraText={ +
+ + handleInputChange( + 'param_override', + JSON.stringify({ temperature: 0 }, null, 2), + ) + } + > + {t('旧格式模板')} + + + handleInputChange( + 'param_override', + JSON.stringify( + { + operations: [ + { + path: 'temperature', + mode: 'set', + value: 0.7, + conditions: [ + { + path: 'model', + mode: 'prefix', + value: 'gpt', + }, + ], + logic: 'AND', + }, + ], + }, + null, + 2, + ), + ) + } + > + {t('新格式模板')} + +
+ } + showClear + /> + + + handleInputChange('header_override', value) + } + extraText={ +
+
+ + handleInputChange( + 'header_override', + JSON.stringify( { - path: 'temperature', - mode: 'set', - value: 0.7, - conditions: [ - { - path: 'model', - mode: 'prefix', - value: 'gpt', - }, - ], - logic: 'AND', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0', + Authorization: 'Bearer{api_key}', }, - ], - }, - null, - 2, - ), + null, + 2, + ), + ) + } + > + {t('填入模板')} + +
+
+ + {t('支持变量:')} + +
+
+ {t('渠道密钥')}: {'{api_key}'} +
+
+
+
+ } + showClear + /> + + + handleInputChange('status_code_mapping', value) + } + template={STATUS_CODE_MAPPING_EXAMPLE} + templateLabel={t('填入模板')} + editorType='keyValue' + formApi={formApiRef.current} + extraText={t( + '键为原状态码,值为要复写的状态码,仅影响本地判断', + )} + /> + + {/* 字段透传控制 - OpenAI 渠道 */} + {inputs.type === 1 && ( + <> +
+ {t('字段透传控制')} +
+ + + handleChannelOtherSettingsChange( + 'allow_service_tier', + value, ) } - > - {t('新格式模板')} -
-
- } - showClear - /> + extraText={t( + 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', + )} + /> - - handleInputChange('header_override', value) - } - extraText={ -
- - handleInputChange( - 'header_override', - JSON.stringify( - { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0', - }, - null, - 2, - ), + + handleChannelOtherSettingsChange( + 'disable_store', + value, ) } - > - {t('格式模板')} - -
- } - showClear - /> + extraText={t( + 'store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用', + )} + /> - - handleInputChange('status_code_mapping', value) - } - template={STATUS_CODE_MAPPING_EXAMPLE} - templateLabel={t('填入模板')} - editorType='keyValue' - formApi={formApiRef.current} - extraText={t( - '键为原状态码,值为要复写的状态码,仅影响本地判断', + + handleChannelOtherSettingsChange( + 'allow_safety_identifier', + value, + ) + } + extraText={t( + 'safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私', + )} + /> + )} - /> - + + {/* 字段透传控制 - Claude 渠道 */} + {inputs.type === 14 && ( + <> +
+ {t('字段透传控制')} +
+ + + handleChannelOtherSettingsChange( + 'allow_service_tier', + value, + ) + } + extraText={t( + 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', + )} + /> + + )} + +
{/* Channel Extra Settings Card */} - - {/* Header: Channel Extra Settings */} -
- - - -
- - {t('渠道额外设置')} - +
+ (formSectionRefs.current.channelExtraSettings = el) + } + > + + {/* Header: Channel Extra Settings */} +
+ + + +
+ + {t('渠道额外设置')} + +
-
- {inputs.type === 1 && ( + {inputs.type === 1 && ( + + handleChannelSettingsChange('force_format', value) + } + extraText={t( + '强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)', + )} + /> + )} + - handleChannelSettingsChange('force_format', value) + handleChannelSettingsChange( + 'thinking_to_content', + value, + ) } extraText={t( - '强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)', + '将 reasoning_content 转换为 标签拼接到内容中', )} /> - )} - - handleChannelSettingsChange('thinking_to_content', value) - } - extraText={t( - '将 reasoning_content 转换为 标签拼接到内容中', - )} - /> + + handleChannelSettingsChange( + 'pass_through_body_enabled', + value, + ) + } + extraText={t('启用请求体透传功能')} + /> - - handleChannelSettingsChange( - 'pass_through_body_enabled', - value, - ) - } - extraText={t('启用请求体透传功能')} - /> + + handleChannelSettingsChange('proxy', value) + } + showClear + extraText={t('用于配置网络代理,支持 socks5 协议')} + /> - - handleChannelSettingsChange('proxy', value) - } - showClear - extraText={t('用于配置网络代理,支持 socks5 协议')} - /> - - - handleChannelSettingsChange('system_prompt', value) - } - autosize - showClear - extraText={t( - '用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置', - )} - /> - - handleChannelSettingsChange( - 'system_prompt_override', - value, - ) - } - extraText={t( - '如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面', - )} - /> - + + handleChannelSettingsChange('system_prompt', value) + } + autosize + showClear + extraText={t( + '用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置', + )} + /> + + handleChannelSettingsChange( + 'system_prompt_override', + value, + ) + } + extraText={t( + '如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面', + )} + /> + +
)} @@ -2374,17 +2825,17 @@ const EditChannelModal = (props) => { onVisibleChange={(visible) => setIsModalOpenurl(visible)} /> - {/* 使用TwoFactorAuthModal组件进行2FA验证 */} - {/* 使用ChannelKeyDisplay组件显示密钥 */} @@ -2407,10 +2858,10 @@ const EditChannelModal = (props) => { {t('渠道密钥信息')}
} - visible={twoFAState.showModal && twoFAState.showKey} - onCancel={resetTwoFAState} + visible={keyDisplayState.showModal} + onCancel={resetKeyDisplayState} footer={ - } @@ -2418,7 +2869,7 @@ const EditChannelModal = (props) => { style={{ maxWidth: '90vw' }} > { case 36: localModels = ['suno_music', 'suno_lyrics']; break; + case 53: + localModels = [ + 'NousResearch/Hermes-4-405B-FP8', + 'Qwen/Qwen3-235B-A22B-Thinking-2507', + 'Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8', + 'Qwen/Qwen3-235B-A22B-Instruct-2507', + 'zai-org/GLM-4.5-FP8', + 'openai/gpt-oss-120b', + 'deepseek-ai/DeepSeek-R1-0528', + 'deepseek-ai/DeepSeek-R1', + 'deepseek-ai/DeepSeek-V3-0324', + 'deepseek-ai/DeepSeek-V3.1', + ]; + break; default: localModels = getChannelModels(value); break; diff --git a/web/src/components/table/channels/modals/ModelTestModal.jsx b/web/src/components/table/channels/modals/ModelTestModal.jsx index c643ed100..1879cd574 100644 --- a/web/src/components/table/channels/modals/ModelTestModal.jsx +++ b/web/src/components/table/channels/modals/ModelTestModal.jsx @@ -25,6 +25,7 @@ import { Table, Tag, Typography, + Select, } from '@douyinfe/semi-ui'; import { IconSearch } from '@douyinfe/semi-icons'; import { copy, showError, showInfo, showSuccess } from '../../../../helpers'; @@ -45,6 +46,8 @@ const ModelTestModal = ({ testChannel, modelTablePage, setModelTablePage, + selectedEndpointType, + setSelectedEndpointType, allSelectingRef, isMobile, t, @@ -59,6 +62,23 @@ const ModelTestModal = ({ ) : []; + const endpointTypeOptions = [ + { value: '', label: t('自动检测') }, + { value: 'openai', label: 'OpenAI (/v1/chat/completions)' }, + { value: 'openai-response', label: 'OpenAI Response (/v1/responses)' }, + { value: 'anthropic', label: 'Anthropic (/v1/messages)' }, + { + value: 'gemini', + label: 'Gemini (/v1beta/models/{model}:generateContent)', + }, + { value: 'jina-rerank', label: 'Jina Rerank (/rerank)' }, + { + value: 'image-generation', + label: t('图像生成') + ' (/v1/images/generations)', + }, + { value: 'embeddings', label: 'Embeddings (/v1/embeddings)' }, + ]; + const handleCopySelected = () => { if (selectedModelKeys.length === 0) { showError(t('请先选择模型!')); @@ -152,7 +172,13 @@ const ModelTestModal = ({ return ( + +
+ +
+ + {modalContent} + +
+
+ ); + } + + return ( +
+ {isLoading && ( +
+ +
+ )} +
+ ); + }; + return ( setIsModalOpen(false)} onCancel={() => setIsModalOpen(false)} closable={null} - bodyStyle={{ height: '400px', overflow: 'auto' }} + bodyStyle={{ + height: isVideo ? '450px' : '400px', + overflow: 'auto', + padding: isVideo && videoError ? '0' : '24px', + }} width={800} > {isVideo ? ( -
diff --git a/web/src/components/table/users/UsersColumnDefs.jsx b/web/src/components/table/users/UsersColumnDefs.jsx index e2b5a4051..9348d5603 100644 --- a/web/src/components/table/users/UsersColumnDefs.jsx +++ b/web/src/components/table/users/UsersColumnDefs.jsx @@ -26,7 +26,9 @@ import { Progress, Popover, Typography, + Dropdown, } from '@douyinfe/semi-ui'; +import { IconMore } from '@douyinfe/semi-icons'; import { renderGroup, renderNumber, renderQuota } from '../../../helpers'; /** @@ -204,6 +206,8 @@ const renderOperations = ( showDemoteModal, showEnableDisableModal, showDeleteModal, + showResetPasskeyModal, + showResetTwoFAModal, t, }, ) => { @@ -211,6 +215,28 @@ const renderOperations = ( return <>; } + const moreMenu = [ + { + node: 'item', + name: t('重置 Passkey'), + onClick: () => showResetPasskeyModal(record), + }, + { + node: 'item', + name: t('重置 2FA'), + onClick: () => showResetTwoFAModal(record), + }, + { + node: 'divider', + }, + { + node: 'item', + name: t('注销'), + type: 'danger', + onClick: () => showDeleteModal(record), + }, + ]; + return ( {record.status === 1 ? ( @@ -253,13 +279,9 @@ const renderOperations = ( > {t('降级')} - + +
@@ -270,7 +289,8 @@ const RechargeCard = ({ {payMethods && payMethods.length > 0 ? ( {payMethods.map((payMethod) => { - const minTopupVal = Number(payMethod.min_topup) || 0; + const minTopupVal = + Number(payMethod.min_topup) || 0; const isStripe = payMethod.type === 'stripe'; const disabled = (!enableOnlineTopUp && !isStripe) || @@ -284,7 +304,9 @@ const RechargeCard = ({ type='tertiary' onClick={() => preTopUp(payMethod.type)} disabled={disabled} - loading={paymentLoading && payWay === payMethod.type} + loading={ + paymentLoading && payWay === payMethod.type + } icon={ payMethod.type === 'alipay' ? ( @@ -295,7 +317,10 @@ const RechargeCard = ({ ) : ( ) } @@ -305,12 +330,22 @@ const RechargeCard = ({ ); - return disabled && minTopupVal > Number(topUpCount || 0) ? ( - + return disabled && + minTopupVal > Number(topUpCount || 0) ? ( + {buttonEl} ) : ( - {buttonEl} + + {buttonEl} + ); })} @@ -325,26 +360,81 @@ const RechargeCard = ({ )} {(enableOnlineTopUp || enableStripeTopUp) && ( - + + {t('选择充值额度')} + {(() => { + const { symbol, rate, type } = getCurrencyConfig(); + if (type === 'USD') return null; + + return ( + + (1 $ = {rate.toFixed(2)} {symbol}) + + ); + })()} +
+ } + >
{presetAmounts.map((preset, index) => { - const discount = preset.discount || topupInfo?.discount?.[preset.value] || 1.0; + const discount = + preset.discount || + topupInfo?.discount?.[preset.value] || + 1.0; const originalPrice = preset.value * priceRatio; const discountedPrice = originalPrice * discount; const hasDiscount = discount < 1.0; const actualPay = discountedPrice; const save = originalPrice - discountedPrice; - + + // 根据当前货币类型换算显示金额和数量 + const { symbol, rate, type } = getCurrencyConfig(); + const statusStr = localStorage.getItem('status'); + let usdRate = 7; // 默认CNY汇率 + try { + if (statusStr) { + const s = JSON.parse(statusStr); + usdRate = s?.usd_exchange_rate || 7; + } + } catch (e) {} + + let displayValue = preset.value; // 显示的数量 + let displayActualPay = actualPay; + let displaySave = save; + + if (type === 'USD') { + // 数量保持USD,价格从CNY转USD + displayActualPay = actualPay / usdRate; + displaySave = save / usdRate; + } else if (type === 'CNY') { + // 数量转CNY,价格已是CNY + displayValue = preset.value * usdRate; + } else if (type === 'CUSTOM') { + // 数量和价格都转自定义货币 + displayValue = preset.value * rate; + displayActualPay = (actualPay / usdRate) * rate; + displaySave = (save / usdRate) * rate; + } + return ( { @@ -356,24 +446,36 @@ const RechargeCard = ({ }} >
- + - {formatLargeNumber(preset.value)} + {formatLargeNumber(preset.value)} $ {hasDiscount && ( - - {t('折').includes('off') ? - ((1 - parseFloat(discount)) * 100).toFixed(1) : - (discount * 10).toFixed(1)}{t('折')} - + + {t('折').includes('off') + ? ( + (1 - parseFloat(discount)) * + 100 + ).toFixed(1) + : (discount * 10).toFixed(1)} + {t('折')} + )} -
- {t('实付')} {actualPay.toFixed(2)}, - {hasDiscount ? `${t('节省')} ${save.toFixed(2)}` : `${t('节省')} 0.00`} +
+ {t('实付')} {symbol} + {displayActualPay.toFixed(2)}, + {hasDiscount + ? `${t('节省')} ${symbol}${displaySave.toFixed(2)}` + : `${t('节省')} ${symbol}0.00`}
diff --git a/web/src/components/topup/index.jsx b/web/src/components/topup/index.jsx index 0fde859e3..4ff05a254 100644 --- a/web/src/components/topup/index.jsx +++ b/web/src/components/topup/index.jsx @@ -37,6 +37,7 @@ import RechargeCard from './RechargeCard'; import InvitationCard from './InvitationCard'; import TransferModal from './modals/TransferModal'; import PaymentConfirmModal from './modals/PaymentConfirmModal'; +import TopupHistoryModal from './modals/TopupHistoryModal'; const TopUp = () => { const { t } = useTranslation(); @@ -83,14 +84,17 @@ const TopUp = () => { const [openTransfer, setOpenTransfer] = useState(false); const [transferAmount, setTransferAmount] = useState(0); + // 账单Modal状态 + const [openHistory, setOpenHistory] = useState(false); + // 预设充值额度选项 const [presetAmounts, setPresetAmounts] = useState([]); const [selectedPreset, setSelectedPreset] = useState(null); - + // 充值配置信息 const [topupInfo, setTopupInfo] = useState({ amount_options: [], - discount: {} + discount: {}, }); const topUp = async () => { @@ -317,9 +321,9 @@ const TopUp = () => { if (success) { setTopupInfo({ amount_options: data.amount_options || [], - discount: data.discount || {} + discount: data.discount || {}, }); - + // 处理支付方式 let payMethods = data.pay_methods || []; try { @@ -335,10 +339,15 @@ const TopUp = () => { payMethods = payMethods.map((method) => { // 规范化最小充值数 const normalizedMinTopup = Number(method.min_topup); - method.min_topup = Number.isFinite(normalizedMinTopup) ? normalizedMinTopup : 0; + method.min_topup = Number.isFinite(normalizedMinTopup) + ? normalizedMinTopup + : 0; // Stripe 的最小充值从后端字段回填 - if (method.type === 'stripe' && (!method.min_topup || method.min_topup <= 0)) { + if ( + method.type === 'stripe' && + (!method.min_topup || method.min_topup <= 0) + ) { const stripeMin = Number(data.stripe_min_topup); if (Number.isFinite(stripeMin)) { method.min_topup = stripeMin; @@ -369,7 +378,11 @@ const TopUp = () => { const enableStripeTopUp = data.enable_stripe_topup || false; const enableOnlineTopUp = data.enable_online_topup || false; const enableCreemTopUp = data.enable_creem_topup || false; - const minTopUpValue = enableOnlineTopUp? data.min_topup : enableStripeTopUp? data.stripe_min_topup : 1; + const minTopUpValue = enableOnlineTopUp + ? data.min_topup + : enableStripeTopUp + ? data.stripe_min_topup + : 1; setEnableOnlineTopUp(enableOnlineTopUp); setEnableStripeTopUp(enableStripeTopUp); setEnableCreemTopUp(enableCreemTopUp); @@ -397,12 +410,12 @@ const TopUp = () => { console.log('解析支付方式失败:', e); setPayMethods([]); } - + // 如果有自定义充值数量选项,使用它们替换默认的预设选项 if (data.amount_options && data.amount_options.length > 0) { - const customPresets = data.amount_options.map(amount => ({ + const customPresets = data.amount_options.map((amount) => ({ value: amount, - discount: data.discount[amount] || 1.0 + discount: data.discount[amount] || 1.0, })); setPresetAmounts(customPresets); } @@ -546,6 +559,14 @@ const TopUp = () => { setOpenTransfer(false); }; + const handleOpenHistory = () => { + setOpenHistory(true); + }; + + const handleHistoryCancel = () => { + setOpenHistory(false); + }; + const handleCreemCancel = () => { setCreemOpen(false); setSelectedCreemProduct(null); @@ -555,7 +576,7 @@ const TopUp = () => { const selectPresetAmount = (preset) => { setTopUpCount(preset.value); setSelectedPreset(preset.value); - + // 计算实际支付金额,考虑折扣 const discount = preset.discount || topupInfo.discount[preset.value] || 1.0; const discountedAmount = preset.value * priceRatio * discount; @@ -607,6 +628,13 @@ const TopUp = () => { discountRate={topupInfo?.discount?.[topUpCount] || 1.0} /> + {/* 充值账单模态框 */} + + {/* Creem 充值确认模态框 */} { renderQuota={renderQuota} statusLoading={statusLoading} topupInfo={topupInfo} + onOpenHistory={handleOpenHistory} />
diff --git a/web/src/components/topup/modals/PaymentConfirmModal.jsx b/web/src/components/topup/modals/PaymentConfirmModal.jsx index 1bffbfed1..8bd5455c7 100644 --- a/web/src/components/topup/modals/PaymentConfirmModal.jsx +++ b/web/src/components/topup/modals/PaymentConfirmModal.jsx @@ -40,9 +40,10 @@ const PaymentConfirmModal = ({ amountNumber, discountRate, }) => { - const hasDiscount = discountRate && discountRate > 0 && discountRate < 1 && amountNumber > 0; - const originalAmount = hasDiscount ? (amountNumber / discountRate) : 0; - const discountAmount = hasDiscount ? (originalAmount - amountNumber) : 0; + const hasDiscount = + discountRate && discountRate > 0 && discountRate < 1 && amountNumber > 0; + const originalAmount = hasDiscount ? amountNumber / discountRate : 0; + const discountAmount = hasDiscount ? originalAmount - amountNumber : 0; return ( . + +For commercial licensing, please contact support@quantumnous.com +*/ +import React, { useState, useEffect, useMemo } from 'react'; +import { + Modal, + Table, + Badge, + Typography, + Toast, + Empty, + Button, + Input, +} from '@douyinfe/semi-ui'; +import { + IllustrationNoResult, + IllustrationNoResultDark, +} from '@douyinfe/semi-illustrations'; +import { Coins } from 'lucide-react'; +import { IconSearch } from '@douyinfe/semi-icons'; +import { API, timestamp2string } from '../../../helpers'; +import { isAdmin } from '../../../helpers/utils'; +import { useIsMobile } from '../../../hooks/common/useIsMobile'; + +const { Text } = Typography; + +// 状态映射配置 +const STATUS_CONFIG = { + success: { type: 'success', key: '成功' }, + pending: { type: 'warning', key: '待支付' }, + expired: { type: 'danger', key: '已过期' }, +}; + +// 支付方式映射 +const PAYMENT_METHOD_MAP = { + stripe: 'Stripe', + alipay: '支付宝', + wxpay: '微信', +}; + +const TopupHistoryModal = ({ visible, onCancel, t }) => { + const [loading, setLoading] = useState(false); + const [topups, setTopups] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [keyword, setKeyword] = useState(''); + + const isMobile = useIsMobile(); + + const loadTopups = async (currentPage, currentPageSize) => { + setLoading(true); + try { + const base = isAdmin() ? '/api/user/topup' : '/api/user/topup/self'; + const qs = + `p=${currentPage}&page_size=${currentPageSize}` + + (keyword ? `&keyword=${encodeURIComponent(keyword)}` : ''); + const endpoint = `${base}?${qs}`; + const res = await API.get(endpoint); + const { success, message, data } = res.data; + if (success) { + setTopups(data.items || []); + setTotal(data.total || 0); + } else { + Toast.error({ content: message || t('加载失败') }); + } + } catch (error) { + console.error('Load topups error:', error); + Toast.error({ content: t('加载账单失败') }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (visible) { + loadTopups(page, pageSize); + } + }, [visible, page, pageSize, keyword]); + + const handlePageChange = (currentPage) => { + setPage(currentPage); + }; + + const handlePageSizeChange = (currentPageSize) => { + setPageSize(currentPageSize); + setPage(1); + }; + + const handleKeywordChange = (value) => { + setKeyword(value); + setPage(1); + }; + + // 管理员补单 + const handleAdminComplete = async (tradeNo) => { + try { + const res = await API.post('/api/user/topup/complete', { + trade_no: tradeNo, + }); + const { success, message } = res.data; + if (success) { + Toast.success({ content: t('补单成功') }); + await loadTopups(page, pageSize); + } else { + Toast.error({ content: message || t('补单失败') }); + } + } catch (e) { + Toast.error({ content: t('补单失败') }); + } + }; + + const confirmAdminComplete = (tradeNo) => { + Modal.confirm({ + title: t('确认补单'), + content: t('是否将该订单标记为成功并为用户入账?'), + onOk: () => handleAdminComplete(tradeNo), + }); + }; + + // 渲染状态徽章 + const renderStatusBadge = (status) => { + const config = STATUS_CONFIG[status] || { type: 'primary', key: status }; + return ( + + + {t(config.key)} + + ); + }; + + // 渲染支付方式 + const renderPaymentMethod = (pm) => { + const displayName = PAYMENT_METHOD_MAP[pm]; + return {displayName ? t(displayName) : pm || '-'}; + }; + + // 检查是否为管理员 + const userIsAdmin = useMemo(() => isAdmin(), []); + + const columns = useMemo(() => { + const baseColumns = [ + { + title: t('订单号'), + dataIndex: 'trade_no', + key: 'trade_no', + render: (text) => {text}, + }, + { + title: t('支付方式'), + dataIndex: 'payment_method', + key: 'payment_method', + render: renderPaymentMethod, + }, + { + title: t('充值额度'), + dataIndex: 'amount', + key: 'amount', + render: (amount) => ( + + + {amount} + + ), + }, + { + title: t('支付金额'), + dataIndex: 'money', + key: 'money', + render: (money) => ¥{money.toFixed(2)}, + }, + { + title: t('状态'), + dataIndex: 'status', + key: 'status', + render: renderStatusBadge, + }, + ]; + + // 管理员才显示操作列 + if (userIsAdmin) { + baseColumns.push({ + title: t('操作'), + key: 'action', + render: (_, record) => { + if (record.status !== 'pending') return null; + return ( + + ); + }, + }); + } + + baseColumns.push({ + title: t('创建时间'), + dataIndex: 'create_time', + key: 'create_time', + render: (time) => timestamp2string(time), + }); + + return baseColumns; + }, [t, userIsAdmin]); + + return ( + +
+ } + placeholder={t('订单号')} + value={keyword} + onChange={handleKeywordChange} + showClear + /> +
+ } + darkModeImage={ + + } + description={t('暂无充值记录')} + style={{ padding: 30 }} + /> + } + /> + + ); +}; + +export default TopupHistoryModal; diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index de08f2bcf..e79e19f6a 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -88,6 +88,11 @@ export const CHANNEL_OPTIONS = [ color: 'purple', label: '智谱 GLM-4V', }, + { + value: 27, + color: 'blue', + label: 'Perplexity', + }, { value: 24, color: 'orange', @@ -159,6 +164,21 @@ export const CHANNEL_OPTIONS = [ color: 'purple', label: 'Vidu', }, + { + value: 53, + color: 'blue', + label: 'SubModel', + }, + { + value: 54, + color: 'blue', + label: '豆包视频', + }, + { + value: 55, + color: 'green', + label: 'Sora', + }, ]; export const MODEL_TABLE_PAGE_SIZE = 10; diff --git a/web/src/constants/console.constants.js b/web/src/constants/console.constants.js index 23ee1e17f..bef19e4b9 100644 --- a/web/src/constants/console.constants.js +++ b/web/src/constants/console.constants.js @@ -24,26 +24,26 @@ export const DATE_RANGE_PRESETS = [ { text: '今天', start: () => dayjs().startOf('day').toDate(), - end: () => dayjs().endOf('day').toDate() + end: () => dayjs().endOf('day').toDate(), }, { text: '近 7 天', start: () => dayjs().subtract(6, 'day').startOf('day').toDate(), - end: () => dayjs().endOf('day').toDate() + end: () => dayjs().endOf('day').toDate(), }, { text: '本周', start: () => dayjs().startOf('week').toDate(), - end: () => dayjs().endOf('week').toDate() + end: () => dayjs().endOf('week').toDate(), }, { text: '近 30 天', start: () => dayjs().subtract(29, 'day').startOf('day').toDate(), - end: () => dayjs().endOf('day').toDate() + end: () => dayjs().endOf('day').toDate(), }, { text: '本月', start: () => dayjs().startOf('month').toDate(), - end: () => dayjs().endOf('month').toDate() + end: () => dayjs().endOf('month').toDate(), }, ]; diff --git a/web/src/helpers/api.js b/web/src/helpers/api.js index bc389b2e8..1ccfffaf2 100644 --- a/web/src/helpers/api.js +++ b/web/src/helpers/api.js @@ -131,13 +131,11 @@ export const buildApiPayload = ( seed: 'seed', }; - Object.entries(parameterMappings).forEach(([key, param]) => { const enabled = parameterEnabled[key]; const value = inputs[param]; const hasValue = value !== undefined && value !== null; - if (enabled && hasValue) { payload[param] = value; } diff --git a/web/src/helpers/data.js b/web/src/helpers/data.js index b894a953c..e45aac3e9 100644 --- a/web/src/helpers/data.js +++ b/web/src/helpers/data.js @@ -23,7 +23,9 @@ export function setStatusData(data) { localStorage.setItem('logo', data.logo); localStorage.setItem('footer_html', data.footer_html); localStorage.setItem('quota_per_unit', data.quota_per_unit); + // 兼容:保留旧字段,同时写入新的额度展示类型 localStorage.setItem('display_in_currency', data.display_in_currency); + localStorage.setItem('quota_display_type', data.quota_display_type || 'USD'); localStorage.setItem('enable_drawing', data.enable_drawing); localStorage.setItem('enable_task', data.enable_task); localStorage.setItem('enable_data_export', data.enable_data_export); diff --git a/web/src/helpers/index.js b/web/src/helpers/index.js index ecdeb20f1..26ebc6495 100644 --- a/web/src/helpers/index.js +++ b/web/src/helpers/index.js @@ -27,3 +27,4 @@ export * from './data'; export * from './token'; export * from './boolean'; export * from './dashboard'; +export * from './passkey'; diff --git a/web/src/helpers/passkey.js b/web/src/helpers/passkey.js new file mode 100644 index 000000000..c3f3e927d --- /dev/null +++ b/web/src/helpers/passkey.js @@ -0,0 +1,177 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +export function base64UrlToBuffer(base64url) { + if (!base64url) return new ArrayBuffer(0); + let padding = '='.repeat((4 - (base64url.length % 4)) % 4); + const base64 = (base64url + padding).replace(/-/g, '+').replace(/_/g, '/'); + const rawData = window.atob(base64); + const buffer = new ArrayBuffer(rawData.length); + const uintArray = new Uint8Array(buffer); + for (let i = 0; i < rawData.length; i += 1) { + uintArray[i] = rawData.charCodeAt(i); + } + return buffer; +} + +export function bufferToBase64Url(buffer) { + if (!buffer) return ''; + const uintArray = new Uint8Array(buffer); + let binary = ''; + for (let i = 0; i < uintArray.byteLength; i += 1) { + binary += String.fromCharCode(uintArray[i]); + } + return window + .btoa(binary) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, ''); +} + +export function prepareCredentialCreationOptions(payload) { + const options = + payload?.publicKey || + payload?.PublicKey || + payload?.response || + payload?.Response; + if (!options) { + throw new Error('无法从服务端响应中解析 Passkey 注册参数'); + } + const publicKey = { + ...options, + challenge: base64UrlToBuffer(options.challenge), + user: { + ...options.user, + id: base64UrlToBuffer(options.user?.id), + }, + }; + + if (Array.isArray(options.excludeCredentials)) { + publicKey.excludeCredentials = options.excludeCredentials.map((item) => ({ + ...item, + id: base64UrlToBuffer(item.id), + })); + } + + if ( + Array.isArray(options.attestationFormats) && + options.attestationFormats.length === 0 + ) { + delete publicKey.attestationFormats; + } + + return publicKey; +} + +export function prepareCredentialRequestOptions(payload) { + const options = + payload?.publicKey || + payload?.PublicKey || + payload?.response || + payload?.Response; + if (!options) { + throw new Error('无法从服务端响应中解析 Passkey 登录参数'); + } + const publicKey = { + ...options, + challenge: base64UrlToBuffer(options.challenge), + }; + + if (Array.isArray(options.allowCredentials)) { + publicKey.allowCredentials = options.allowCredentials.map((item) => ({ + ...item, + id: base64UrlToBuffer(item.id), + })); + } + + return publicKey; +} + +export function buildRegistrationResult(credential) { + if (!credential) return null; + + const { response } = credential; + const transports = + typeof response.getTransports === 'function' + ? response.getTransports() + : undefined; + + return { + id: credential.id, + rawId: bufferToBase64Url(credential.rawId), + type: credential.type, + authenticatorAttachment: credential.authenticatorAttachment, + response: { + attestationObject: bufferToBase64Url(response.attestationObject), + clientDataJSON: bufferToBase64Url(response.clientDataJSON), + transports, + }, + clientExtensionResults: credential.getClientExtensionResults?.() ?? {}, + }; +} + +export function buildAssertionResult(assertion) { + if (!assertion) return null; + + const { response } = assertion; + + return { + id: assertion.id, + rawId: bufferToBase64Url(assertion.rawId), + type: assertion.type, + authenticatorAttachment: assertion.authenticatorAttachment, + response: { + authenticatorData: bufferToBase64Url(response.authenticatorData), + clientDataJSON: bufferToBase64Url(response.clientDataJSON), + signature: bufferToBase64Url(response.signature), + userHandle: response.userHandle + ? bufferToBase64Url(response.userHandle) + : null, + }, + clientExtensionResults: assertion.getClientExtensionResults?.() ?? {}, + }; +} + +export async function isPasskeySupported() { + if (typeof window === 'undefined' || !window.PublicKeyCredential) { + return false; + } + if ( + typeof window.PublicKeyCredential.isConditionalMediationAvailable === + 'function' + ) { + try { + const available = + await window.PublicKeyCredential.isConditionalMediationAvailable(); + if (available) return true; + } catch (error) { + // ignore + } + } + if ( + typeof window.PublicKeyCredential + .isUserVerifyingPlatformAuthenticatorAvailable === 'function' + ) { + try { + return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); + } catch (error) { + return false; + } + } + return true; +} diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx index c19e2849d..1c3dda854 100644 --- a/web/src/helpers/render.jsx +++ b/web/src/helpers/render.jsx @@ -54,6 +54,7 @@ import { FastGPT, Kling, Jimeng, + Perplexity, } from '@lobehub/icons'; import { @@ -309,6 +310,8 @@ export function getChannelIcon(channelType) { return ; case 25: // Moonshot return ; + case 27: // Perplexity + return ; case 20: // OpenRouter return ; case 19: // 360 智脑 @@ -337,6 +340,8 @@ export function getChannelIcon(channelType) { return ; case 51: // 即梦 Jimeng return ; + case 54: // 豆包视频 Doubao Video + return ; case 8: // 自定义渠道 case 22: // 知识库:FastGPT return ; @@ -830,12 +835,25 @@ export function renderQuotaNumberWithDigit(num, digits = 2) { if (typeof num !== 'number' || isNaN(num)) { return 0; } - let displayInCurrency = localStorage.getItem('display_in_currency'); + const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; num = num.toFixed(digits); - if (displayInCurrency) { + if (quotaDisplayType === 'CNY') { + return '¥' + num; + } else if (quotaDisplayType === 'USD') { return '$' + num; + } else if (quotaDisplayType === 'CUSTOM') { + const statusStr = localStorage.getItem('status'); + let symbol = '¤'; + try { + if (statusStr) { + const s = JSON.parse(statusStr); + symbol = s?.custom_currency_symbol || symbol; + } + } catch (e) {} + return symbol + num; + } else { + return num; } - return num; } export function renderNumberWithPoint(num) { @@ -887,33 +905,111 @@ export function getQuotaWithUnit(quota, digits = 6) { } export function renderQuotaWithAmount(amount) { - let displayInCurrency = localStorage.getItem('display_in_currency'); - displayInCurrency = displayInCurrency === 'true'; - if (displayInCurrency) { - return '$' + amount; - } else { + const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; + if (quotaDisplayType === 'TOKENS') { return renderNumber(renderUnitWithQuota(amount)); } + if (quotaDisplayType === 'CNY') { + return '¥' + amount; + } else if (quotaDisplayType === 'CUSTOM') { + const statusStr = localStorage.getItem('status'); + let symbol = '¤'; + try { + if (statusStr) { + const s = JSON.parse(statusStr); + symbol = s?.custom_currency_symbol || symbol; + } + } catch (e) {} + return symbol + amount; + } + return '$' + amount; +} + +/** + * 获取当前货币配置信息 + * @returns {Object} - { symbol, rate, type } + */ +export function getCurrencyConfig() { + const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; + const statusStr = localStorage.getItem('status'); + + let symbol = '$'; + let rate = 1; + + if (quotaDisplayType === 'CNY') { + symbol = '¥'; + try { + if (statusStr) { + const s = JSON.parse(statusStr); + rate = s?.usd_exchange_rate || 7; + } + } catch (e) {} + } else if (quotaDisplayType === 'CUSTOM') { + try { + if (statusStr) { + const s = JSON.parse(statusStr); + symbol = s?.custom_currency_symbol || '¤'; + rate = s?.custom_currency_exchange_rate || 1; + } + } catch (e) {} + } + + return { symbol, rate, type: quotaDisplayType }; +} + +/** + * 将美元金额转换为当前选择的货币 + * @param {number} usdAmount - 美元金额 + * @param {number} digits - 小数位数 + * @returns {string} - 格式化后的货币字符串 + */ +export function convertUSDToCurrency(usdAmount, digits = 2) { + const { symbol, rate } = getCurrencyConfig(); + const convertedAmount = usdAmount * rate; + return symbol + convertedAmount.toFixed(digits); } export function renderQuota(quota, digits = 2) { let quotaPerUnit = localStorage.getItem('quota_per_unit'); - let displayInCurrency = localStorage.getItem('display_in_currency'); + const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; quotaPerUnit = parseFloat(quotaPerUnit); - displayInCurrency = displayInCurrency === 'true'; - if (displayInCurrency) { - const result = quota / quotaPerUnit; - const fixedResult = result.toFixed(digits); - - // 如果 toFixed 后结果为 0 但原始值不为 0,显示最小值 - if (parseFloat(fixedResult) === 0 && quota > 0 && result > 0) { - const minValue = Math.pow(10, -digits); - return '$' + minValue.toFixed(digits); - } - - return '$' + fixedResult; + if (quotaDisplayType === 'TOKENS') { + return renderNumber(quota); } - return renderNumber(quota); + const resultUSD = quota / quotaPerUnit; + let symbol = '$'; + let value = resultUSD; + if (quotaDisplayType === 'CNY') { + const statusStr = localStorage.getItem('status'); + let usdRate = 1; + try { + if (statusStr) { + const s = JSON.parse(statusStr); + usdRate = s?.usd_exchange_rate || 1; + } + } catch (e) {} + value = resultUSD * usdRate; + symbol = '¥'; + } else if (quotaDisplayType === 'CUSTOM') { + const statusStr = localStorage.getItem('status'); + let symbolCustom = '¤'; + let rate = 1; + try { + if (statusStr) { + const s = JSON.parse(statusStr); + symbolCustom = s?.custom_currency_symbol || symbolCustom; + rate = s?.custom_currency_exchange_rate || rate; + } + } catch (e) {} + value = resultUSD * rate; + symbol = symbolCustom; + } + const fixedResult = value.toFixed(digits); + if (parseFloat(fixedResult) === 0 && quota > 0 && value > 0) { + const minValue = Math.pow(10, -digits); + return symbol + minValue.toFixed(digits); + } + return symbol + fixedResult; } function isValidGroupRatio(ratio) { @@ -1036,13 +1132,19 @@ export function renderModelPrice( ); groupRatio = effectiveGroupRatio; + // 获取货币配置 + const { symbol, rate } = getCurrencyConfig(); + if (modelPrice !== -1) { + const displayPrice = (modelPrice * rate).toFixed(6); + const displayTotal = (modelPrice * groupRatio * rate).toFixed(6); return i18next.t( - '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}', + '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}', { - price: modelPrice, + symbol: symbol, + price: displayPrice, ratio: groupRatio, - total: modelPrice * groupRatio, + total: displayTotal, ratioType: ratioLabel, }, ); @@ -1072,25 +1174,30 @@ export function renderModelPrice( (completionTokens / 1000000) * completionRatioPrice * groupRatio + (webSearchCallCount / 1000) * webSearchPrice * groupRatio + (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio + - (imageGenerationCallPrice * groupRatio); + imageGenerationCallPrice * groupRatio; return ( <>

- {i18next.t('输入价格:${{price}} / 1M tokens{{audioPrice}}', { - price: inputRatioPrice, - audioPrice: audioInputSeperatePrice - ? `,音频 $${audioInputPrice} / 1M tokens` - : '', - })} + {i18next.t( + '输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}', + { + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + audioPrice: audioInputSeperatePrice + ? `,音频 ${symbol}${(audioInputPrice * rate).toFixed(6)} / 1M tokens` + : '', + }, + )}

{i18next.t( - '输出价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', + '输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})', { - price: inputRatioPrice, - total: completionRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + total: (completionRatioPrice * rate).toFixed(6), completionRatio: completionRatio, }, )} @@ -1098,10 +1205,11 @@ export function renderModelPrice( {cacheTokens > 0 && (

{i18next.t( - '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', + '缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', { - price: inputRatioPrice, - total: inputRatioPrice * cacheRatio, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + total: (inputRatioPrice * cacheRatio * rate).toFixed(6), cacheRatio: cacheRatio, }, )} @@ -1110,11 +1218,12 @@ export function renderModelPrice( {image && imageOutputTokens > 0 && (

{i18next.t( - '图片输入价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (图片倍率: {{imageRatio}})', + '图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})', { - price: imageRatioPrice, + symbol: symbol, + price: (imageRatioPrice * rate).toFixed(6), ratio: groupRatio, - total: imageRatioPrice * groupRatio, + total: (imageRatioPrice * groupRatio * rate).toFixed(6), imageRatio: imageRatio, }, )} @@ -1122,22 +1231,25 @@ export function renderModelPrice( )} {webSearch && webSearchCallCount > 0 && (

- {i18next.t('Web搜索价格:${{price}} / 1K 次', { - price: webSearchPrice, + {i18next.t('Web搜索价格:{{symbol}}{{price}} / 1K 次', { + symbol: symbol, + price: (webSearchPrice * rate).toFixed(6), })}

)} {fileSearch && fileSearchCallCount > 0 && (

- {i18next.t('文件搜索价格:${{price}} / 1K 次', { - price: fileSearchPrice, + {i18next.t('文件搜索价格:{{symbol}}{{price}} / 1K 次', { + symbol: symbol, + price: (fileSearchPrice * rate).toFixed(6), })}

)} {imageGenerationCall && imageGenerationCallPrice > 0 && (

- {i18next.t('图片生成调用:${{price}} / 1次', { - price: imageGenerationCallPrice, + {i18next.t('图片生成调用:{{symbol}}{{price}} / 1次', { + symbol: symbol, + price: (imageGenerationCallPrice * rate).toFixed(6), })}

)} @@ -1147,50 +1259,55 @@ export function renderModelPrice( let inputDesc = ''; if (image && imageOutputTokens > 0) { inputDesc = i18next.t( - '(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}}', + '(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * {{symbol}}{{price}}', { nonImageInput: inputTokens - imageOutputTokens, imageInput: imageOutputTokens, imageRatio: imageRatio, - price: inputRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), }, ); } else if (cacheTokens > 0) { inputDesc = i18next.t( - '(输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}}', + '(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}', { nonCacheInput: inputTokens - cacheTokens, cacheInput: cacheTokens, - price: inputRatioPrice, - cachePrice: cacheRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + cachePrice: (cacheRatioPrice * rate).toFixed(6), }, ); } else if (audioInputSeperatePrice && audioInputTokens > 0) { inputDesc = i18next.t( - '(输入 {{nonAudioInput}} tokens / 1M tokens * ${{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * ${{audioPrice}}', + '(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}', { nonAudioInput: inputTokens - audioInputTokens, audioInput: audioInputTokens, - price: inputRatioPrice, - audioPrice: audioInputPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + audioPrice: (audioInputPrice * rate).toFixed(6), }, ); } else { inputDesc = i18next.t( - '(输入 {{input}} tokens / 1M tokens * ${{price}}', + '(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}', { input: inputTokens, - price: inputRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), }, ); } // 构建输出部分描述 const outputDesc = i18next.t( - '输出 {{completion}} tokens / 1M tokens * ${{compPrice}}) * {{ratioType}} {{ratio}}', + '输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}', { completion: completionTokens, - compPrice: completionRatioPrice, + symbol: symbol, + compPrice: (completionRatioPrice * rate).toFixed(6), ratio: groupRatio, ratioType: ratioLabel, }, @@ -1200,10 +1317,11 @@ export function renderModelPrice( const extraServices = [ webSearch && webSearchCallCount > 0 ? i18next.t( - ' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}', + ' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', { count: webSearchCallCount, - price: webSearchPrice, + symbol: symbol, + price: (webSearchPrice * rate).toFixed(6), ratio: groupRatio, ratioType: ratioLabel, }, @@ -1211,10 +1329,11 @@ export function renderModelPrice( : '', fileSearch && fileSearchCallCount > 0 ? i18next.t( - ' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}', + ' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', { count: fileSearchCallCount, - price: fileSearchPrice, + symbol: symbol, + price: (fileSearchPrice * rate).toFixed(6), ratio: groupRatio, ratioType: ratioLabel, }, @@ -1222,9 +1341,10 @@ export function renderModelPrice( : '', imageGenerationCall && imageGenerationCallPrice > 0 ? i18next.t( - ' + 图片生成调用 ${{price}} / 1次 * {{ratioType}} {{ratio}}', + ' + 图片生成调用 {{symbol}}{{price}} / 1次 * {{ratioType}} {{ratio}}', { - price: imageGenerationCallPrice, + symbol: symbol, + price: (imageGenerationCallPrice * rate).toFixed(6), ratio: groupRatio, ratioType: ratioLabel, }, @@ -1233,12 +1353,13 @@ export function renderModelPrice( ].join(''); return i18next.t( - '{{inputDesc}} + {{outputDesc}}{{extraServices}} = ${{total}}', + '{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}', { inputDesc, outputDesc, extraServices, - total: price.toFixed(6), + symbol: symbol, + total: (price * rate).toFixed(6), }, ); })()} @@ -1270,9 +1391,13 @@ export function renderLogContent( useUserGroupRatio: useUserGroupRatio, } = getEffectiveRatio(groupRatio, user_group_ratio); + // 获取货币配置 + const { symbol, rate } = getCurrencyConfig(); + if (modelPrice !== -1) { - return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', { - price: modelPrice, + return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', { + symbol: symbol, + price: (modelPrice * rate).toFixed(6), ratioType: ratioLabel, ratio, }); @@ -1365,14 +1490,19 @@ export function renderAudioModelPrice( user_group_ratio, ); groupRatio = effectiveGroupRatio; + + // 获取货币配置 + const { symbol, rate } = getCurrencyConfig(); + // 1 ratio = $0.002 / 1K tokens if (modelPrice !== -1) { return i18next.t( - '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}', + '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}', { - price: modelPrice, + symbol: symbol, + price: (modelPrice * rate).toFixed(6), ratio: groupRatio, - total: modelPrice * groupRatio, + total: (modelPrice * groupRatio * rate).toFixed(6), ratioType: ratioLabel, }, ); @@ -1407,16 +1537,18 @@ export function renderAudioModelPrice( <>

- {i18next.t('提示价格:${{price}} / 1M tokens', { - price: inputRatioPrice, + {i18next.t('提示价格:{{symbol}}{{price}} / 1M tokens', { + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), })}

{i18next.t( - '补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', + '补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})', { - price: inputRatioPrice, - total: completionRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + total: (completionRatioPrice * rate).toFixed(6), completionRatio: completionRatio, }, )} @@ -1424,10 +1556,11 @@ export function renderAudioModelPrice( {cacheTokens > 0 && (

{i18next.t( - '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', + '缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', { - price: inputRatioPrice, - total: inputRatioPrice * cacheRatio, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + total: (inputRatioPrice * cacheRatio * rate).toFixed(6), cacheRatio: cacheRatio, }, )} @@ -1435,20 +1568,27 @@ export function renderAudioModelPrice( )}

{i18next.t( - '音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})', + '音频提示价格:{{symbol}}{{price}} * {{audioRatio}} = {{symbol}}{{total}} / 1M tokens (音频倍率: {{audioRatio}})', { - price: inputRatioPrice, - total: inputRatioPrice * audioRatio, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + total: (inputRatioPrice * audioRatio * rate).toFixed(6), audioRatio: audioRatio, }, )}

{i18next.t( - '音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})', + '音频补全价格:{{symbol}}{{price}} * {{audioRatio}} * {{audioCompRatio}} = {{symbol}}{{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})', { - price: inputRatioPrice, - total: inputRatioPrice * audioRatio * audioCompletionRatio, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + total: ( + inputRatioPrice * + audioRatio * + audioCompletionRatio * + rate + ).toFixed(6), audioRatio: audioRatio, audioCompRatio: audioCompletionRatio, }, @@ -1457,48 +1597,60 @@ export function renderAudioModelPrice(

{cacheTokens > 0 ? i18next.t( - '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', + '文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', { nonCacheInput: inputTokens - cacheTokens, cacheInput: cacheTokens, - cachePrice: inputRatioPrice * cacheRatio, - price: inputRatioPrice, + symbol: symbol, + cachePrice: (inputRatioPrice * cacheRatio * rate).toFixed( + 6, + ), + price: (inputRatioPrice * rate).toFixed(6), completion: completionTokens, - compPrice: completionRatioPrice, - total: textPrice.toFixed(6), + compPrice: (completionRatioPrice * rate).toFixed(6), + total: (textPrice * rate).toFixed(6), }, ) : i18next.t( - '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', + '文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', { input: inputTokens, - price: inputRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), completion: completionTokens, - compPrice: completionRatioPrice, - total: textPrice.toFixed(6), + compPrice: (completionRatioPrice * rate).toFixed(6), + total: (textPrice * rate).toFixed(6), }, )}

{i18next.t( - '音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}', + '音频提示 {{input}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} = {{symbol}}{{total}}', { input: audioInputTokens, completion: audioCompletionTokens, - audioInputPrice: audioRatio * inputRatioPrice, - audioCompPrice: - audioRatio * audioCompletionRatio * inputRatioPrice, - total: audioPrice.toFixed(6), + symbol: symbol, + audioInputPrice: (audioRatio * inputRatioPrice * rate).toFixed( + 6, + ), + audioCompPrice: ( + audioRatio * + audioCompletionRatio * + inputRatioPrice * + rate + ).toFixed(6), + total: (audioPrice * rate).toFixed(6), }, )}

{i18next.t( - '总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}', + '总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}', { - total: price.toFixed(6), - textPrice: textPrice.toFixed(6), - audioPrice: audioPrice.toFixed(6), + symbol: symbol, + total: (price * rate).toFixed(6), + textPrice: (textPrice * rate).toFixed(6), + audioPrice: (audioPrice * rate).toFixed(6), }, )}

@@ -1510,9 +1662,8 @@ export function renderAudioModelPrice( } export function renderQuotaWithPrompt(quota, digits) { - let displayInCurrency = localStorage.getItem('display_in_currency'); - displayInCurrency = displayInCurrency === 'true'; - if (displayInCurrency) { + const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; + if (quotaDisplayType !== 'TOKENS') { return i18next.t('等价金额:') + renderQuota(quota, digits); } return ''; @@ -1537,14 +1688,18 @@ export function renderClaudeModelPrice( ); groupRatio = effectiveGroupRatio; + // 获取货币配置 + const { symbol, rate } = getCurrencyConfig(); + if (modelPrice !== -1) { return i18next.t( - '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}', + '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}', { - price: modelPrice, + symbol: symbol, + price: (modelPrice * rate).toFixed(6), ratioType: ratioLabel, ratio: groupRatio, - total: modelPrice * groupRatio, + total: (modelPrice * groupRatio * rate).toFixed(6), }, ); } else { @@ -1573,28 +1728,31 @@ export function renderClaudeModelPrice( <>

- {i18next.t('提示价格:${{price}} / 1M tokens', { - price: inputRatioPrice, + {i18next.t('提示价格:{{symbol}}{{price}} / 1M tokens', { + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), })}

{i18next.t( - '补全价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens', + '补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens', { - price: inputRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), ratio: completionRatio, - total: completionRatioPrice, + total: (completionRatioPrice * rate).toFixed(6), }, )}

{cacheTokens > 0 && (

{i18next.t( - '缓存价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', + '缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', { - price: inputRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), ratio: cacheRatio, - total: cacheRatioPrice, + total: (cacheRatioPrice * rate).toFixed(2), cacheRatio: cacheRatio, }, )} @@ -1603,11 +1761,12 @@ export function renderClaudeModelPrice( {cacheCreationTokens > 0 && (

{i18next.t( - '缓存创建价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})', + '缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})', { - price: inputRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), ratio: cacheCreationRatio, - total: cacheCreationRatioPrice, + total: (cacheCreationRatioPrice * rate).toFixed(6), cacheCreationRatio: cacheCreationRatio, }, )} @@ -1617,33 +1776,37 @@ export function renderClaudeModelPrice(

{cacheTokens > 0 || cacheCreationTokens > 0 ? i18next.t( - '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}', + '提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', { nonCacheInput: nonCachedTokens, cacheInput: cacheTokens, cacheRatio: cacheRatio, cacheCreationInput: cacheCreationTokens, cacheCreationRatio: cacheCreationRatio, - cachePrice: cacheRatioPrice, - cacheCreationPrice: cacheCreationRatioPrice, - price: inputRatioPrice, + symbol: symbol, + cachePrice: (cacheRatioPrice * rate).toFixed(2), + cacheCreationPrice: ( + cacheCreationRatioPrice * rate + ).toFixed(6), + price: (inputRatioPrice * rate).toFixed(6), completion: completionTokens, - compPrice: completionRatioPrice, + compPrice: (completionRatioPrice * rate).toFixed(6), ratio: groupRatio, ratioType: ratioLabel, - total: price.toFixed(6), + total: (price * rate).toFixed(6), }, ) : i18next.t( - '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}', + '提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', { input: inputTokens, - price: inputRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), completion: completionTokens, - compPrice: completionRatioPrice, + compPrice: (completionRatioPrice * rate).toFixed(6), ratio: groupRatio, ratioType: ratioLabel, - total: price.toFixed(6), + total: (price * rate).toFixed(6), }, )}

@@ -1669,9 +1832,13 @@ export function renderClaudeLogContent( ); groupRatio = effectiveGroupRatio; + // 获取货币配置 + const { symbol, rate } = getCurrencyConfig(); + if (modelPrice !== -1) { - return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', { - price: modelPrice, + return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', { + symbol: symbol, + price: (modelPrice * rate).toFixed(6), ratioType: ratioLabel, ratio: groupRatio, }); diff --git a/web/src/helpers/secureApiCall.js b/web/src/helpers/secureApiCall.js new file mode 100644 index 000000000..0054e04ae --- /dev/null +++ b/web/src/helpers/secureApiCall.js @@ -0,0 +1,62 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +/** + * 安全 API 调用包装器 + * 自动处理需要验证的 403 错误,透明地触发验证流程 + */ + +/** + * 检查错误是否是需要安全验证的错误 + * @param {Error} error - 错误对象 + * @returns {boolean} + */ +export function isVerificationRequiredError(error) { + if (!error.response) return false; + + const { status, data } = error.response; + + // 检查是否是 403 错误且包含验证相关的错误码 + if (status === 403 && data) { + const verificationCodes = [ + 'VERIFICATION_REQUIRED', + 'VERIFICATION_EXPIRED', + 'VERIFICATION_INVALID', + ]; + + return verificationCodes.includes(data.code); + } + + return false; +} + +/** + * 从错误中提取验证需求信息 + * @param {Error} error - 错误对象 + * @returns {Object} 验证需求信息 + */ +export function extractVerificationInfo(error) { + const data = error.response?.data || {}; + + return { + code: data.code, + message: data.message || '需要安全验证', + required: true, + }; +} diff --git a/web/src/helpers/utils.jsx b/web/src/helpers/utils.jsx index bcd13230e..0cbdebd15 100644 --- a/web/src/helpers/utils.jsx +++ b/web/src/helpers/utils.jsx @@ -646,9 +646,25 @@ export const calculateModelPrice = ({ const numCompletion = parseFloat(rawDisplayCompletion.replace(/[^0-9.]/g, '')) / unitDivisor; + let symbol = '$'; + if (currency === 'CNY') { + symbol = '¥'; + } else if (currency === 'CUSTOM') { + try { + const statusStr = localStorage.getItem('status'); + if (statusStr) { + const s = JSON.parse(statusStr); + symbol = s?.custom_currency_symbol || '¤'; + } else { + symbol = '¤'; + } + } catch (e) { + symbol = '¤'; + } + } return { - inputPrice: `${currency === 'CNY' ? '¥' : '$'}${numInput.toFixed(precision)}`, - completionPrice: `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(precision)}`, + inputPrice: `${symbol}${numInput.toFixed(precision)}`, + completionPrice: `${symbol}${numCompletion.toFixed(precision)}`, unitLabel, isPerToken: true, usedGroup, diff --git a/web/src/hooks/channels/useChannelsData.jsx b/web/src/hooks/channels/useChannelsData.jsx index 65460a06b..f3f99f01e 100644 --- a/web/src/hooks/channels/useChannelsData.jsx +++ b/web/src/hooks/channels/useChannelsData.jsx @@ -83,9 +83,11 @@ export const useChannelsData = () => { const [testingModels, setTestingModels] = useState(new Set()); const [selectedModelKeys, setSelectedModelKeys] = useState([]); const [isBatchTesting, setIsBatchTesting] = useState(false); - const [testQueue, setTestQueue] = useState([]); - const [isProcessingQueue, setIsProcessingQueue] = useState(false); const [modelTablePage, setModelTablePage] = useState(1); + const [selectedEndpointType, setSelectedEndpointType] = useState(''); + + // 使用 ref 来避免闭包问题,类似旧版实现 + const shouldStopBatchTestingRef = useRef(false); // Multi-key management states const [showMultiKeyManageModal, setShowMultiKeyManageModal] = useState(false); @@ -751,125 +753,237 @@ export const useChannelsData = () => { } }; - // Test channel - const testChannel = async (record, model) => { - setTestQueue((prev) => [...prev, { channel: record, model }]); - if (!isProcessingQueue) { - setIsProcessingQueue(true); + // Test channel - 单个模型测试,参考旧版实现 + const testChannel = async (record, model, endpointType = '') => { + const testKey = `${record.id}-${model}`; + + // 检查是否应该停止批量测试 + if (shouldStopBatchTestingRef.current && isBatchTesting) { + return Promise.resolve(); } - }; - // Process test queue - const processTestQueue = async () => { - if (!isProcessingQueue || testQueue.length === 0) return; - - const { channel, model, indexInFiltered } = testQueue[0]; - - if (currentTestChannel && currentTestChannel.id === channel.id) { - let pageNo; - if (indexInFiltered !== undefined) { - pageNo = Math.floor(indexInFiltered / MODEL_TABLE_PAGE_SIZE) + 1; - } else { - const filteredModelsList = currentTestChannel.models - .split(',') - .filter((m) => - m.toLowerCase().includes(modelSearchKeyword.toLowerCase()), - ); - const modelIdx = filteredModelsList.indexOf(model); - pageNo = - modelIdx !== -1 - ? Math.floor(modelIdx / MODEL_TABLE_PAGE_SIZE) + 1 - : 1; - } - setModelTablePage(pageNo); - } + // 添加到正在测试的模型集合 + setTestingModels((prev) => new Set([...prev, model])); try { - setTestingModels((prev) => new Set([...prev, model])); - const res = await API.get( - `/api/channel/test/${channel.id}?model=${model}`, - ); + let url = `/api/channel/test/${record.id}?model=${model}`; + if (endpointType) { + url += `&endpoint_type=${endpointType}`; + } + const res = await API.get(url); + + // 检查是否在请求期间被停止 + if (shouldStopBatchTestingRef.current && isBatchTesting) { + return Promise.resolve(); + } + const { success, message, time } = res.data; + // 更新测试结果 setModelTestResults((prev) => ({ ...prev, - [`${channel.id}-${model}`]: { success, time }, + [testKey]: { + success, + message, + time: time || 0, + timestamp: Date.now(), + }, })); if (success) { - updateChannelProperty(channel.id, (ch) => { - ch.response_time = time * 1000; - ch.test_time = Date.now() / 1000; + // 更新渠道响应时间 + updateChannelProperty(record.id, (channel) => { + channel.response_time = time * 1000; + channel.test_time = Date.now() / 1000; }); - if (!model) { + + if (!model || model === '') { showInfo( t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。') - .replace('${name}', channel.name) + .replace('${name}', record.name) + .replace('${time.toFixed(2)}', time.toFixed(2)), + ); + } else { + showInfo( + t( + '通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。', + ) + .replace('${name}', record.name) + .replace('${model}', model) .replace('${time.toFixed(2)}', time.toFixed(2)), ); } } else { - showError(message); + showError(`${t('模型')} ${model}: ${message}`); } } catch (error) { - showError(error.message); + // 处理网络错误 + const testKey = `${record.id}-${model}`; + setModelTestResults((prev) => ({ + ...prev, + [testKey]: { + success: false, + message: error.message || t('网络错误'), + time: 0, + timestamp: Date.now(), + }, + })); + showError(`${t('模型')} ${model}: ${error.message || t('测试失败')}`); } finally { + // 从正在测试的模型集合中移除 setTestingModels((prev) => { const newSet = new Set(prev); newSet.delete(model); return newSet; }); } - - setTestQueue((prev) => prev.slice(1)); }; - // Monitor queue changes - useEffect(() => { - if (testQueue.length > 0 && isProcessingQueue) { - processTestQueue(); - } else if (testQueue.length === 0 && isProcessingQueue) { - setIsProcessingQueue(false); - setIsBatchTesting(false); - } - }, [testQueue, isProcessingQueue]); - - // Batch test models + // 批量测试单个渠道的所有模型,参考旧版实现 const batchTestModels = async () => { - if (!currentTestChannel) return; + if (!currentTestChannel || !currentTestChannel.models) { + showError(t('渠道模型信息不完整')); + return; + } - setIsBatchTesting(true); - setModelTablePage(1); - - const filteredModels = currentTestChannel.models + const models = currentTestChannel.models .split(',') .filter((model) => model.toLowerCase().includes(modelSearchKeyword.toLowerCase()), ); - setTestQueue( - filteredModels.map((model, idx) => ({ - channel: currentTestChannel, - model, - indexInFiltered: idx, - })), - ); - setIsProcessingQueue(true); + if (models.length === 0) { + showError(t('没有找到匹配的模型')); + return; + } + + setIsBatchTesting(true); + shouldStopBatchTestingRef.current = false; // 重置停止标志 + + // 清空该渠道之前的测试结果 + setModelTestResults((prev) => { + const newResults = { ...prev }; + models.forEach((model) => { + const testKey = `${currentTestChannel.id}-${model}`; + delete newResults[testKey]; + }); + return newResults; + }); + + try { + showInfo( + t('开始批量测试 ${count} 个模型,已清空上次结果...').replace( + '${count}', + models.length, + ), + ); + + // 提高并发数量以加快测试速度,参考旧版的并发限制 + const concurrencyLimit = 5; + const results = []; + + for (let i = 0; i < models.length; i += concurrencyLimit) { + // 检查是否应该停止 + if (shouldStopBatchTestingRef.current) { + showInfo(t('批量测试已停止')); + break; + } + + const batch = models.slice(i, i + concurrencyLimit); + showInfo( + t('正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)') + .replace('${current}', i + 1) + .replace('${end}', Math.min(i + concurrencyLimit, models.length)) + .replace('${total}', models.length), + ); + + const batchPromises = batch.map((model) => + testChannel(currentTestChannel, model, selectedEndpointType), + ); + const batchResults = await Promise.allSettled(batchPromises); + results.push(...batchResults); + + // 再次检查是否应该停止 + if (shouldStopBatchTestingRef.current) { + showInfo(t('批量测试已停止')); + break; + } + + // 短暂延迟避免过于频繁的请求 + if (i + concurrencyLimit < models.length) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } + + if (!shouldStopBatchTestingRef.current) { + // 等待一小段时间确保所有结果都已更新 + await new Promise((resolve) => setTimeout(resolve, 300)); + + // 使用当前状态重新计算结果统计 + setModelTestResults((currentResults) => { + let successCount = 0; + let failCount = 0; + + models.forEach((model) => { + const testKey = `${currentTestChannel.id}-${model}`; + const result = currentResults[testKey]; + if (result && result.success) { + successCount++; + } else { + failCount++; + } + }); + + // 显示完成消息 + setTimeout(() => { + showSuccess( + t('批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}') + .replace('${success}', successCount) + .replace('${fail}', failCount) + .replace('${total}', models.length), + ); + }, 100); + + return currentResults; // 不修改状态,只是为了获取最新值 + }); + } + } catch (error) { + showError(t('批量测试过程中发生错误: ') + error.message); + } finally { + setIsBatchTesting(false); + } + }; + + // 停止批量测试 + const stopBatchTesting = () => { + shouldStopBatchTestingRef.current = true; + setIsBatchTesting(false); + setTestingModels(new Set()); + showInfo(t('已停止批量测试')); + }; + + // 清空测试结果 + const clearTestResults = () => { + setModelTestResults({}); + showInfo(t('已清空测试结果')); }; // Handle close modal const handleCloseModal = () => { + // 如果正在批量测试,先停止测试 if (isBatchTesting) { - setTestQueue([]); - setIsProcessingQueue(false); - setIsBatchTesting(false); - showSuccess(t('已停止测试')); - } else { - setShowModelTestModal(false); - setModelSearchKeyword(''); - setSelectedModelKeys([]); - setModelTablePage(1); + shouldStopBatchTestingRef.current = true; + showInfo(t('关闭弹窗,已停止批量测试')); } + + setShowModelTestModal(false); + setModelSearchKeyword(''); + setIsBatchTesting(false); + setTestingModels(new Set()); + setSelectedModelKeys([]); + setModelTablePage(1); + setSelectedEndpointType(''); + // 可选择性保留测试结果,这里不清空以便用户查看 }; // Type counts @@ -956,6 +1070,8 @@ export const useChannelsData = () => { isBatchTesting, modelTablePage, setModelTablePage, + selectedEndpointType, + setSelectedEndpointType, allSelectingRef, // Multi-key management states diff --git a/web/src/hooks/common/useHeaderBar.js b/web/src/hooks/common/useHeaderBar.js index 3458a1d16..f3ec86962 100644 --- a/web/src/hooks/common/useHeaderBar.js +++ b/web/src/hooks/common/useHeaderBar.js @@ -40,7 +40,7 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { const location = useLocation(); const loading = statusState?.status === undefined; - const isLoading = useMinimumLoadingTime(loading); + const isLoading = useMinimumLoadingTime(loading, 200); const systemName = getSystemName(); const logo = getLogo(); diff --git a/web/src/hooks/common/useSecureVerification.jsx b/web/src/hooks/common/useSecureVerification.jsx new file mode 100644 index 000000000..9109ec7d7 --- /dev/null +++ b/web/src/hooks/common/useSecureVerification.jsx @@ -0,0 +1,274 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import { useState, useEffect, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { SecureVerificationService } from '../../services/secureVerification'; +import { showError, showSuccess } from '../../helpers'; +import { isVerificationRequiredError } from '../../helpers/secureApiCall'; + +/** + * 通用安全验证 Hook + * @param {Object} options - 配置选项 + * @param {Function} options.onSuccess - 验证成功回调 + * @param {Function} options.onError - 验证失败回调 + * @param {string} options.successMessage - 成功提示消息 + * @param {boolean} options.autoReset - 验证完成后是否自动重置状态,默认为 true + */ +export const useSecureVerification = ({ + onSuccess, + onError, + successMessage, + autoReset = true, +} = {}) => { + const { t } = useTranslation(); + + // 验证方式可用性状态 + const [verificationMethods, setVerificationMethods] = useState({ + has2FA: false, + hasPasskey: false, + passkeySupported: false, + }); + + // 模态框状态 + const [isModalVisible, setIsModalVisible] = useState(false); + + // 当前验证状态 + const [verificationState, setVerificationState] = useState({ + method: null, // '2fa' | 'passkey' + loading: false, + code: '', + apiCall: null, + }); + + // 检查可用的验证方式 + const checkVerificationMethods = useCallback(async () => { + const methods = + await SecureVerificationService.checkAvailableVerificationMethods(); + setVerificationMethods(methods); + return methods; + }, []); + + // 初始化时检查验证方式 + useEffect(() => { + checkVerificationMethods(); + }, [checkVerificationMethods]); + + // 重置状态 + const resetState = useCallback(() => { + setVerificationState({ + method: null, + loading: false, + code: '', + apiCall: null, + }); + setIsModalVisible(false); + }, []); + + // 开始验证流程 + const startVerification = useCallback( + async (apiCall, options = {}) => { + const { preferredMethod, title, description } = options; + + // 检查验证方式 + const methods = await checkVerificationMethods(); + + if (!methods.has2FA && !methods.hasPasskey) { + const errorMessage = t('您需要先启用两步验证或 Passkey 才能执行此操作'); + showError(errorMessage); + onError?.(new Error(errorMessage)); + return false; + } + + // 设置默认验证方式 + let defaultMethod = preferredMethod; + if (!defaultMethod) { + if (methods.hasPasskey && methods.passkeySupported) { + defaultMethod = 'passkey'; + } else if (methods.has2FA) { + defaultMethod = '2fa'; + } + } + + setVerificationState((prev) => ({ + ...prev, + method: defaultMethod, + apiCall, + title, + description, + })); + setIsModalVisible(true); + + return true; + }, + [checkVerificationMethods, onError, t], + ); + + // 执行验证 + const executeVerification = useCallback( + async (method, code = '') => { + if (!verificationState.apiCall) { + showError(t('验证配置错误')); + return; + } + + setVerificationState((prev) => ({ ...prev, loading: true })); + + try { + // 先调用验证 API,成功后后端会设置 session + await SecureVerificationService.verify(method, code); + + // 验证成功,调用业务 API(此时中间件会通过) + const result = await verificationState.apiCall(); + + // 显示成功消息 + if (successMessage) { + showSuccess(successMessage); + } + + // 调用成功回调 + onSuccess?.(result, method); + + // 自动重置状态 + if (autoReset) { + resetState(); + } + + return result; + } catch (error) { + showError(error.message || t('验证失败,请重试')); + onError?.(error); + throw error; + } finally { + setVerificationState((prev) => ({ ...prev, loading: false })); + } + }, + [ + verificationState.apiCall, + successMessage, + onSuccess, + onError, + autoReset, + resetState, + t, + ], + ); + + // 设置验证码 + const setVerificationCode = useCallback((code) => { + setVerificationState((prev) => ({ ...prev, code })); + }, []); + + // 切换验证方式 + const switchVerificationMethod = useCallback((method) => { + setVerificationState((prev) => ({ ...prev, method, code: '' })); + }, []); + + // 取消验证 + const cancelVerification = useCallback(() => { + resetState(); + }, [resetState]); + + // 检查是否可以使用某种验证方式 + const canUseMethod = useCallback( + (method) => { + switch (method) { + case '2fa': + return verificationMethods.has2FA; + case 'passkey': + return ( + verificationMethods.hasPasskey && + verificationMethods.passkeySupported + ); + default: + return false; + } + }, + [verificationMethods], + ); + + // 获取推荐的验证方式 + const getRecommendedMethod = useCallback(() => { + if ( + verificationMethods.hasPasskey && + verificationMethods.passkeySupported + ) { + return 'passkey'; + } + if (verificationMethods.has2FA) { + return '2fa'; + } + return null; + }, [verificationMethods]); + + /** + * 包装 API 调用,自动处理验证错误 + * 当 API 返回需要验证的错误时,自动弹出验证模态框 + * @param {Function} apiCall - API 调用函数 + * @param {Object} options - 验证选项(同 startVerification) + * @returns {Promise} + */ + const withVerification = useCallback( + async (apiCall, options = {}) => { + try { + // 直接尝试调用 API + return await apiCall(); + } catch (error) { + // 检查是否是需要验证的错误 + if (isVerificationRequiredError(error)) { + // 自动触发验证流程 + await startVerification(apiCall, options); + // 不抛出错误,让验证模态框处理 + return null; + } + // 其他错误继续抛出 + throw error; + } + }, + [startVerification], + ); + + return { + // 状态 + isModalVisible, + verificationMethods, + verificationState, + + // 方法 + startVerification, + executeVerification, + cancelVerification, + resetState, + setVerificationCode, + switchVerificationMethod, + checkVerificationMethods, + + // 辅助方法 + canUseMethod, + getRecommendedMethod, + withVerification, // 新增:自动处理验证的包装函数 + + // 便捷属性 + hasAnyVerificationMethod: + verificationMethods.has2FA || verificationMethods.hasPasskey, + isLoading: verificationState.loading, + currentMethod: verificationState.method, + code: verificationState.code, + }; +}; diff --git a/web/src/hooks/common/useSidebar.js b/web/src/hooks/common/useSidebar.js index 13d76fd86..76d74ac34 100644 --- a/web/src/hooks/common/useSidebar.js +++ b/web/src/hooks/common/useSidebar.js @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { useState, useEffect, useMemo, useContext } from 'react'; +import { useState, useEffect, useMemo, useContext, useRef } from 'react'; import { StatusContext } from '../../context/Status'; import { API } from '../../helpers'; @@ -29,6 +29,13 @@ export const useSidebar = () => { const [statusState] = useContext(StatusContext); const [userConfig, setUserConfig] = useState(null); const [loading, setLoading] = useState(true); + const instanceIdRef = useRef(null); + const hasLoadedOnceRef = useRef(false); + + if (!instanceIdRef.current) { + const randomPart = Math.random().toString(16).slice(2); + instanceIdRef.current = `sidebar-${Date.now()}-${randomPart}`; + } // 默认配置 const defaultAdminConfig = { @@ -74,9 +81,17 @@ export const useSidebar = () => { }, [statusState?.status?.SidebarModulesAdmin]); // 加载用户配置的通用方法 - const loadUserConfig = async () => { + const loadUserConfig = async ({ withLoading } = {}) => { + const shouldShowLoader = + typeof withLoading === 'boolean' + ? withLoading + : !hasLoadedOnceRef.current; + try { - setLoading(true); + if (shouldShowLoader) { + setLoading(true); + } + const res = await API.get('/api/user/self'); if (res.data.success && res.data.data.sidebar_modules) { let config; @@ -122,18 +137,25 @@ export const useSidebar = () => { }); setUserConfig(defaultUserConfig); } finally { - setLoading(false); + if (shouldShowLoader) { + setLoading(false); + } + hasLoadedOnceRef.current = true; } }; // 刷新用户配置的方法(供外部调用) const refreshUserConfig = async () => { - if (Object.keys(adminConfig).length > 0) { - await loadUserConfig(); + if (Object.keys(adminConfig).length > 0) { + await loadUserConfig({ withLoading: false }); } // 触发全局刷新事件,通知所有useSidebar实例更新 - sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT)); + sidebarEventTarget.dispatchEvent( + new CustomEvent(SIDEBAR_REFRESH_EVENT, { + detail: { sourceId: instanceIdRef.current, skipLoader: true }, + }), + ); }; // 加载用户配置 @@ -146,16 +168,25 @@ export const useSidebar = () => { // 监听全局刷新事件 useEffect(() => { - const handleRefresh = () => { + const handleRefresh = (event) => { + if (event?.detail?.sourceId === instanceIdRef.current) { + return; + } + if (Object.keys(adminConfig).length > 0) { - loadUserConfig(); + loadUserConfig({ + withLoading: event?.detail?.skipLoader ? false : undefined, + }); } }; sidebarEventTarget.addEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh); return () => { - sidebarEventTarget.removeEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh); + sidebarEventTarget.removeEventListener( + SIDEBAR_REFRESH_EVENT, + handleRefresh, + ); }; }, [adminConfig]); diff --git a/web/src/hooks/model-pricing/useModelPricingData.jsx b/web/src/hooks/model-pricing/useModelPricingData.jsx index 799cdc136..0a407236d 100644 --- a/web/src/hooks/model-pricing/useModelPricingData.jsx +++ b/web/src/hooks/model-pricing/useModelPricingData.jsx @@ -64,6 +64,29 @@ export const useModelPricingData = () => { () => statusState?.status?.usd_exchange_rate ?? priceRate, [statusState, priceRate], ); + const customExchangeRate = useMemo( + () => statusState?.status?.custom_currency_exchange_rate ?? 1, + [statusState], + ); + const customCurrencySymbol = useMemo( + () => statusState?.status?.custom_currency_symbol ?? '¤', + [statusState], + ); + + // 默认货币与站点展示类型同步(USD/CNY),TOKENS 时仍允许切换视图内货币 + const siteDisplayType = useMemo( + () => statusState?.status?.quota_display_type || 'USD', + [statusState], + ); + useEffect(() => { + if ( + siteDisplayType === 'USD' || + siteDisplayType === 'CNY' || + siteDisplayType === 'CUSTOM' + ) { + setCurrency(siteDisplayType); + } + }, [siteDisplayType]); const filteredModels = useMemo(() => { let result = models; @@ -156,6 +179,8 @@ export const useModelPricingData = () => { if (currency === 'CNY') { return `¥${(priceInUSD * usdExchangeRate).toFixed(3)}`; + } else if (currency === 'CUSTOM') { + return `${customCurrencySymbol}${(priceInUSD * customExchangeRate).toFixed(3)}`; } return `$${priceInUSD.toFixed(3)}`; }; diff --git a/web/src/hooks/usage-logs/useUsageLogsData.jsx b/web/src/hooks/usage-logs/useUsageLogsData.jsx index d434e7333..6041d7427 100644 --- a/web/src/hooks/usage-logs/useUsageLogsData.jsx +++ b/web/src/hooks/usage-logs/useUsageLogsData.jsx @@ -377,6 +377,12 @@ export const useLogsData = () => { other.file_search_call_count || 0, ), }); + if (logs[i]?.content) { + expandDataLocal.push({ + key: t('其他详情'), + value: logs[i].content, + }); + } } if (logs[i].type === 2) { let modelMapped = @@ -462,6 +468,12 @@ export const useLogsData = () => { }); } } + if (other?.request_path) { + expandDataLocal.push({ + key: t('请求路径'), + value: other.request_path, + }); + } expandDatesLocal[logs[i].key] = expandDataLocal; } diff --git a/web/src/hooks/users/useUsersData.jsx b/web/src/hooks/users/useUsersData.jsx index ef57e7333..f906be543 100644 --- a/web/src/hooks/users/useUsersData.jsx +++ b/web/src/hooks/users/useUsersData.jsx @@ -154,6 +154,40 @@ export const useUsersData = () => { setLoading(false); }; + const resetUserPasskey = async (user) => { + if (!user) { + return; + } + try { + const res = await API.delete(`/api/user/${user.id}/reset_passkey`); + const { success, message } = res.data; + if (success) { + showSuccess(t('Passkey 已重置')); + } else { + showError(message || t('操作失败,请重试')); + } + } catch (error) { + showError(t('操作失败,请重试')); + } + }; + + const resetUserTwoFA = async (user) => { + if (!user) { + return; + } + try { + const res = await API.delete(`/api/user/${user.id}/2fa`); + const { success, message } = res.data; + if (success) { + showSuccess(t('二步验证已重置')); + } else { + showError(message || t('操作失败,请重试')); + } + } catch (error) { + showError(t('操作失败,请重试')); + } + }; + // Handle page change const handlePageChange = (page) => { setActivePage(page); @@ -271,6 +305,8 @@ export const useUsersData = () => { loadUsers, searchUsers, manageUser, + resetUserPasskey, + resetUserTwoFA, handlePageChange, handlePageSizeChange, handleRow, diff --git a/web/src/i18n/i18n.js b/web/src/i18n/i18n.js index 7198ee336..8715951bf 100644 --- a/web/src/i18n/i18n.js +++ b/web/src/i18n/i18n.js @@ -22,7 +22,9 @@ import { initReactI18next } from 'react-i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; import enTranslation from './locales/en.json'; +import frTranslation from './locales/fr.json'; import zhTranslation from './locales/zh.json'; +import ruTranslation from './locales/ru.json'; i18n .use(LanguageDetector) @@ -30,12 +32,10 @@ i18n .init({ load: 'languageOnly', resources: { - en: { - translation: enTranslation, - }, - zh: { - translation: zhTranslation, - }, + en: enTranslation, + zh: zhTranslation, + fr: frTranslation, + ru: ruTranslation, }, fallbackLng: 'zh', interpolation: { diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 69718b669..1b4195fcc 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1,2164 +1,2105 @@ { - "主页": "Home", - "控制台": "Console", - "$%.6f 额度": "$%.6f quota", - "或": "or", - "登 录": "Log In", - "注 册": "Sign Up", - "使用 邮箱或用户名 登录": "Sign in with Email or Username", - "使用 GitHub 继续": "Continue with GitHub", - "使用 OIDC 继续": "Continue with OIDC", - "使用 微信 继续": "Continue with WeChat", - "使用 LinuxDO 继续": "Continue with LinuxDO", - "使用 用户名 注册": "Sign up with Username", - "其他登录选项": "Other login options", - "其他注册选项": "Other registration options", - "请输入您的用户名或邮箱地址": "Please enter your username or email address", - "请输入您的邮箱地址": "Please enter your email address", - "请输入您的密码": "Please enter your password", - "继续": "Continue", - "%d 点额度": "%d point quota", - "尚未实现": "Not yet implemented", - "余额不足": "Insufficient quota", - "危险操作": "Dangerous operation", - "输入你的账户名": "Enter your account name", - "确认删除": "Confirm deletion", - "确认绑定": "Confirm binding", - "您正在删除自己的帐户,将清空所有数据且不可恢复": "You are deleting your account. All data will be cleared and cannot be recovered.", - "通道「%s」(#%d)已被禁用": "Channel %s (#%d) has been disabled", - "通道「%s」(#%d)已被禁用,原因:%s": "Channel %s (#%d) has been disabled, reason: %s", - "测试已在运行中": "Test is already running", - "响应时间 %.2fs 超过阈值 %.2fs": "Response time %.2fs exceeds threshold %.2fs", - "通道测试完成": "Channel test completed", - "通道测试完成,如果没有收到禁用通知,说明所有通道都正常": "Channel test completed. If no disable notification received, all channels are functioning normally", - "无法连接至 GitHub 服务器,请稍后重试!": "Unable to connect to GitHub server. Please try again later!", - "返回值非法,用户字段为空,请稍后重试!": "Invalid return value, user field is empty. Please try again later!", - "管理员未开启通过 GitHub 登录以及注册": "Administrator has not enabled GitHub login and registration", - "管理员关闭了新用户注册": "Administrator has disabled new user registration", - "用户已被封禁": "User has been banned", - "该 GitHub 账户已被绑定": "This GitHub account is already bound", - "邮箱地址已被占用": "Email address is already in use", - "%s邮箱验证邮件": "%s Email verification", - "

您好,你正在进行%s邮箱验证。

": "

Hello, you are verifying your %s email.

", - "

您的验证码为: %s

": "

Your verification code is: %s

", - "

验证码 %d 分钟内有效,如果不是本人操作,请忽略。

": "

Verification code is valid for %d minutes. If you did not request this, please ignore.

", - "无效的参数": "Invalid parameter", - "该邮箱地址未注册": "This email address is not registered", - "%s密码重置": "%s Password reset", - "

您好,你正在进行%s密码重置。

": "

Hello, you are resetting your %s password.

", - "

点击此处进行密码重置。

": "

Click here to reset your password.

", - "

重置链接 %d 分钟内有效,如果不是本人操作,请忽略。

": "

Reset link is valid for %d minutes. If you did not request this, please ignore.

", - "重置链接非法或已过期": "Reset link is invalid or expired", - "无法启用 GitHub OAuth,请先填入 GitHub Client ID 以及 GitHub Client Secret!": "Unable to enable GitHub OAuth. Please enter GitHub Client ID and GitHub Client Secret first!", - "无法启用微信登录,请先填入微信登录相关配置信息!": "Unable to enable WeChat login. Please enter WeChat login configuration first!", - "无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!": "Unable to enable Turnstile verification. Please enter Turnstile verification configuration first!", - "兑换码名称长度必须在1-20之间": "Redemption code name must be between 1-20 characters", - "兑换码个数必须大于0": "Number of redemption codes must be greater than 0", - "一次兑换码批量生成的个数不能大于 100": "Cannot generate more than 100 redemption codes at once", - "当前分组上游负载已饱和,请稍后再试": "Current group upstream load is saturated. Please try again later", - "令牌名称过长": "Token name is too long", - "令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "Token has expired and cannot be enabled. Please modify token expiration time or set to never expire", - "令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "Token quota is depleted and cannot be enabled. Please modify remaining quota or set to unlimited", - "管理员关闭了密码登录": "Administrator has disabled password login", - "无法保存会话信息,请重试": "Unable to save session information. Please try again", - "管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册": "Administrator has disabled password registration. Please register using third-party account verification", - "输入不合法 ": "Invalid input ", - "管理员开启了邮箱验证,请输入邮箱地址和验证码": "Administrator has enabled email verification. Please enter email address and verification code", - "验证码错误或已过期": "Verification code is incorrect or expired", - "无权获取同级或更高等级用户的信息": "No permission to access information of users at same or higher level", - "请重试,系统生成的 UUID 竟然重复了!": "Please try again, system generated UUID is duplicated!", - "输入不合法": "Invalid input", - "无权更新同权限等级或更高权限等级的用户信息": "No permission to update user information at same or higher permission level", - "管理员将用户额度从 %s修改为 %s": "Administrator modified user quota from %s to %s", - "无权删除同权限等级或更高权限等级的用户": "No permission to delete users at same or higher permission level", - "无法创建权限大于等于自己的用户": "Cannot create users with permissions greater than or equal to your own", - "用户不存在": "User does not exist", - "无法禁用超级管理员用户": "Cannot disable super administrator user", - "无法删除超级管理员用户": "Cannot delete super administrator user", - "普通管理员用户无法提升其他用户为管理员": "Regular administrator cannot promote other users to administrator", - "该用户已经是管理员": "This user is already an administrator", - "无法降级超级管理员用户": "Cannot downgrade super administrator user", - "该用户已经是普通用户": "This user is already a regular user", - "管理员未开启通过微信登录以及注册": "Administrator has not enabled WeChat login and registration", - "该微信账号已被绑定": "This WeChat account is already bound", - "无权进行此操作,未登录且未提供 access token": "No permission for this operation: not logged in and no access token provided", - "无权进行此操作,access token 无效": "No permission for this operation: invalid access token", - "无权进行此操作,权限不足": "No permission for this operation: insufficient permissions", - "普通用户不支持指定渠道": "Regular users cannot specify channels", - "无效的渠道 ID": "Invalid channel ID", - "该渠道已被禁用": "This channel has been disabled", - "无效的请求": "Invalid request", - "无可用渠道": "No available channels", - "Turnstile token 为空": "Turnstile token is empty", - "Turnstile 校验失败,请刷新重试!": "Turnstile verification failed. Please refresh and try again!", - "id 为空!": "ID is empty!", - "未提供兑换码": "No redemption code provided", - "无效的 user id": "Invalid user ID", - "无效的兑换码": "Invalid redemption code", - "该兑换码已被使用": "This redemption code has been used", - "通过兑换码充值 %s": "Recharge %s via redemption code", - "未提供令牌": "No token provided", - "该令牌状态不可用": "This token status is unavailable", - "该令牌已过期": "This token has expired", - "该令牌额度已用尽": "This token's quota is depleted", - "无效的令牌": "Invalid token", - "id 或 userId 为空!": "ID or userID is empty!", - "quota 不能为负数!": "Quota cannot be negative!", - "令牌额度不足": "Insufficient token quota", - "用户额度不足": "Insufficient user quota", - "您的额度即将用尽": "Your quota is almost depleted", - "您的额度已用尽": "Your quota is depleted", - "%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。
充值链接:%s": "%s, current remaining quota is %d. To avoid service interruption, please recharge promptly.
Recharge link: %s", - "affCode 为空!": "Affiliate code is empty!", - "新用户注册赠送 %s": "New user registration bonus: %s", - "使用邀请码赠送 %s": "Invitation code bonus: %s", - "邀请用户赠送 %s": "Referral bonus: %s", - "用户名或密码为空": "Username or password is empty", - "用户名或密码错误,或用户已被封禁": "Username or password is incorrect, or user has been banned", - "email 为空!": "Email is empty!", - "GitHub id 为空!": "GitHub ID is empty!", - "WeChat id 为空!": "WeChat ID is empty!", - "username 为空!": "Username is empty!", - "邮箱地址或密码为空!": "Email address or password is empty!", - "OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用": "OpenAI API aggregation management system supporting multiple channels including Azure. Can be used for key management and redistribution. Single executable file, pre-packaged Docker image, one-click deployment, ready to use", - "未知类型": "Unknown type", - "不支持": "Not supported", - "操作成功完成!": "Operation completed successfully!", - "已启用": "Enabled", - "已禁用": "Disabled", - "未知状态": "Unknown status", - " 秒": "s", - " 分钟 ": "m", - " 小时 ": "h", - " 天 ": "d", - " 个月 ": "M", - " 年 ": "y", - "未测试": "Not tested", - "通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test successful, took ${time.toFixed(2)} seconds.", - "已成功开始测试所有已启用通道,请刷新页面查看结果。": "Successfully started testing all enabled channels. Please refresh page to view results.", - "通道 ${name} 余额更新成功!": "Channel ${name} quota updated successfully!", - "已更新完毕所有已启用通道余额!": "Updated quota for all enabled channels!", - "渠道ID,名称,密钥,API地址": "Channel ID, name, key, Base URL", - "名称": "Name", - "分组": "Group", - "类型": "Type", - "状态": "Status", - "响应时间": "Response time", - "余额": "Balance", - "操作": "Actions", - "未更新": "Not updated", - "测试": "Test", - "更新余额": "Update balance", - "删除": "Delete", - "删除渠道 {channel.name}": "Delete channel {channel.name}", - "禁用": "Disable", - "启用": "Enable", - "编辑": "Edit", - "添加新的渠道": "Add new channel", - "测试所有已启用通道": "Test all enabled channels", - "更新所有已启用通道余额": "Update balance for all enabled channels", - "刷新": "Refresh", - "绑定成功!": "Binding successful!", - "登录成功!": "Login successful!", - "操作失败,重定向至登录界面中...": "Operation failed, redirecting to login page...", - "出现错误,第 ${count} 次重试中...": "Error occurred, retry attempt ${count}...", - "首页": "Home", - "渠道": "Channel", - "令牌": "Tokens", - "兑换额度": "Redeem", - "充值": "Recharge", - "用户": "Users", - "日志": "Logs", - "设置": "Settings", - "关于": "About", - "价格": "Pricing", - "注销成功!": "Logout successful!", - "注销": "Logout", - "登录": "Sign in", - "注册": "Sign up", - "未登录或登录已过期,请重新登录!": "Not logged in or session expired. Please login again!", - "用户登录": "User Login", - "密码": "Password", - "忘记密码?": "Forgot password?", - "点击重置": "Click to reset", - "; 没有账户?": "; No account?", - "点击注册": "Click to register", - "微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "Scan WeChat QR code to follow official account, enter \"verification code\" to get code (valid for 3 minutes)", - "全部用户": "All users", - "当前用户": "Current user", - "全部'": "All'", - "充值'": "Recharge'", - "消费'": "Consume'", - "管理'": "Manage'", - "系统'": "System'", - " 充值 ": " Recharge ", - " 消费 ": " Consume ", - " 管理 ": " Manage ", - " 系统 ": " System ", - " 未知 ": " Unknown ", - "时间": "Time", - "详情": "Details", - "选择模式": "Select mode", - "选择明细分类": "Select detail category", - "模型倍率不是合法的 JSON 字符串": "Model ratio is not a valid JSON string", - "通用设置": "General Settings", - "充值链接": "Recharge Link", - "例如发卡网站的购买链接": "E.g., purchase link from card issuing website", - "文档地址": "Document Link", - "例如 https://docs.newapi.pro": "E.g., https://docs.newapi.pro", - "聊天页面链接": "Chat Page Link", - "例如 ChatGPT Next Web 的部署地址": "E.g., ChatGPT Next Web deployment address", - "单位美元额度": "Quota per USD", - "一单位货币能兑换的额度": "Quota exchangeable per unit currency", - "启用额度消费日志记录": "Enable quota consumption logging", - "以货币形式显示额度": "Display quota as currency", - "相关 API 显示令牌额度而非用户额度": "Related APIs show token quota instead of user quota", - "保存通用设置": "Save General Settings", - "监控设置": "Monitoring Settings", - "测试所有渠道的最长响应时间": "Maximum response time for testing all channels", - "单位秒": "Unit: seconds", - "当运行通道全部测试时": "When running all channel tests", - "超过此时间将自动禁用通道": "Channels exceeding this time will be automatically disabled", - "额度提醒阈值": "Quota reminder threshold", - "低于此额度时将发送邮件提醒用户": "Email reminder will be sent when quota falls below this", - "失败时自动禁用通道": "Automatically disable channel on failure", - "保存监控设置": "Save Monitoring Settings", - "额度设置": "Quota Settings", - "新用户初始额度": "Initial quota for new users", - "例如": "For example", - "请求预扣费额度": "Pre-deduction quota for requests", - "请求结束后多退少补": "Adjust after request completion", - "邀请新用户奖励额度": "Referral bonus quota", - "新用户使用邀请码奖励额度": "New user invitation code bonus quota", - "保存额度设置": "Save Quota Settings", - "倍率设置": "Ratio Settings", - "模型倍率": "Model ratio", - "为一个 JSON 文本": "Is a JSON text", - "键为模型名称": "Key is model name", - "值为倍率": "Value is ratio", - "分组倍率": "Group ratio", - "键为分组名称": "Key is group name", - "保存倍率设置": "Save Ratio Settings", - "已是最新版本": "Is the latest version", - "检查更新": "Check for updates", - "公告": "Announcement", - "在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code", - "保存公告": "Save Announcement", - "个性化设置": "Personalization Settings", - "系统名称": "System Name", - "在此输入系统名称": "Enter the system name here", - "设置系统名称": "Set system name", - "图片地址": "Image URL", - "在此输入 Logo 图片地址": "Enter the Logo image URL here", - "首页内容": "Home Page Content", - "在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "Enter the home page content here, supports Markdown", - "保存首页内容": "Save Home Page Content", - "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面": "Enter new about content here, support Markdown", - "保存关于": "Save About", - "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "Removal of One API copyright mark must first be authorized. Project maintenance requires a lot of effort. If this project is meaningful to you, please actively support it.", - "页脚": "Footer", - "在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码": "Enter the new footer here, leave blank to use the default footer, supports HTML code.", - "设置页脚": "Set Footer", - "新版本": "New Version", - "关闭": "Close", - "密码已重置并已复制到剪贴板:": "Password has been reset and copied to clipboard: ", - "密码已复制到剪贴板:": "Password has been copied to clipboard: ", - "密码重置确认": "Password Reset Confirmation", - "邮箱地址": "Email address", - "提交": "Submit", - "等待获取邮箱信息...": "Waiting to get email information...", - "确认重置密码": "Confirm Password Reset", - "无效的重置链接,请重新发起密码重置请求": "Invalid reset link, please initiate a new password reset request", - "请输入邮箱地址": "Please enter the email address", - "请稍后几秒重试": "Please retry in a few seconds", - "正在检查用户环境": "Checking user environment", - "重置邮件发送成功": "Reset mail sent successfully", - "请检查邮箱": "Please check your email", - "密码重置": "Password Reset", - "令牌已重置并已复制到剪贴板": "Token has been reset and copied to clipboard", - "邀请链接已复制到剪切板": "Invitation link has been copied to clipboard", - "微信账户绑定成功": "WeChat account binding succeeded", - "验证码发送成功": "Verification code sent successfully", - "邮箱账户绑定成功": "Email account binding succeeded", - "注意": "Note", - "此处生成的令牌用于系统管理": "The token generated here is used for system management", - "而非用于请求 OpenAI 相关的服务": "Not for requesting OpenAI related services", - "请知悉": "Please be aware", - "更新个人信息": "Update Personal Information", - "生成系统访问令牌": "Generate System Access Token", - "复制邀请链接": "Copy Invitation Link", - "账号绑定": "Account Binding", - "绑定微信账号": "Bind WeChat Account", - "微信扫码关注公众号": "Scan the QR code with WeChat to follow the official account", - "输入": "Input", - "输出": "Output", - "验证码": "Verification Code", - "获取验证码": "Get Verification Code", - "三分钟内有效": "Valid for three minutes", - "绑定": "Bind", - "绑定 GitHub 账号": "Bind GitHub Account", - "绑定邮箱地址": "Bind Email Address", - "输入邮箱地址": "Enter Email Address", - "未使用": "Unused", - "已使用": "Used", - "操作成功完成": "Operation successfully completed", - "搜索兑换码的 ID 和名称": "Search for ID and name", - "额度": "Quota", - "创建时间": "Creation Time", - "兑换时间": "Redemption Time", - "尚未兑换": "Not yet redeemed", - "已复制到剪贴板": "Copied to clipboard", - "无法复制到剪贴板": "Unable to copy to clipboard", - "请手动复制": "Please copy manually", - "已将兑换码填入搜索框": "The voucher code has been filled into the search box", - "复制": "Copy", - "添加新的兑换码": "Add a new voucher", - "密码长度不得小于 8 位": "Password length must not be less than 8 characters", - "两次输入的密码不一致": "The two passwords entered do not match", - "注册成功": "Registration succeeded", - "请稍后几秒重试,Turnstile 正在检查用户环境": "Please retry in a few seconds, Turnstile is checking user environment", - "验证码发送成功,请检查你的邮箱": "Verification code sent successfully, please check your email", - "新用户注册": "New User Registration", - "输入用户名,最长 12 位": "Enter username, up to 12 characters", - "输入密码,最短 8 位,最长 20 位": "Enter password, at least 8 characters and up to 20 characters", - "输入验证码": "Enter Verification Code", - "已有账户": "Already have an account", - "点击登录": "Click to log in", - "服务器地址": "Server Address", - "更新服务器地址": "Update Server Address", - "配置登录注册": "Configure Login/Registration", - "允许通过密码进行登录": "Allow login via password", - "允许通过密码进行注册": "Allow registration via password", - "通过密码注册时需要进行邮箱验证": "Email verification is required when registering via password", - "允许通过 GitHub 账户登录 & 注册": "Allow login & registration via GitHub account", - "允许通过微信登录 & 注册": "Allow login & registration via WeChat", - "允许新用户注册(此项为否时,新用户将无法以任何方式进行注册": "Allow new user registration (if this option is off, new users will not be able to register in any way", - "启用 Turnstile 用户校验": "Enable Turnstile user verification", - "配置 SMTP": "Configure SMTP", - "用以支持系统的邮件发送": "To support the system email sending", - "SMTP 服务器地址": "SMTP Server Address", - "例如:smtp.qq.com": "For example: smtp.qq.com", - "SMTP 端口": "SMTP Port", - "默认: 587": "Default: 587", - "SMTP 账户": "SMTP Account", - "通常是邮箱地址": "Usually an email address", - "发送者邮箱": "Sender email", - "通常和邮箱地址保持一致": "Usually consistent with the email address", - "SMTP 访问凭证": "SMTP Access Credential", - "敏感信息不会发送到前端显示": "Sensitive information will not be displayed in the frontend", - "保存 SMTP 设置": "Save SMTP Settings", - "配置 GitHub OAuth App": "Configure GitHub OAuth App", - "用以支持通过 GitHub 进行登录注册": "To support login & registration via GitHub", - "点击此处": "click here", - "管理你的 GitHub OAuth App": "Manage your GitHub OAuth App", - "输入你注册的 GitHub OAuth APP 的 ID": "Enter your registered GitHub OAuth APP ID", - "保存 GitHub OAuth 设置": "Save GitHub OAuth Settings", - "配置 WeChat Server": "Configure WeChat Server", - "用以支持通过微信进行登录注册": "To support login & registration via WeChat", - "了解 WeChat Server": "Learn about WeChat Server", - "WeChat Server 访问凭证": "WeChat Server Access Credential", - "微信公众号二维码图片链接": "WeChat Public Account QR Code Image Link", - "输入一个图片链接": "Enter an image link", - "保存 WeChat Server 设置": "Save WeChat Server Settings", - "配置 Turnstile": "Configure Turnstile", - "用以支持用户校验": "To support user verification", - "管理你的 Turnstile Sites,推荐选择 Invisible Widget Type": "Manage your Turnstile Sites, recommend selecting Invisible Widget Type", - "输入你注册的 Turnstile Site Key": "Enter your registered Turnstile Site Key", - "保存 Turnstile 设置": "Save Turnstile Settings", - "已过期": "Expired", - "已耗尽": "Exhausted", - "搜索令牌的名称 ...": "Search for the name of the token...", - "已用额度": "Quota used", - "剩余额度": "Remaining quota", - "总额度": "Total quota", - "剩余额度/总额度": "Remaining/Total", - "智能熔断": "Smart fallback", - "当前分组为 auto,会自动选择最优分组,当一个组不可用时自动降级到下一个组(熔断机制)": "The current group is auto, it will automatically select the optimal group, and automatically downgrade to the next group when a group is unavailable (breakage mechanism)", - "过期时间": "Expiration time", - "无": "None", - "无限制": "Unlimited", - "永不过期": "Never expires", - "无法复制到剪贴板,请手动复制,已将令牌填入搜索框": "Unable to copy to clipboard, please copy manually, the token has been entered into the search box", - "删除令牌": "Delete Token", - "添加新的令牌": "Add New Token", - "普通用户": "Normal User", - "管理员": "Admin", - "超级管理员": "Super Admin", - "未知身份": "Unknown Identity", - "已激活": "Activated", - "已封禁": "Banned", - "搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...": "Search user ID, username, display name, and email address...", - "用户名": "Username", - "用户角色": "User Role", - "未绑定邮箱地址": "Email not bound", - "请求次数": "Number of Requests", - "提升": "Promote", - "降级": "Demote", - "删除用户": "Delete User", - "添加新的用户": "Add New User", - "自定义": "Custom", - "等价金额:": "Equivalent Amount: ", - "未登录或登录已过期,请重新登录": "Not logged in or login has expired, please log in again", - "请求次数过多,请稍后再试": "Too many requests, please try again later", - "服务器内部错误,请联系管理员": "Server internal error, please contact the administrator", - "本站仅作演示之用,无服务端": "This site is for demonstration purposes only, no server-side", - "超级管理员未设置充值链接!": "Super administrator has not set the recharge link!", - "错误:": "Error: ", - "新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面": "New version available: ${data.version}, please refresh the page using shortcut Shift + F5", - "无法正常连接至服务器": "Unable to connect to the server normally", - "管理渠道": "Manage Channels", - "系统状况": "System Status", - "系统信息": "System Information", - "系统信息总览": "System Information Overview", - "版本": "Version", - "源码": "Source Code", - "启动时间": "Startup Time", - "系统配置": "System Configuration", - "系统配置总览": "System Configuration Overview", - "邮箱验证": "Email Verification", - "未启用": "Not Enabled", - "GitHub 身份验证": "GitHub Authentication", - "微信身份验证": "WeChat Authentication", - "Turnstile 用户校验": "Turnstile User Verification", - "创建新的渠道": "Create New Channel", - "是否自动禁用": "Whether to automatically disable", - "仅当自动禁用开启时有效,关闭后不会自动禁用该渠道": "Only effective when automatic disabling is enabled, after closing, the channel will not be automatically disabled", - "镜像": "Mirror", - "请输入镜像站地址,格式为:https://domain.com,可不填,不填则使用渠道默认值": "Please enter the mirror site address, the format is: https://domain.com, it can be left blank, if left blank, the default value of the channel will be used", - "模型": "Model", - "请选择该通道所支持的模型": "Please select the model supported by the channel", - "填入基础模型": "Fill in the basic model", - "填入所有模型": "Fill in all models", - "清除所有模型": "Clear all models", - "复制所有模型": "Copy all models", - "密钥": "Key", - "请输入密钥": "Please enter the key", - "批量创建": "Batch Create", - "更新渠道信息": "Update Channel Information", - "我的令牌": "My Tokens", - "管理兑换码": "Manage Redeem Codes", - "兑换码": "Redeem Code", - "管理用户": "Manage Users", - "额度明细": "Quota Details", - "运营设置": "Operation Settings", - "其他设置": "Other Settings", - "项目仓库地址": "Project Repository Address", - "可在设置页面设置关于内容,支持 HTML & Markdown": "The About content can be set on the settings page, supporting HTML & Markdown", - "由": "developed by", - "开发,基于": "based on", - "MIT 协议": "MIT License", - "充值额度": "Recharge Quota", - "获取兑换码": "Get Redeem Code", - "一个月后过期": "Expires after one month", - "一天后过期": "Expires after one day", - "一小时后过期": "Expires after one hour", - "一分钟后过期": "Expires after one minute", - "创建新的令牌": "Create New Token", - "令牌分组,默认为用户的分组": "Token group, default is the your's group", - "IP白名单": "IP whitelist", - "令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制": "The quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account", - "无限额度": "Unlimited quota", - "更新令牌信息": "Update Token Information", - "请输入充值码!": "Please enter the recharge code!", - "请输入名称": "Please enter a name", - "请输入密钥,一行一个": "Please enter the key, one per line", - "请输入额度": "Please enter the quota", - "令牌创建成功": "Token created successfully", - "令牌更新成功": "Token updated successfully", - "充值成功!": "Recharge successful!", - "更新用户信息": "Update User Information", - "请输入新的用户名": "Please enter a new username", - "请输入新的密码": "Please enter a new password", - "显示名称": "Display Name", - "请输入新的显示名称": "Please enter a new display name", - "已绑定的 GITHUB 账户": "Bound GitHub Account", - "已绑定的 WECHAT 账户": "Bound WeChat Account", - "已绑定的 EMAIL 账户": "Bound Email Account", - "已绑定的 TELEGRAM 账户": "Bound Telegram Account", - "此项只读,要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "This item is read-only. Users need to bind through the relevant binding button on the personal settings page, and cannot be modified directly", - "用户信息更新成功!": "User information updated successfully!", - "使用明细(总消耗额度:{renderQuota(stat.quota)})": "Usage Details (Total Consumption Quota: {renderQuota(stat.quota)})", - "用户名称": "User Name", - "令牌名称": "Token Name", - "留空则查询全部用户": "Leave blank to query all users", - "留空则查询全部令牌": "Leave blank to query all tokens", - "模型名称": "Model Name", - "留空则查询全部模型": "Leave blank to query all models", - "起始时间": "Start Time", - "结束时间": "End Time", - "查询": "Query", - "提示": "Prompt", - "补全": "Completion", - "消耗额度": "Used Quota", - "渠道不存在:%d": "Channel does not exist: %d", - "数据库一致性已被破坏,请联系管理员": "Database consistency has been broken, please contact the administrator", - "使用近似的方式估算 token 数以减少计算量": "Estimate the number of tokens in an approximate way to reduce computational load", - "请填写ChannelName和ChannelKey!": "Please fill in the ChannelName and ChannelKey!", - "请至少选择一个Model!": "Please select at least one Model!", - "加载关于内容失败": "Failed to load content about", - "用户账户创建成功!": "User account created successfully!", - "生成数量": "Generate quantity", - "请输入生成数量": "Please enter the quantity to generate", - "创建新用户账户": "Create new user account", - "渠道更新成功!": "Channel updated successfully!", - "渠道创建成功!": "Channel created successfully!", - "请选择分组": "Please select a group", - "更新兑换码信息": "Update redemption code information", - "创建新的兑换码": "Create a new redemption code", - "未找到所请求的页面": "The requested page was not found", - "过期时间格式错误!": "Expiration time format error!", - "过期时间不能早于当前时间!": "Expiration time cannot be earlier than the current time!", - "请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制": "Please enter the expiration time, the format is yyyy-MM-dd HH:mm:ss, -1 means no limit", - "此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:": "This is optional, it's a JSON text, the key is the model name requested by the user, and the value is the model name to be replaced, for example:", - "此项可选,输入镜像站地址,格式为:": "This is optional, enter the mirror site address, the format is:", - "模型映射": "Model mapping", - "请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖": "Please enter the default API version, for example: 2023-03-15-preview, this configuration can be overridden by the actual request query parameters", - "默认": "Default", - "图片演示": "Image demo", - "注意,系统请求的时模型名称中的点会被剔除,例如:gpt-4.1会请求为gpt-41,所以在Azure部署的时候,部署模型名称需要手动改为gpt-41": "Note that the dot in the model name requested by the system will be removed, for example: gpt-4.1 will be requested as gpt-41, so when deploying on Azure, the deployment model name needs to be manually changed to gpt-41", - "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "After May 10, 2025, channels added do not need to remove the dot in the model name during deployment", - "模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!", - "取消": "Cancel", - "重置": "Reset", - "请输入新的剩余额度": "Please enter the new remaining quota", - "请输入单个兑换码中包含的额度": "Please enter the quota included in a single redemption code", - "请输入用户名": "Please enter username", - "请输入显示名称": "Please enter display name", - "请输入密码": "Please enter password", - "注意,模型部署名称必须和模型名称保持一致": "Note that the model deployment name must be consistent with the model name", - "请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT", - "请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel", - "Homepage URL 填": "Fill in the Homepage URL", - "Authorization callback URL 填": "Fill in the Authorization callback URL", - "请为通道命名": "Please name the channel", - "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:": "This is optional, used to modify the model name in the request body, it's a JSON string, the key is the model name in the request, and the value is the model name to be replaced, for example:", - "模型重定向": "Model redirection", - "请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel", - "注意,": "Note that, ", - ",图片演示。": "related image demo.", - "令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!", - "代理": "Proxy", - "此项可选,用于通过自定义API地址来进行 API 调用,请输入API地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com", - "取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?", - "按照如下格式输入:": "Enter in the following format:", - "模型版本": "Model version", - "请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1": "Please enter the version of the Starfire model, note that it is the version number in the interface address, for example: v2.1", - "点击查看": "click to view", - "请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!": "Please make sure that the gpt-35-turbo model has been created on Azure, and the apiVersion has been filled in correctly!", - "建议收藏所有地址,以防失联。": "It is recommended to bookmark all addresses to prevent losing contact.", - "无法正常请求API的用户,请联系管理员。": "For users who cannot request the API normally, please contact the administrator.", - "温馨提示": "Kind tips", - "获取API URL列表时发生错误,请稍后重试。": "An error occurred while retrieving the API URL list, please try again later.", - ",时间:": ",time:", - "已用/剩余": "Used/Remaining", - ",点击更新": ", click Update", - "确定是否要清空此渠道记录额度?": "Are you sure you want to clear the record quota of this channel?", - "此修改将不可逆": "This modification will be irreversible", - "优先级": "Priority", - "权重": "Weight", - "测试操作项目组": "Test operation project team", - "确定是否要删除此渠道?": "Are you sure you want to delete this channel?", - "确定是否要复制此渠道?": "Are you sure you want to copy this channel?", - "复制渠道的所有信息": "Copy all information for a channel", - "展开操作": "Expand operation", - "_复制": "_copy", - "渠道未找到,请刷新页面后重试。": "Channel not found, please refresh the page and try again.", - "渠道复制成功": "Channel copy successful", - "渠道复制失败: ": "Channel copy failed:", - "已成功开始测试所有通道,请刷新页面查看结果。": "Testing of all channels has been started successfully, please refresh the page to view the results.", - "请先选择要删除的通道!": "Please select the channel you want to delete first!", - "搜索渠道关键词": "Search channel keywords", - "模型关键字": "model keyword", - "选择分组": "Select group", - "使用ID排序": "Sort by ID", - "是否用ID排序": "Whether to sort by ID", - "确定?": "Sure?", - "确定是否要删除禁用通道?": "Are you sure you want to delete the disabled channel?", - "开启批量操作": "Enable batch selection", - "是否开启批量操作": "Whether to enable batch selection", - "确定是否要删除所选通道?": "Are you sure you want to delete the selected channels?", - "确定是否要修复数据库一致性?": "Are you sure you want to repair database consistency?", - "进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "When performing this operation, it may cause channel access errors. Please only use it when there is a problem with the database.", - "当前没有可用的启用令牌,请确认是否有令牌处于启用状态!": "There are currently no enablement tokens available, please confirm if one is enabled!", - "Midjourney日志": "Midjourney", - "模型列表": "Model list", - "常见问题": "FAQ", - "免费体验": "Free trial", - "新用户注册赠送$": "Free $ for new user registration", - "测试金额": "Test amount", - "请稍后几秒重试,Turnstile 正在检查用户环境!": "Please try again in a few seconds, Turnstile is checking the user environment!", - "您正在使用默认密码!": "You are using the default password!", - "请立刻修改默认密码!": "Please change the default password immediately!", - "请输入用户名和密码!": "Please enter username and password!", - "用户名或邮箱": "Username or email", - "微信扫码登录": "WeChat scan code to log in", - "刷新成功": "Refresh successful", - "刷新失败": "Refresh failed", - "用时/首字": "Time/first word", - "重试": "Retry", - "用户信息": "User information", - "无法复制到剪贴板,请手动复制": "Unable to copy to clipboard, please copy manually", - "消费": "Consume", - "管理": "Manage", - "系统": "System", - "用时": "time", - "首字时间": "First word time", - "是否流式": "Whether to stream", - "非流": "not stream", - "渠道 ID": "Channel ID", - "用户ID": "User ID", - "花费": "Spend", - "列设置": "Column settings", - "补偿": "compensate", - "错误": "mistake", - "未知": "unknown", - "全选": "Select all", - "组名必须唯一": "Group name must be unique", - "解析 JSON 出错:": "Error parsing JSON:", - "解析 GroupModel 时发生错误: ": "An error occurred while parsing GroupModel:", - "GroupModel 未定义,无法更新分组": "GroupModel is not defined, cannot update grouping", - "重置成功": "Reset successful", - "加载数据出错:": "Error loading data:", - "加载数据时发生错误: ": "An error occurred while loading data:", - "部分保存失败,请重试": "Partial saving failed, please try again", - "请检查输入": "Please check your input", - "如何区分不同分组不同模型的价格:供参考的配置方式": "How to distinguish the prices of different models in different groups: configuration method for reference", - "获取价格顺序": "Get price order", - "确定同步远程数据吗?": "Are you sure you want to synchronize remote data?", - "此修改将不可逆!建议同步前先备份自己的设置!": "This modification will be irreversible! It is recommended to back up your settings before synchronizing!", - "模型固定价格(按次计费模型用)": "Model fixed price (for pay-per-view models)", - "模型倍率(按量计费模型用)": "Model magnification (for pay-as-you-go model)", - "为一个 JSON 文本,键为模型名称,值为倍率": "is a JSON text, the key is the model name, and the value is the magnification", - "隐藏": "Hide", - "分组名称": "Group name", - "提交结果": "Results", - "模式": "Mode", - "任务状态": "Status", - "耗时": "Time consuming", - "结果图片": "Result", - "失败原因": "Failure reason", - "全部": "All", - "成功": "Success", - "未启动": "No start", - "队列中": "In queue", - "窗口等待": "window wait", - "失败": "Failed", - "绘图": "Drawing", - "放大": "Upscalers", - "微妙放大": "Upscale (Subtle)", - "创造放大": "Upscale (Creative)", - "强变换": "Low Variation", - "弱变换": "High Variation", - "图生文": "Describe", - "图混合": "Blend", - "重绘": "Vary", - "局部重绘-提交": "Vary Region", - "自定义变焦-提交": "Custom Zoom-Submit", - "窗口处理": "window handling", - "缩词后生图": "epigenetic diagram of abbreviation", - "图生文按钮生图": "Picture and text button", - "任务 ID": "Task ID", - "速度模式": "speed mode", - "错误:未登录或登录已过期,请重新登录!": "Error: Not logged in or your login has expired, please log in again!", - "错误:请求次数过多,请稍后再试!": "Error: Too many requests, please try again later!", - "错误:服务器内部错误,请联系管理员!": "Error: Internal server error, please contact the administrator!", - "本站仅作演示之用,无服务端!": "This site is for demonstration purposes only, no server!", - "已用额度:": "Used amount:", - "请求次数:": "Number of requests:", - "平移": "Pan", - "上传文件": "Upload", - "图生文后生图": "Pictures give rise to text and later pictures", - "已提交": "Submitted", - "重复提交": "Duplicate submission", - "未提交": "Not submitted", - "缩词": "Shorten", - "变焦": "zoom", - "按次计费": "Pay per view", - "按量计费": "Pay as you go", - "标签": "Label", - "人民币": "RMB", - "说明": "illustrate", - "可用性": "Availability", - "数据加载失败": "Data loading failed", - "发生错误,请重试": "An error occurred, please try again", - "本站汇率1美金=": "The exchange rate of this site is 1 USD =", - "模糊搜索": "fuzzy search", - "选择标签": "Select label", - "令牌分组": "Token grouping", - "隐": "hidden", - "本站当前已启用模型": "The model is currently enabled on this site", - "个": " indivual", - "倍率是本站的计算方式,不同模型有着不同的倍率,并非官方价格的多少倍,请务必知晓。": "The magnification is the calculation method of this website. Different models have different magnifications, which are not multiples of the official price. Please be sure to know.", - "所有各厂聊天模型请统一使用OpenAI方式请求,支持OpenAI官方库
Claude()Claude官方格式请求": "Please use the OpenAI method to request all chat models from each factory, and support the OpenAI official library
Claude()Claude official format request", - "分组说明": "Group description", - "倍率是为了方便换算不同价格的模型": "The magnification is to facilitate the conversion of models with different prices.", - "点击查看倍率说明": "Click to view the magnification description", - "显": "show", - "当前分组可用": "Available in current group", - "当前分组不可用": "The current group is unavailable", - "提示:": "input:", - "输入:": "input:", - "补全:": "output:", - "输出:": "output:", - "图片输出:": "Image output:", - "模型价格:": "Model price:", - "模型:": "Model:", - "分组:": "Grouping:", - "最终价格": "final price", - "计费类型": "Billing type", - "美元": "Dollar", - "倍率": "Ratio", - "常见问题不是合法的 JSON 字符串": "FAQ is not a valid JSON string", - "常见问题更新失败": "FAQ update failed", - "活动内容已更新": "Event content has been updated", - "活动内容更新失败": "Event content update failed", - "页脚内容已更新": "Footer content updated", - "页脚内容更新失败": "Footer content update failed", - "Logo 图片地址": "Logo image address", - "在此输入图片地址": "Enter image address here", - "在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。": "Enter the home page content here, support Markdown", - "令牌分组说明": "Token grouping description", - "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。": "Enter new about content here, support Markdown", - "API地址列表": "API address list", - "在此输入新的常见问题,json格式;键为问题,值为答案。": "Enter a new FAQ here in json format; the key is the question and the value is the answer.", - "活动内容": "Activity content", - "在此输入新的活动内容。": "Enter new event content here.", - "总计": "Total", - "无数据": "No data", - "小时": "Hour", - "新密码": "New Password", - "重置邮件发送成功,请检查邮箱!": "The reset email was sent successfully, please check your email!", - "输入你的账户名{{username}}以确认删除": "Enter your account name{{username}}to confirm deletion", - "账户已删除!": "Account has been deleted!", - "微信账户绑定成功!": "WeChat account bound successfully!", - "两次输入的密码不一致!": "The passwords entered twice are inconsistent!", - "密码修改成功!": "Password changed successfully!", - "划转金额最低为": "The minimum transfer amount is", - "请输入邮箱!": "Please enter your email!", - "验证码发送成功,请检查邮箱!": "The verification code was sent successfully, please check your email!", - "请输入邮箱验证码!": "Please enter the email verification code!", - "请输入要划转的数量": "Please enter the amount to be transferred", - "当前余额": "Current balance", - "单独并发限制": "Individual concurrency limits", - "未设置单独并发限制": "No individual concurrency limit is set", - "无效的用户单独并发限制数据": "Invalid user individual concurrency limit data", - "未绑定": "Not bound", - "修改绑定": "Modify binding", - "确认新密码": "Confirm new password", - "历史消耗": "Consumption", - "查看": "Check", - "修改密码": "Change password", - "删除个人账户": "Delete personal account", - "已绑定": "Bound", - "获取二维码失败": "Failed to obtain QR code", - "获取当前设置失败": "Failed to get current settings", - "设置已更新": "Settings updated", - "更新设置失败": "Update settings failed", - "确认解绑": "Confirm unbinding", - "您确定要解绑WxPusher吗?": "Are you sure you want to unbind WxPusher?", - "解绑失败": "Unbinding failed", - "订阅事件": "Subscribe to events", - "通知方式": "Notification method", - "留空将通知到账号邮箱": "Leave this blank to be notified to the account email", - "查看接入文档": "View access documentation", - "企业微信机器人Key": "Enterprise WeChat Robot Key", - "您已绑定WxPusher,可以点击下方解绑": "You have bound WxPusher, you can click below to unbind", - "请扫描二维码绑定WxPusher": "Please scan the QR code to bind WxPusher", - "预警额度(需订阅事件)": "Alert quota (need to subscribe to events)", - " 时,将收到预警邮件(2小时最多1次)": "When, you will receive an early warning email (maximum once every 2 hours)", - "兑换人ID": "Redeemer ID", - "确定是否要删除此兑换码?": "Are you sure you want to delete this redemption code?", - "已复制到剪贴板!": "Copied to clipboard!", - "搜索关键字": "Search keywords", - "关键字(id或者名称)": "Keyword (id or name)", - "复制所选兑换码": "Copy selected redemption code", - "请至少选择一个兑换码!": "Please select at least one redemption code!", - "密码长度不得小于 8 位!": "Password must be at least 8 characters long!", - "注册成功!": "Registration successful!", - "验证码发送成功,请检查你的邮箱!": "The verification code was sent successfully, please check your email!", - "确认密码": "Confirm Password", - "邀请码": "Invitation code", - "输入邀请码": "Enter invitation code", - "账户": "Account", - "邮箱": "Email", - "已有账户?": "Already have an account?", - "创意任务": "Tasks", - "任务ID(点击查看详情)": "Task ID (click to view details)", - "进度": "schedule", - "花费时间": "spend time", - "生成音乐": "generate music", - "生成歌词": "Generate lyrics", - "歌曲拼接": "song splicing", - "上传歌曲": "Upload songs", - "生成视频": "Generate video", - "扩展视频": "Extended video", - "获取无水印": "Get no watermark", - "生成图片": "Generate pictures", - "可灵": "Kling", - "即梦": "Jimeng", - "正在提交": "Submitting", - "执行中": "processing", - "平台": "platform", - "排队中": "Queuing", - "已启用:限制模型": "Enabled: restricted model", - "AMA 问天": "AMA Wentian", - "项目操作按钮组": "Project action button group", - "AMA 问天(BotGem)": "AMA Wentian (BotGem)", - "确定是否要删除此令牌?": "Are you sure you want to delete this token?", - "管理员未设置聊天链接": "The administrator has not set up a chat link", - "复制所选令牌": "Copy selected token", - "请至少选择一个令牌!": "Please select at least one token!", - "管理员未设置查询页链接": "The administrator has not set the query page link", - "批量删除令牌": "Batch delete token", - "确定要删除所选的 {{count}} 个令牌吗?": "Are you sure you want to delete the selected {{count}} tokens?", - "删除所选令牌": "Delete selected token", - "请先选择要删除的令牌!": "Please select the token to be deleted!", - "已删除 {{count}} 个令牌!": "Deleted {{count}} tokens!", - "删除失败": "Delete failed", - "复制令牌": "Copy token", - "请选择你的复制方式": "Please select your copy method", - "名称+密钥": "Name + key", - "仅密钥": "Only key", - "查看API地址": "View API address", - "打开查询页": "Open query page", - "时间(仅显示近3天)": "Time (only displays the last 3 days)", - "请输入兑换码!": "Please enter the redemption code!", - "兑换成功!": "Redemption successful!", - "成功兑换额度:": "Successful redemption amount:", - "请求失败": "Request failed", - "管理员未开启在线充值!": "The administrator has not enabled online recharge!", - "充值数量不能小于": "The recharge amount cannot be less than", - "管理员未开启Stripe在线充值!": "The administrator has not enabled Stripe online recharge!", - "当前充值1美金=": "Current recharge = 1 USD =", - "请选择充值方式!": "Please choose a recharge method!", - "元": "CNY", - "充值记录": "Recharge record", - "返利记录": "Rebate record", - "确定要充值 $": "Confirm to top up $", - "微信/支付宝 实付金额:": "WeChat/Alipay actual payment amount:", - "Stripe 实付金额:": "Stripe actual payment amount:", - "支付中...": "Paying", - "支付宝": "Alipay", - "收益统计": "Income statistics", - "待使用收益": "Proceeds to be used", - "邀请人数": "Number of people invited", - "兑换码充值": "Redemption code recharge", - "奖励说明": "Reward description", - "选择支付方式": "Select payment method", - "在找兑换码?": "Looking for a redemption code? ", - "购买兑换码": "Buy redemption code", - "账户统计": "Account statistics", - "账户充值": "Account recharge", - "多种充值方式,安全便捷": "Multiple recharge methods, safe and convenient", - "支付方式": "Payment method", - "邀请奖励": "Invite reward", - "或输入自定义金额": "Or enter a custom amount", - "选择充值额度": "Select recharge amount", - "实付": "Actual payment", - "快速方便的充值方式": "Quick and convenient recharge method", - "邀请好友获得额外奖励": "Invite friends to get additional rewards", - "邀请好友注册,好友充值后您可获得相应奖励": "Invite friends to register, and you can get the corresponding reward after the friend recharges", - "通过划转功能将奖励额度转入到您的账户余额中": "Transfer the reward amount to your account balance through the transfer function", - "邀请的好友越多,获得的奖励越多": "The more friends you invite, the more rewards you will get", - "在线充值": "Online recharge", - "充值数量,最低 ": "Recharge quantity, minimum", - "请选择充值金额": "Please select the recharge amount", - "微信": "WeChat", - "邀请返利": "Invite rebate", - "总收益": "total revenue", - "邀请信息": "Invitation information", - "代理加盟": "Agent to join", - "代理商信息": "Agent information", - "分红记录": "Dividend record", - "提现记录": "Withdrawal records", - "代理商管理": "Agent management", - "自定义输入": "custom input", - "加载token失败": "Failed to load token", - "配置聊天": "Configure chat", - "模型消耗分布": "Model consumption distribution", - "模型调用次数占比": "Model call ratio", - "用户消耗分布": "User consumption distribution", - "时间粒度": "Time granularity", - "天": "day", - "模型概览": "Model overview", - "用户概览": "User overview", - "正在策划中": "Under planning", - "请求首页内容失败": "Requesting homepage content failed", - "返回首页": "Return to home page", - "获取用户数据时发生错误,请稍后重试。": "An error occurred while retrieving user data, please try again later.", - "无额度": "No limit", - "累计消费": "Accumulated consumption", - "累计请求": "Cumulative requests", - "你好,": "Hello,", - "线路监控": "line monitoring", - "查看全部": "View all", - "异常": "Abnormal", - "的未命名令牌": "unnamed token", - "令牌更新成功!": "Token updated successfully!", - "(origin) Discord原链接": "(origin) Discord original link", - "请选择过期时间": "Please select expiration time", - "数量": "quantity", - "请选择或输入创建令牌的数量": "Please select or enter the number of tokens to create", - "请选择渠道": "Please select a channel", - "允许的IP,一行一个,不填写则不限制": "Allowed IPs, one per line, not filled in means no restrictions", - "IP黑名单": "IP blacklist", - "不允许的IP,一行一个": "IPs not allowed, one per line", - "请选择该渠道所支持的模型": "Please select the model supported by this channel", - "次": "times", - "达到限速报错内容": "Error content when the speed limit is reached", - "不填则使用默认报错": "If not filled in, the default error will be reported.", - "Midjouney 设置 (可选)": "Midjouney settings (optional)", - "令牌纬度控制 Midjouney 配置,设置优先级:令牌 {": "Token latitude controls Midjouney configuration, setting priority: token {", - "图片代理地址最好用自己的,本站绘图量大,公用代理地址可能有时网速不佳": "It is best to use your own image proxy address. This site has a large amount of drawings, and public proxy addresses may sometimes have poor network speeds.", - "【突发备用号池】用于应对高强度风控情况,当普通号池全部重试失败,任务进入备用号池执行并额外计费。": "[Sudden backup number pool] is used to deal with high-intensity risk control situations. When all retries in the ordinary number pool fail, the task will be executed in the backup number pool and additional charges will be incurred.", - "绘图模式": "Drawing mode", - "请选择模式": "Please select mode", - "图片代理方式": "Picture agency method", - "用于替换 https://cdn.discordapp.com 的域名": "The domain name used to replace https://cdn.discordapp.com", - "一个月": "A month", - "一天": "One day", - "令牌渠道分组选择": "Token channel grouping selection", - "只可使用对应分组包含的模型。": "Only models contained in the corresponding group can be used.", - "渠道分组": "Channel grouping", - "安全设置(可选)": "Security settings (optional)", - "IP 限制": "IP restrictions", - "模型限制": "Model restrictions", - "秒": "Second", - "更新令牌后需等待几分钟生效": "It will take a few minutes to take effect after updating the token.", - "一小时": "One hour", - "新建数量": "New quantity", - "未设置": "Not set", - "API文档": "API documentation", - "不是合法的 JSON 字符串": "Not a valid JSON string", - "个人中心": "Personal center", - "代理商": "Agent", - "备注": "Remark", - "工作台": "Workbench", - "已复制:": "Copied:", - "提交时间": "Submission time", - "无法正常连接至服务器!": "Unable to connect to the server properly!", - "无记录": "No record", - "日间模式": "day mode", - "活动福利": "Activity benefits", - "聊天/绘画": "Chat/Draw", - "跟随系统": "Follow the system", - "黑夜模式": "Dark mode", - "管理员设置": "Admin", - "待更新": "To be updated", - "支付中..": "Paying", - "查看图片": "View pictures", - "并发限制": "Concurrency limit", - "正常": "Normal", - "周期": "cycle", - "同步频率10-20分钟": "Synchronization frequency 10-20 minutes", - "模型调用占比": "Model call proportion", - "次,平均每天": "times, average per day", - ",平均每天": ", on average every day", - "启用突发备用号池(建议勾选,极大降低故障率)": "Enable burst backup number pool (it is recommended to check this box to greatly reduce the failure rate)", - "查看说明": "View instructions", - "添加令牌": "Create token", - "IP限制": "IP restrictions", - "令牌纬度控制 Midjouney 配置,设置优先级:令牌 > 路径参数 > 系统默认": "Token latitude controls Midjouney configuration, setting priority: token > path parameter > system default", - "启用速率限制": "Enable rate limiting", - "复制BaseURL": "Copy BaseURL", - "总消耗额度": "Total consumption amount", - "近一分钟内消耗Token数": "Number of tokens consumed in the past minute", - "近一分钟内消耗额度": "Quota consumed in the past minute", - "近一分钟内请求次数": "Number of requests in the past minute", - "预估一天消耗量": "Estimated daily consumption", - "模型固定价格:": "Model fixed price:", - "仅供参考,以实际扣费为准": "For reference only, actual deduction shall prevail", - "导出CSV": "Export CSV", - "流": "stream", - "任务ID": "Task ID", - "周": "week", - "总计:": "Total:", - "划转到余额": "Transfer to balance", - "可用额度": "Available credit", - "邀请码:": "Invitation code:", - "最低": "lowest", - "划转额度": "Transfer amount", - "邀请链接": "Invitation link", - "划转邀请额度": "Transfer invitation quota", - "可用邀请额度": "Available invitation quota", - "更多优惠": "More offers", - "企业微信": "Enterprise WeChat", - "点击解绑WxPusher": "Click to unbind WxPusher", - "点击显示二维码": "Click to display the QR code", - "二维码已过期,点击重新获取": "The QR code has expired, click to get it again", - "邮件": "Mail", - "个人信息": "Personal information", - "余额不足预警": "Insufficient balance warning", - "促销活动通知": "Promotion notification", - "修改密码、邮箱、微信等": "Change password, email, WeChat, etc.", - "更多选项": "More options", - "模型调价通知": "Model price adjustment notice", - "系统公告通知": "System announcement notification", - "订阅管理": "Subscription management", - "防失联-定期通知": "Prevent loss of contact - regular notifications", - "订阅事件后,当事件触发时,您将会收到相应的通知": "After subscribing to the event, you will receive the corresponding notification when the event is triggered.", - "当余额低于 ": "When the balance is lower than", - "保存": "save", - "计费说明": "Billing instructions", - "高稳定性": "High stability", - "没有账号请先": "If you don't have an account, please", - "注册账号": "Register an account", - "第三方登录": "Third party login", - "欢迎回来": "welcome back", - "忘记密码": "forget the password", - "想起来了?": "Remember?", - "退出": "Quit", - "确定": "OK", - "请输入星火大模型版本,注意是接口地址中的版本号,例如:v2[1]": "Please enter the Spark model version, note that it is the version number in the interface address, for example: v2.1", - "等待中": "Waiting", - "所有各厂聊天模型请统一使用OpenAI方式请求,支持OpenAI官方库": "Please use the OpenAI method to request all chat models from each factory, and support the OpenAI official library.", - "实付金额:": "Actual payment amount: ", - "金额": "Amount", - "充值金额": "Recharge amount", - "易支付 实付金额:": "Easy Pay Actual payment amount:", - "微信扫码关注公众号,输入 ": "Scan the QR code on WeChat to follow the official account and enter", - " 获取验证码(三分钟内有效)": "Get verification code (valid within three minutes)", - "不可用模型": "Unavailable model", - "关": "close", - "加载首页内容失败": "Failed to load home page content", - "打开聊天": "Open chat", - "新窗口打开": "New window opens", - "禁用(仍可为用户单独开启)": "Disabled (can still be turned on individually for users)", - "重新配置": "Reconfigure", - "隐藏不可用模型": "Hide unavailable models", - " 时,将收到预警通知(2小时最多1次)": "When, you will receive an early warning notification (maximum once every 2 hours)", - "在iframe中加载": "Load in iframe", - "补全倍率": "Completion ratio", - "保存分组数据失败": "Failed to save group data", - "没有可用的使用信息": "No usage information available", - "使用详情": "Usage details", - "收起": "Collapse", - "计费详情": "Billing details", - "提示Token": "Tip Token", - "补全Token": "Complete Token", - "提示Token详情": "Prompt Token details", - "补全Token详情": "Complete Token details", - "输出Token详情": "Output Token details", - "缓存Token": "CacheToken", - "内部缓存Token": "Internal cache token", - "图像Token": "ImageToken", - "音频Token": "AudioToken", - "开": "open", - "推理Token": "ReasoningToken", - "文本Token": "TextToken", - "显示禁用渠道": "Show disabled channels", - "输入Token详情": "Enter Token details", - "输出Token": "OutputToken", - "隐藏禁用渠道": "Hide disabled channels", - "今日不再提醒": "No more reminders today", - "平台/类型": "Platform/Type", - "平台和类型": "Platforms and types", - "当前选择分组": "Currently selected group", - "表情迁移": "Expression migration", - "音频输入:": "Audio input:", - "音频输出:": "Audio output:", - "风格重绘": "style repaint", - "发送测试通知失败": "Failed to send test notification", - "开始时间": "start time", - "当前所选分组不可用": "The currently selected group is unavailable", - "接口凭证": "Interface credentials", - "文字输入": "Text input", - "文字输出": "text output", - "日志详情": "Log details", - "未完成": "Not completed", - "测试单个渠道操作项目组": "Test a single channel operation project group", - "测试通知": "Test notification", - "测试通知发送成功": "Test notification sent successfully", - "点击此处查看接入文档": "Click here to view access documentation", - "类型1": "Type 1", - "类型1 (Imagine)": "Type 1 (Imagine)", - "类型1价格": "Type 1 price", - "类型2": "Type 2", - "类型2 (Upscale)": "Type 2 (Upscale)", - "类型2价格": "Type 2 price", - "类型3价格": "Type 3 price", - "计费过程": "Binning process", - "语音输入": "Voice input", - "语音输出": "Voice output", - "请在右侧切换到可用分组": "Please switch to available groups on the right", - "请联系管理员~": "Please contact the administrator~", - "调用消费": "Call consumption", - "质量": "quality", - "速度": "speed", - "钉钉机器人Key": "DingTalk Robot Key", - "需要@的用户手机号": "Need @ user mobile phone number", - "(提示": "(hint", - "下载文件": "Download file", - "https...xxx.com.webhook": "", - "搜索渠道的 ID,名称和密钥 ": "", - "搜索用户的 ID,用户名,显示名称,以及邮箱地址 ": "", - "操作失败,重定向至登录界面中": "", - "支付中": "", - "等级": "grade", - "钉钉": "DingTalk", - "模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}": "Model price: ${{price}} * Group ratio: {{ratio}} = ${{total}}", - "输入:${{price}} * {{ratio}} = ${{total}} / 1M tokens": "Prompt: ${{price}} * {{ratio}} = ${{total}} / 1M tokens", - "输出:${{price}} * {{ratio}} = ${{total}} / 1M tokens": "Completion: ${{price}} * {{ratio}} = ${{total}} / 1M tokens", - "图片输入:${{price}} * {{ratio}} = ${{total}} / 1M tokens (图片倍率: {{imageRatio}})": "Image input: ${{price}} * {{ratio}} = ${{total}} / 1M tokens (Image ratio: {{imageRatio}})", - "音频输入:${{price}} * {{ratio}} * {{audioRatio}} = ${{total}} / 1M tokens": "Audio prompt: ${{price}} * {{ratio}} * {{audioRatio}} = ${{total}} / 1M tokens", - "音频提示 {{input}} tokens / 1M tokens * ${{price}} * {{audioRatio}} + 音频补全 {{completion}} tokens / 1M tokens * ${{price}} * {{audioRatio}} * {{audioCompRatio}}": "Audio prompt {{input}} tokens / 1M tokens * ${{price}} * {{audioRatio}} + Audio completion {{completion}} tokens / 1M tokens * ${{price}} * {{audioRatio}} * {{audioCompRatio}}", - "音频输出:${{price}} * {{ratio}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens": "Audio completion: ${{price}} * {{ratio}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens", - "输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Input {{nonImageInput}} tokens + Image input {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + Output {{completion}} tokens / 1M tokens * ${{compPrice}} * Group {{ratio}} = ${{total}}", - "(文字 + 音频)* 分组倍率 {{ratio}} = ${{total}}": "(Text + Audio) * Group ratio {{ratio}} = ${{total}}", - "文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} +": "Text prompt {{input}} tokens / 1M tokens * ${{price}} + Text completion {{completion}} tokens / 1M tokens * ${{compPrice}} +", - "输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Prompt {{input}} tokens / 1M tokens * ${{price}} + Completion {{completion}} tokens / 1M tokens * ${{compPrice}} * Group {{ratio}} = ${{total}}", - "价格:${{price}} * 分组:{{ratio}}": "Price: ${{price}} * Group: {{ratio}}", - "模型: {{ratio}} * 分组: {{groupRatio}}": "Model: {{ratio}} * Group: {{groupRatio}}", - "统计额度": "Statistical quota", - "统计Tokens": "Statistical Tokens", - "统计次数": "Statistical count", - "平均RPM": "Average RPM", - "平均TPM": "Average TPM", - "消耗分布": "Consumption distribution", - "调用次数分布": "Models call distribution", - "消耗趋势": "Consumption trend", - "模型消耗趋势": "Model consumption trend", - "调用次数排行": "Models call ranking", - "模型调用次数排行": "Model call ranking", - "添加渠道": "Add channel", - "测试所有通道": "Test all channels", - "删除禁用通道": "Delete disabled channels", - "修复数据库一致性": "Fix database consistency", - "删除所选通道": "Delete selected channels", - "标签聚合模式": "Enable tag mode", - "没有账户?": "No account? ", - "请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "Please enter AZURE_OPENAI_ENDPOINT, e.g.: https://docs-test-001.openai.azure.com", - "默认 API 版本": "Default API Version", - "请输入默认 API 版本,例如:2025-04-01-preview": "Please enter default API version, e.g.: 2025-04-01-preview.", - "请为渠道命名": "Please name the channel", - "请选择可以使用该渠道的分组": "Please select groups that can use this channel", - "请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit Group ratios in system settings to add new groups:", - "部署地区": "Deployment Region", - "请输入部署地区,例如:us-central1\n支持使用模型映射格式": "Please enter deployment region, e.g.: us-central1\nSupports model mapping format", - "填入模板": "Fill Template", - "鉴权json": "Authentication JSON", - "请输入鉴权json": "Please enter authentication JSON", - "组织": "Organization", - "组织,不填则为默认组织": "Organization, default if empty", - "请输入组织org-xxx": "Please enter organization org-xxx", - "默认测试模型": "Default Test Model", - "不填则为模型列表第一个": "First model in list if empty", - "是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道": "Auto-disable (only effective when auto-disable is enabled). When turned off, this channel will not be automatically disabled", - "状态码复写": "Status Code Override", - "此项可选,用于复写返回的状态码,仅影响本地判断,不修改返回到上游的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:": "Optional, used to override returned status codes, only affects local judgment, does not modify status code returned upstream, e.g. rewriting Claude channel's 400 error to 500 (for retry). Do not abuse this feature. Example:", - "渠道标签": "Channel Tag", - "渠道优先级": "Channel Priority", - "渠道权重": "Channel Weight", - "仅支持 OpenAI 接口格式": "Only OpenAI interface format is supported", - "请填写密钥": "Please enter the key", - "获取模型列表成功": "Successfully retrieved model list", - "获取模型列表失败": "Failed to retrieve model list", - "请填写渠道名称和渠道密钥!": "Please enter channel name and key!", - "请至少选择一个模型!": "Please select at least one model!", - "提交失败,请勿重复提交!": "Submission failed, please do not submit repeatedly!", - "某些模型已存在!": "Some models already exist!", - "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "If you are connecting to upstream One API or New API forwarding projects, please use OpenAI type. Do not use this type unless you know what you are doing.", - "完整的 Base URL,支持变量{model}": "Complete Base URL, supports variable {model}", - "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Please enter complete URL, e.g.: https://api.openai.com/v1/chat/completions", - "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "Optional for API calls through custom API address, do not add /v1 and / at the end", - "私有部署地址": "Private Deployment Address", - "请输入私有部署地址,格式为:https://fastgpt.run/api/openapi": "Please enter private deployment address, format: https://fastgpt.run/api/openapi", - "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "Note: For non-Chat API, please make sure to enter the correct API address, otherwise it may not work", - "请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com": "Please enter the path before /suno, usually the domain, e.g.: https://api.example.com", - "填入相关模型": "Fill Related Models", - "新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出": "When creating a new channel, requests are sent through the current browser; when editing an existing channel, requests are sent through the backend server", - "获取模型列表": "Get Model List", - "填入": "Fill", - "输入自定义模型名称": "Enter Custom Model Name", - "知识库 ID": "Knowledge Base ID", - "请输入知识库 ID,例如:123456": "Please enter knowledge base ID, e.g.: 123456", - "可选值": "Optional value", - "你好": "Hello", - "你好,请问有什么可以帮助您的吗?": "Hello, how may I help you?", - "用户分组": "Your default group", - "每页条数": "Items per page", - "令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。": "Tokens cannot accurately control usage, only for self-use, please do not distribute tokens directly to others.", - "添加兑换码": "Add redemption code", - "复制所选兑换码到剪贴板": "Copy selected redemption codes to clipboard", - "新建兑换码": "Code", - "兑换码更新成功!": "Redemption code updated successfully!", - "兑换码创建成功!": "Redemption code created successfully!", - "兑换码创建成功": "Redemption Code Created", - "兑换码创建成功,是否下载兑换码?": "Redemption code created successfully. Do you want to download it?", - "兑换码将以文本文件的形式下载,文件名为兑换码的名称。": "The redemption code will be downloaded as a text file, with the filename being the redemption code name.", - "模型价格": "Model price", - "按K显示单位": "Display in K", - "可用分组": "Available groups", - "您的默认分组为:{{group}},分组倍率为:{{ratio}}": "Your default group is: {{group}}, group ratio: {{ratio}}", - "按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)": "The cost of pay-as-you-go = Group ratio × Model ratio × (Prompt token number + Completion token number × Completion ratio) / 500000 (Unit: USD)", - "模糊搜索模型名称": "Fuzzy search model name", - "您还未登陆,显示的价格为默认分组倍率: {{ratio}}": "You are not logged in, the displayed price is the default group ratio: {{ratio}}", - "你的分组无权使用该模型": "Your group is not authorized to use this model", - "您的分组可以使用该模型": "Your group can use this model", - "当前查看的分组为:{{group}},倍率为:{{ratio}}": "Current group: {{group}}, ratio: {{ratio}}", - "添加用户": "Add user", - "角色": "Role", - "已绑定的 Telegram 账户": "Bound Telegram account", - "新额度:": "New quota: ", - "需要添加的额度(支持负数)": "Need to add quota (supports negative numbers)", - "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "Read-only, user's personal settings, and cannot be modified directly", - "请输入新的密码,最短 8 位": "Please enter a new password, at least 8 characterss", - "添加额度": "Add quota", - "以下信息不可修改": "The following information cannot be modified", - "充值确认": "Recharge confirmation", - "充值数量": "Recharge quantity", - "实付金额": "Actual payment amount", - "是否确认充值?": "Confirm recharge?", - "默认聊天页面链接": "Default chat page link", - "聊天页面 2 链接": "Chat page 2 link", - "失败重试次数": "Failed retry times", - "额度查询接口返回令牌额度而非用户额度": "Displays token quota instead of user quota", - "默认折叠侧边栏": "Default collapse sidebar", - "聊天链接功能已经弃用,请使用下方聊天设置功能": "Chat link function has been deprecated, please use the chat settings below", - "你似乎并没有修改什么": "You seem to have not modified anything", - "聊天设置": "Chat settings", - "必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能": "Must set all chat links above to empty to use the chat settings below", - "链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1": "The {key} in the link will be automatically replaced with sk-xxxx, the {address} will be automatically replaced with the server address in system settings, and the end will not have / and /v1", - "聊天配置": "Chat configuration", - "保存聊天设置": "Save chat settings", - "绘图设置": "Drawing settings", - "启用绘图功能": "Enable drawing function", - "允许回调(会泄露服务器 IP 地址)": "Allow callback (will leak server IP address)", - "允许 AccountFilter 参数": "Allow AccountFilter parameter", - "开启之后将上游地址替换为服务器地址": "After enabling, the upstream address will be replaced with the server address", - "开启之后会清除用户提示词中的": "After enabling, the user prompt will be cleared", - "检测必须等待绘图成功才能进行放大等操作": "Detection must wait for drawing to succeed before performing zooming and other operations", - "保存绘图设置": "Save drawing settings", - "以及": "and", - "参数": "parameter", - "屏蔽词过滤设置": "Sensitive word filtering settings", - "启用屏蔽词过滤功能": "Enable sensitive word filtering function", - "启用 Prompt 检查": "Enable Prompt check", - "屏蔽词列表": "Sensitive word list", - "一行一个屏蔽词,不需要符号分割": "One line per sensitive word, no symbols are required", - "保存屏蔽词过滤设置": "Save sensitive word filtering settings", - "日志设置": "Log settings", - "日志记录时间": "Log record time", - "请选择日志记录时间": "Please select log record time", - "清除历史日志": "Clear historical logs", - "条日志已清理!": "logs have been cleared!", - "保存日志设置": "Save log settings", - "数据看板设置": "Data dashboard settings", - "启用数据看板(实验性)": "Enable data dashboard (experimental)", - "数据看板更新间隔": "Data dashboard update interval", - "数据看板默认时间粒度": "Data dashboard default time granularity", - "保存数据看板设置": "Save data dashboard settings", - "请选择最长响应时间": "Please select longest response time", - "成功时自动启用通道": "Enable channel when successful", - "分钟": "minutes", - "设置过短会影响数据库性能": "Setting too short will affect database performance", - "仅修改展示粒度,统计精确到小时": "Only modify display granularity, statistics accurate to the hour", - "当运行通道全部测试时,超过此时间将自动禁用通道": "When running all channel tests, the channel will be automatically disabled when this time is exceeded", - "设置公告": "Set notice", - "设置 Logo": "Set Logo", - "设置首页内容": "Set home page content", - "设置关于": "Set about", - "公告已更新": "Notice updated", - "系统名称已更新": "System name updated", - "Logo 已更新": "Logo updated", - "首页内容已更新": "Home page content updated", - "关于已更新": "About updated", - "模型测试": "model test", - "当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。": "Current Midjourney callback is not enabled, some projects may not be able to obtain drawing results, which can be enabled in the operation settings.", - "Telegram 身份验证": "Telegram authentication", - "Linux DO 身份验证": "Linux DO authentication", - "协议": "License", - "修改子渠道权重": "Modify sub-channel weight", - "确定要修改所有子渠道权重为 ": "Confirm to modify all sub-channel weights to ", - " 吗?": "?", - "修改子渠道优先级": "Modify sub-channel priority", - "确定要修改所有子渠道优先级为 ": "Confirm to modify all sub-channel priorities to ", - "分组倍率设置": "Group ratio settings", - "用户可选分组": "User selectable groups", - "保存分组倍率设置": "Save group ratio settings", - "模型倍率设置": "Model ratio settings", - "可视化倍率设置": "Visual model ratio settings", - "确定重置模型倍率吗?": "Confirm to reset model ratio?", - "模型固定价格": "Model price per call", - "模型补全倍率(仅对自定义模型有效)": "Model completion ratio (only effective for custom models)", - "保存模型倍率设置": "Save model ratio settings", - "重置模型倍率": "Reset model ratio", - "一次调用消耗多少刀,优先级大于模型倍率": "How much USD one call costs, priority over model ratio", - "仅对自定义模型有效": "Only effective for custom models", - "添加模型": "Add model", - "应用更改": "Apply changes", - "更多": "Expand more", - "个模型": "models", - "可用模型": "Available models", - "时间范围": "Time range", - "批量设置标签": "Batch set tag", - "请输入要设置的标签名称": "Please enter the tag name to be set", - "请输入标签名称": "Please enter the tag name", - "支持搜索用户的 ID、用户名、显示名称和邮箱地址": "Support searching for user ID, username, display name, and email address", - "已注销": "Logged out", - "自动禁用关键词": "Automatic disable keywords", - "一行一个,不区分大小写": "One line per keyword, not case-sensitive", - "当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道": "When the upstream channel returns an error containing these keywords (not case-sensitive), automatically disable the channel", - "请求并计费模型": "Request and charge model", - "实际模型": "Actual model", - "渠道信息": "Channel information", - "通知设置": "Notification settings", - "Webhook地址": "Webhook URL", - "请输入Webhook地址,例如: https://example.com/webhook": "Please enter the Webhook URL, e.g.: https://example.com/webhook", - "邮件通知": "Email notification", - "Webhook通知": "Webhook notification", - "接口凭证(可选)": "Interface credentials (optional)", - "密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性": "The secret will be added to the request header as a Bearer token to verify the legitimacy of the webhook request", - "Authorization: Bearer your-secret-key": "Authorization: Bearer your-secret-key", - "额度预警阈值": "Quota warning threshold", - "当剩余额度低于此数值时,系统将通过选择的方式发送通知": "When the remaining quota is lower than this value, the system will send a notification through the selected method", - "Webhook请求结构": "Webhook request structure", - "只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求": "Only https is supported, the system will send a notification through POST, please ensure the address can receive POST requests", - "通知邮箱": "Notification email", - "设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱": "Set the email address for receiving quota warning notifications, if not set, the email address bound to the account will be used", - "留空则使用账号绑定的邮箱": "If left blank, the email address bound to the account will be used", - "API地址": "Base URL", - "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "For official channels, the new-api has a built-in address. Unless it is a third-party proxy site or a special Azure access address, there is no need to fill it in", - "渠道额外设置": "Channel extra settings", - "强制格式化": "Force format", - "强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)": "Force format responses to OpenAI standard format (Only for OpenAI channel types)", - "思考内容转换": "Thinking content conversion", - "将 reasoning_content 转换为 标签拼接到内容中": "Convert reasoning_content to tags and append to content", - "透传请求体": "Pass through body", - "启用请求体透传功能": "Enable request body pass-through functionality", - "代理地址": "Proxy address", - "例如: socks5://user:pass@host:port": "e.g.: socks5://user:pass@host:port", - "用于配置网络代理,支持 socks5 协议": "Used to configure network proxy, supports socks5 protocol", - "系统提示词": "System Prompt", - "输入系统提示词,用户的系统提示词将优先于此设置": "Enter system prompt, user's system prompt will take priority over this setting", - "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "User priority: If the user specifies a system prompt in the request, the user's setting will be used first", - "参数覆盖": "Parameters override", - "模型请求速率限制": "Model request rate limit", - "启用用户模型请求速率限制(可能会影响高并发性能)": "Enable user model request rate limit (may affect high concurrency performance)", - "限制周期": "Limit period", - "用户每周期最多请求次数": "User max request times per period", - "用户每周期最多请求完成次数": "User max successful request times per period", - "包括失败请求的次数,0代表不限制": "Including failed request times, 0 means no limit", - "频率限制的周期(分钟)": "Rate limit period (minutes)", - "只包括请求成功的次数": "Only include successful request times", - "保存模型速率限制": "Save model rate limit settings", - "速率限制设置": "Rate limit settings", - "获取启用模型失败:": "Failed to get enabled models:", - "获取启用模型失败": "Failed to get enabled models", - "JSON解析错误:": "JSON parsing error:", - "保存失败:": "Save failed:", - "输入模型倍率": "Enter model ratio", - "输入补全倍率": "Enter completion ratio", - "请输入数字": "Please enter a number", - "模型名称已存在": "Model name already exists", - "添加成功": "Added successfully", - "请先选择需要批量设置的模型": "Please select models for batch setting first", - "请输入模型倍率和补全倍率": "Please enter model ratio and completion ratio", - "请输入有效的数字": "Please enter a valid number", - "请输入填充值": "Please enter a value", - "批量设置成功": "Batch setting successful", - "已为 {{count}} 个模型设置{{type}}": "Set {{type}} for {{count}} models", - "固定价格": "Fixed Price", - "模型倍率和补全倍率": "Model Ratio and Completion Ratio", - "批量设置": "Batch Setting", - "搜索模型名称": "Search model name", - "此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除": "This page only shows models without price or ratio settings. After setting, they will be automatically removed from the list", - "没有未设置的模型": "No unconfigured models", - "定价模式": "Pricing Mode", - "固定价格(每次)": "Fixed Price (per use)", - "输入每次价格": "Enter per-use price", - "批量设置模型参数": "Batch Set Model Parameters", - "设置类型": "Setting Type", - "模型倍率值": "Model Ratio Value", - "补全倍率值": "Completion Ratio Value", - "请输入模型倍率": "Enter model ratio", - "请输入补全倍率": "Enter completion ratio", - "请输入数值": "Enter a value", - "将为选中的 ": "Will set for selected ", - " 个模型设置相同的值": " models with the same value", - "当前设置类型: ": "Current setting type: ", - "固定价格值": "Fixed Price Value", - "未设置倍率模型": "Models without ratio settings", - "模型倍率和补全倍率同时设置": "Both model ratio and completion ratio are set", - "自用模式": "Self-use mode", - "开启后不限制:必须设置模型倍率": "After enabling, no limit: must set model ratio", - "演示站点模式": "Demo site mode", - "当前版本": "Current version", - "Gemini设置": "Gemini settings", - "Gemini安全设置": "Gemini safety settings", - "default为默认设置,可单独设置每个分类的安全等级": "\"default\" is the default setting, and each category can be set separately", - "Gemini版本设置": "Gemini version settings", - "default为默认设置,可单独设置每个模型的版本": "\"default\" is the default setting, and each model can be set separately", - "Claude设置": "Claude settings", - "Claude请求头覆盖": "Claude request header override", - "示例": "Example", - "缺省 MaxTokens": "Default MaxTokens", - "启用Claude思考适配(-thinking后缀)": "Enable Claude thinking adaptation (-thinking suffix)", - "和Claude不同,默认情况下Gemini的思考模型会自动决定要不要思考,就算不开启适配模型也可以正常使用,": "Unlike Claude, Gemini's thinking model automatically decides whether to think by default, and can be used normally even without enabling the adaptation model.", - "如果您需要计费,推荐设置无后缀模型价格按思考价格设置。": "If you need billing, it is recommended to set the no-suffix model price according to the thinking price.", - "支持使用 gemini-2.5-pro-preview-06-05-thinking-128 格式来精确传递思考预算。": "Supports using gemini-2.5-pro-preview-06-05-thinking-128 format to precisely pass thinking budget.", - "启用Gemini思考后缀适配": "Enable Gemini thinking suffix adaptation", - "适配-thinking、-thinking-预算数字和-nothinking后缀": "Adapt -thinking, -thinking-budgetNumber, and -nothinking suffixes", - "思考预算占比": "Thinking budget ratio", - "Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Claude thinking adaptation BudgetTokens = MaxTokens * BudgetTokens percentage", - "思考适配 BudgetTokens 百分比": "Thinking adaptation BudgetTokens percentage", - "0.1-1之间的小数": "Decimal between 0.1 and 1", - "模型相关设置": "Model related settings", - "收起侧边栏": "Collapse sidebar", - "展开侧边栏": "Expand sidebar", - "提示缓存倍率": "Prompt cache ratio", - "缓存:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Cache: ${{price}} * {{ratio}} = ${{total}} / 1M tokens (cache ratio: {{cacheRatio}})", - "提示 {{nonCacheInput}} tokens + 缓存 {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Prompt {{nonCacheInput}} tokens + cache {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + completion {{completion}} tokens / 1M tokens * ${{compPrice}} * group {{ratio}} = ${{total}}", - "缓存 Tokens": "Cache Tokens", - "系统初始化": "System initialization", - "管理员账号已经初始化过,请继续设置其他参数": "The admin account has already been initialized, please continue to set other parameters", - "管理员账号": "Admin account", - "请输入管理员用户名": "Please enter the admin username", - "请输入管理员密码": "Please enter the admin password", - "请确认管理员密码": "Please confirm the admin password", - "请选择使用模式": "Please select the usage mode", - "数据库警告": "Database warning", - "您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!": "You are using the SQLite database. If you are running in a container environment, please ensure that the database file persistence mapping is correctly set, otherwise all data will be lost after container restart!", - "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "It is recommended to use MySQL or PostgreSQL databases in production environments, or ensure that the SQLite database file is mapped to the persistent storage of the host machine.", - "使用模式": "Usage mode", - "对外运营模式": "Default mode", - "密码长度至少为8个字符": "Password must be at least 8 characters long", - "表单引用错误,请刷新页面重试": "Form reference error, please refresh the page and try again", - "初始化系统": "Initialize system", - "支持众多的大模型供应商": "Supporting various LLM providers", - "统一的大模型接口网关": "The Unified LLMs API Gateway", - "更好的价格,更好的稳定性,只需要将模型基址替换为:": "Better price, better stability, no subscription required, just replace the model BASE URL with: ", - "获取密钥": "Get Key", - "关于我们": "About Us", - "关于项目": "About Project", - "联系我们": "Contact Us", - "功能特性": "Features", - "快速开始": "Quick Start", - "安装指南": "Installation Guide", - "API 文档": "API Documentation", - "相关项目": "Related Projects", - "基于New API的项目": "Projects Based on New API", - "版权所有": "All rights reserved", - "设计与开发由": "Designed & Developed with love by", - "演示站点": "Demo Site", - "页面未找到,请检查您的浏览器地址是否正确": "Page not found, please check if your browser address is correct", - "您无权访问此页面,请联系管理员": "You do not have permission to access this page. Please contact the administrator.", - "New API项目仓库地址:": "New API project repository address: ", - "© {{currentYear}}": "© {{currentYear}}", - "| 基于": " | Based on ", - "MIT许可证": "MIT License", - "AGPL v3.0协议": "AGPL v3.0 License", - "本项目根据": "This project is licensed under the ", - "授权,需在遵守": " and must be used in compliance with the ", - "的前提下使用。": ".", - "管理员暂时未设置任何关于内容": "The administrator has not set any custom About content yet", - "早上好": "Good morning", - "中午好": "Good afternoon", - "下午好": "Good afternoon", - "晚上好": "Good evening", - "更多提示信息": "More Prompts", - "新建": "Create", - "更新": "Update", - "基本信息": "Basic Information", - "设置令牌的基本信息": "Set token basic information", - "设置令牌可用额度和数量": "Set token available quota and quantity", - "访问限制": "Access Restrictions", - "设置令牌的访问限制": "Set token access restrictions", - "请勿过度信任此功能,IP可能被伪造": "Do not over-trust this feature, IP can be spoofed", - "模型限制列表": "Model restrictions list", - "请选择该令牌支持的模型,留空支持所有模型": "Select models supported by the token, leave blank to support all models", - "非必要,不建议启用模型限制": "Not necessary, model restrictions are not recommended", - "分组信息": "Group Information", - "设置令牌的分组": "Set token grouping", - "管理员未设置用户可选分组": "Administrator has not set user-selectable groups", - "10个": "10 items", - "20个": "20 items", - "30个": "30 items", - "100个": "100 items", - "Midjourney 任务记录": "Midjourney Task Records", - "任务记录": "Task Records", - "剩余": "Remaining", - "已用": "Used", - "调用": "Calls", - "调用次数": "Call Count", - "邀请": "Invitations", - "收益": "Earnings", - "无邀请人": "No Inviter", - "邀请人": "Inviter", - "设置兑换码的基本信息": "Set redemption code basic information", - "设置兑换码的额度和数量": "Set redemption code quota and quantity", - "编辑用户": "Edit User", - "权限设置": "Permission Settings", - "用户的基本账户信息": "User basic account information", - "用户分组和额度管理": "User Group and Quota Management", - "绑定信息": "Binding Information", - "第三方账户绑定状态(只读)": "Third-party account binding status (read-only)", - "已绑定的 OIDC 账户": "Bound OIDC accounts", - "使用兑换码充值余额": "Recharge balance with redemption code", - "支持多种支付方式": "Support multiple payment methods", - "尊敬的": "Dear", - "请输入兑换码": "Please enter the redemption code", - "在线充值功能未开启": "Online recharge function is not enabled", - "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "The administrator has not enabled the online recharge function, please contact the administrator to enable it or recharge with a redemption code.", - "点击模型名称可复制": "Click the model name to copy", - "管理您的邀请链接和收益": "Manage your invitation link and earnings", - "没有可用模型": "No available models", - "账户绑定": "Account Binding", - "安全设置": "Security Settings", - "系统访问令牌": "System Access Token", - "用于API调用的身份验证令牌,请妥善保管": "Authentication token for API calls, please keep it safe", - "密码管理": "Password Management", - "定期更改密码可以提高账户安全性": "Regularly changing your password can improve account security", - "删除账户": "Delete Account", - "此操作不可逆,所有数据将被永久删除": "This operation is irreversible, all data will be permanently deleted", - "生成令牌": "Generate Token", - "通过邮件接收通知": "Receive notifications via email", - "通过HTTP请求接收通知": "Receive notifications via HTTP request", - "价格设置": "Price Settings", - "重新生成": "Regenerate", - "绑定微信账户": "Bind WeChat Account", - "原密码": "Original Password", - "请输入原密码": "Please enter the original password", - "请输入新密码": "Please enter the new password", - "请再次输入新密码": "Please enter the new password again", - "删除账户确认": "Delete Account Confirmation", - "请输入您的用户名以确认删除": "Please enter your username to confirm deletion", - "接受未设置价格模型": "Accept models without price settings", - "当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用": "Accept calls even if the model has no price settings, use only when you trust the website, which may incur high costs", - "批量操作": "Batch Operations", - "未开始": "Not Started", - "测试中": "Testing", - "请求时长: ${time}s": "Request time: ${time}s", - "搜索模型...": "Search models...", - "批量测试${count}个模型": "Batch test ${count} models", - "测试中...": "Testing...", - "渠道的模型测试": "Channel Model Test", - "共": "Total", - "确定要测试所有通道吗?": "Are you sure you want to test all channels?", - "确定要更新所有已启用通道余额吗?": "Are you sure you want to update the balance of all enabled channels?", - "已选择 ${count} 个渠道": "Selected ${count} channels", - "渠道的基本配置信息": "Channel basic configuration information", - "API 配置": "API Configuration", - "API 地址和相关配置": "API URL and related configuration", - "模型配置": "Model Configuration", - "模型选择和映射设置": "Model selection and mapping settings", - "高级设置": "Advanced Settings", - "渠道的高级配置选项": "Advanced channel configuration options", - "设置说明": "Setting Description", - "此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:": "This is optional, used to configure channel-specific settings, as a JSON string, for example:", - "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:": "This is optional, used to override request parameters. Does not support overriding the stream parameter. As a JSON string, for example:", - "编辑标签": "Edit Tag", - "标签信息": "Tag Information", - "标签的基本配置": "Tag basic configuration", - "所有编辑均为覆盖操作,留空则不更改": "All edits are overwrite operations, leaving blank will not change", - "标签名称": "Tag Name", - "请选择该渠道所支持的模型,留空则不更改": "Please select the models supported by the channel, leaving blank will not change", - "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改": "This is optional, used to modify the model name in the request body, as a JSON string, the key is the model name in the request, the value is the model name to be replaced, leaving blank will not change", - "清空重定向": "Clear redirect", - "不更改": "Not change", - "用户分组配置": "User group configuration", - "请选择可以使用该渠道的分组,留空则不更改": "Please select the groups that can use this channel, leaving blank will not change", - "启用全部": "Enable all", - "禁用全部": "Disable all", - "模型定价": "Model Pricing", - "当前分组": "Current group", - "全部模型": "All Models", - "智谱": "Zhipu AI", - "通义千问": "Qwen", - "文心一言": "ERNIE Bot", - "讯飞星火": "Spark Desk", - "腾讯混元": "Hunyuan", - "360智脑": "360 AI Brain", - "零一万物": "Yi", - "豆包": "Doubao", - "系统公告": "System Notice", - "今日关闭": "Close Today", - "关闭公告": "Close Notice", - "搜索条件": "Search Conditions", - "加载中...": "Loading...", - "正在跳转...": "Redirecting...", - "暂无公告": "No Notice", - "欢迎使用,请完成以下设置以开始使用系统": "Welcome to use, please complete the following settings to start using the system", - "数据库检查": "Database Check", - "验证数据库连接状态": "Verify database connection status", - "设置管理员登录信息": "Set administrator login information", - "选择系统运行模式": "Select system running mode", - "完成初始化": "Complete initialization", - "确认设置并完成初始化": "Confirm settings and complete initialization", - "数据库信息": "Database Information", - "请填写完整的管理员账号信息": "Please fill in the complete administrator account information", - "准备完成初始化": "Ready to complete initialization", - "请确认以下设置信息,点击\"初始化系统\"开始配置": "Please confirm the following settings information, click \"Initialize system\" to start configuration", - "数据库类型": "Database Type", - "您正在使用 MySQL 数据库。MySQL 是一个可靠的关系型数据库管理系统,适合生产环境使用。": "You are using the MySQL database. MySQL is a reliable relational database management system, suitable for production environments.", - "您正在使用 PostgreSQL 数据库。PostgreSQL 是一个功能强大的开源关系型数据库系统,提供了出色的可靠性和数据完整性,适合生产环境使用。": "You are using the PostgreSQL database. PostgreSQL is a powerful open-source relational database system that provides excellent reliability and data integrity, suitable for production environments.", - "选择适合您使用场景的模式": "Select the mode suitable for your usage scenario", - "适用于为多个用户提供服务的场景": "Suitable for scenarios where multiple users are provided.", - "适用于个人使用的场景,不需要设置模型价格": "Suitable for personal use, no need to set model price.", - "适用于展示系统功能的场景,提供基础功能演示": "Suitable for scenarios where the system functions are displayed, providing basic feature demonstrations.", - "账户数据": "Account Data", - "使用统计": "Usage Statistics", - "资源消耗": "Resource Consumption", - "性能指标": "Performance Indicators", - "模型数据分析": "Model Data Analysis", - "搜索无结果": "No results found", - "仪表盘设置": "Dashboard Settings", - "API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)": "API information management, you can configure multiple API addresses for status display and load balancing (maximum 50)", - "线路描述": "Route description", - "颜色": "Color", - "标识颜色": "Identifier color", - "添加API": "Add API", - "API信息": "API Information", - "暂无API信息": "No API information", - "请输入API地址": "Please enter the API address", - "请输入线路描述": "Please enter the route description", - "如:大带宽批量分析图片推荐": "e.g. Large bandwidth batch analysis of image recommendations", - "请输入说明": "Please enter the description", - "如:香港线路": "e.g. Hong Kong line", - "请联系管理员在系统设置中配置API信息": "Please contact the administrator to configure API information in the system settings.", - "请联系管理员在系统设置中配置公告信息": "Please contact the administrator to configure notice information in the system settings.", - "请联系管理员在系统设置中配置常见问答": "Please contact the administrator to configure FAQ information in the system settings.", - "请联系管理员在系统设置中配置Uptime": "Please contact the administrator to configure Uptime in the system settings.", - "确定要删除此API信息吗?": "Are you sure you want to delete this API information?", - "测速": "Speed Test", - "跳转": "Jump", - "批量删除": "Batch Delete", - "常见问答": "FAQ", - "进行中": "Ongoing", - "警告": "Warning", - "添加公告": "Add Notice", - "编辑公告": "Edit Notice", - "公告内容": "Notice Content", - "请输入公告内容": "Please enter the notice content", - "请输入公告内容(支持 Markdown/HTML)": "Please enter the notice content (supports Markdown/HTML)", - "发布日期": "Publish Date", - "请选择发布日期": "Please select the publish date", - "发布时间": "Publish Time", - "公告类型": "Notice Type", - "说明信息": "Description", - "可选,公告的补充说明": "Optional, additional information for the notice", - "确定要删除此公告吗?": "Are you sure you want to delete this notice?", - "系统公告管理,可以发布系统通知和重要消息": "System notice management, you can publish system notices and important messages", - "暂无系统公告": "No system notice", - "添加问答": "Add FAQ", - "编辑问答": "Edit FAQ", - "问题标题": "Question Title", - "请输入问题标题": "Please enter the question title", - "回答内容": "Answer Content", - "请输入回答内容": "Please enter the answer content", - "请输入回答内容(支持 Markdown/HTML)": "Please enter the answer content (supports Markdown/HTML)", - "确定要删除此问答吗?": "Are you sure you want to delete this FAQ?", - "系统公告管理,可以发布系统通知和重要消息(最多100个,前端显示最新20条)": "System notice management, you can publish system notices and important messages (maximum 100, display latest 20 on the front end)", - "常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)": "FAQ management, providing answers to common questions for users (maximum 50, display latest 20 on the front end)", - "暂无常见问答": "No FAQ", - "显示最新20条": "Display latest 20", - "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Uptime Kuma monitoring category management, you can configure multiple monitoring categories for service status display (maximum 20)", - "添加分类": "Add Category", - "分类名称": "Category Name", - "Uptime Kuma地址": "Uptime Kuma Address", - "状态页面Slug": "Status Page Slug", - "请输入分类名称,如:OpenAI、Claude等": "Please enter the category name, such as: OpenAI, Claude, etc.", - "请输入Uptime Kuma服务地址,如:https://status.example.com": "Please enter the Uptime Kuma service address, such as: https://status.example.com", - "请输入状态页面的Slug,如:my-status": "Please enter the slug for the status page, such as: my-status", - "确定要删除此分类吗?": "Are you sure you want to delete this category?", - "配置": "Configure", - "服务监控地址,用于展示服务状态信息": "service monitoring address for displaying status information", - "服务可用性": "Service Status", - "可用率": "Availability", - "有异常": "Abnormal", - "高延迟": "High latency", - "维护中": "Maintenance", - "暂无监控数据": "No monitoring data", - "IP记录": "IP Record", - "记录请求与错误日志 IP": "Record request and error log IP", - "开启后,仅\"消费\"和\"错误\"日志将记录您的客户端IP地址": "After enabling, only \"consumption\" and \"error\" logs will record your client IP address", - "只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录": "Only when the user sets IP recording, the IP recording of request and error type logs will be performed", - "设置保存成功": "Settings saved successfully", - "设置保存失败": "Settings save failed", - "已新增 {{count}} 个模型:{{list}}": "Added {{count}} models: {{list}}", - "未发现新增模型": "No new models were added", - "清除失效兑换码": "Clear invalid redemption codes", - "确定清除所有失效兑换码?": "Are you sure you want to clear all invalid redemption codes?", - "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "This will delete all used, disabled, and expired redemption codes, this operation cannot be undone.", - "选择过期时间(可选,留空为永久)": "Select expiration time (optional, leave blank for permanent)", - "请输入备注(仅管理员可见)": "Please enter a remark (only visible to administrators)", - "上游倍率同步": "Upstream ratio synchronization", - "获取渠道失败:": "Failed to get channels: ", - "请至少选择一个渠道": "Please select at least one channel", - "获取倍率失败:": "Failed to get ratios: ", - "后端请求失败": "Backend request failed", - "部分渠道测试失败:": "Some channels failed to test: ", - "未找到差异化倍率,无需同步": "No differential ratio found, no synchronization is required", - "请求后端接口失败:": "Failed to request the backend interface: ", - "同步成功": "Synchronization successful", - "部分保存失败": "Some settings failed to save", - "保存失败": "Save failed", - "选择同步渠道": "Select synchronization channel", - "应用同步": "Apply synchronization", - "倍率类型": "Ratio type", - "当前值": "Current value", - "上游值": "Upstream value", - "差异": "Difference", - "搜索渠道名称或地址": "Search channel name or address", - "缓存倍率": "Cache ratio", - "暂无差异化倍率显示": "No differential ratio display", - "请先选择同步渠道": "Please select the synchronization channel first", - "与本地相同": "Same as local", - "未找到匹配的模型": "No matching model found", - "暴露倍率接口": "Expose ratio API", - "支付设置": "Payment Settings", - "(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Currently only supports Epay interface, the default callback address is the server address above!)", - "支付地址": "Payment address", - "易支付商户ID": "Epay merchant ID", - "易支付商户密钥": "Epay merchant key", - "回调地址": "Callback address", - "充值价格(x元/美金)": "Recharge price (x yuan/dollar)", - "最低充值美元数量": "Minimum recharge dollar amount", - "充值分组倍率": "Recharge group ratio", - "充值方式设置": "Recharge method settings", - "更新支付设置": "Update payment settings", - "通知": "Notice", - "源地址": "Source address", - "同步接口": "Synchronization interface", - "置信度": "Confidence", - "谨慎": "Cautious", - "该数据可能不可信,请谨慎使用": "This data may not be reliable, please use with caution", - "可信": "Reliable", - "所有上游数据均可信": "All upstream data is reliable", - "以下上游数据可能不可信:": "The following upstream data may not be reliable: ", - "按倍率类型筛选": "Filter by ratio type", - "内容": "Content", - "放大编辑": "Expand editor", - "编辑公告内容": "Edit announcement content", - "自适应列表": "Adaptive list", - "紧凑列表": "Compact list", - "仅显示矛盾倍率": "Only show conflicting ratios", - "矛盾": "Conflict", - "确认冲突项修改": "Confirm conflict item modification", - "该模型存在固定价格与倍率计费方式冲突,请确认选择": "The model has a fixed price and ratio billing method conflict, please confirm the selection", - "当前计费": "Current billing", - "修改为": "Modify to", - "状态筛选": "Status filter", - "没有模型可以复制": "No models to copy", - "模型列表已复制到剪贴板": "Model list copied to clipboard", - "复制失败": "Copy failed", - "复制已选": "Copy selected", - "选择成功": "Selection successful", - "暂无成功模型": "No successful models", - "请先选择模型!": "Please select a model first!", - "已复制 ${count} 个模型": "Copied ${count} models", - "复制失败,请手动复制": "Copy failed, please copy manually", - "过期时间快捷设置": "Expiration time quick settings", - "批量创建时会在名称后自动添加随机后缀": "When creating in batches, a random suffix will be automatically added to the name", - "额度必须大于0": "Quota must be greater than 0", - "生成数量必须大于0": "Generation quantity must be greater than 0", - "可用端点类型": "Supported endpoint types", - "未登录,使用默认分组倍率:": "Not logged in, using default group ratio: ", - "该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置": "This server address will affect the payment callback address and the address displayed on the default homepage, please ensure correct configuration", - "密钥聚合模式": "Key aggregation mode", - "随机": "Random", - "轮询": "Polling", - "密钥文件 (.json)": "Key file (.json)", - "点击上传文件或拖拽文件到这里": "Click to upload file or drag and drop file here", - "仅支持 JSON 文件": "Only JSON files are supported", - "仅支持 JSON 文件,支持多文件": "Only JSON files are supported, multiple files are supported", - "请上传密钥文件": "Please upload the key file", - "请填写部署地区": "Please fill in the deployment region", - "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "Please enter the deployment region, for example: us-central1\nSupports using model mapping format\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}", - "其他": "Other", - "未知渠道": "Unknown channel", - "切换为单密钥模式": "Switch to single key mode", - "将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "Only the first key file will be retained, and the remaining files will be removed. Continue?", - "自定义模型名称": "Custom model name", - "启用全部密钥": "Enable all keys", - "充值价格显示": "Recharge price", - "美元汇率(非充值汇率,仅用于定价页面换算)": "USD exchange rate (not recharge rate, only used for pricing page conversion)", - "美元汇率": "USD exchange rate", - "隐藏操作项": "Hide actions", - "显示操作项": "Show actions", - "用户组": "User group", - "邀请获得额度": "Invitation quota", - "显示第": "Showing", - "条 - 第": "to", - "条,共": "of", - "条": "items", - "选择模型": "Select model", - "已选择 {{selected}} / {{total}}": "Selected {{selected}} / {{total}}", - "新获取的模型": "New models", - "已有的模型": "Existing models", - "搜索模型": "Search models", - "缓存: {{cacheRatio}}": "Cache: {{cacheRatio}}", - "缓存创建: {{cacheCreationRatio}}": "Cache creation: {{cacheCreationRatio}}", - "图片输入: {{imageRatio}}": "Image input: {{imageRatio}}", - "系统提示覆盖": "System prompt override", - "模型: {{ratio}}": "Model: {{ratio}}", - "专属倍率": "Exclusive group ratio", - "匹配类型": "Matching type", - "描述": "Description", - "供应商": "Vendor", - "供应商介绍": "Vendor introduction", - "端点": "Endpoint", - "已绑定渠道": "Bound channels", - "更新时间": "Update time", - "未配置模型": "No model configured", - "预填组管理": "Pre-filled group", - "搜索供应商": "Search vendor", - "新增供应商": "Add vendor", - "创建新的模型": "Create new model", - "更新模型信息": "Update model information", - "请输入模型名称,如:gpt-4": "Please enter the model name, such as: gpt-4", - "设置模型的基本信息": "Set the basic information of the model", - "名称匹配类型": "Name matching type", - "根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含": "Find model metadata based on model name and matching rules, priority: exact > prefix > suffix > contains", - "请选择名称匹配类型": "Please select the name matching type", - "请输入模型描述": "Please enter the model description", - "输入标签或使用\",\"分隔多个标签": "Enter tags or use \",\" to separate multiple tags", - "选择模型供应商": "Select model vendor", - "端点映射": "Endpoint mapping", - "可视化": "Visualization", - "手动编辑": "Manual editing", - "暂无数据,点击下方按钮添加键值对": "No data, click the button below to add key-value pairs", - "添加键值对": "Add key-value pair", - "留空则使用默认端点;支持 {path, method}": "Leave blank to use the default endpoint; supports {path, method}", - "未配置的模型列表": "Models not configured", - "个未配置模型": "models not configured", - "组列表": "Group list", - "管理模型、标签、端点等预填组": "Manage model, tag, endpoint, etc. pre-filled groups", - "新建组": "New group", - "组名": "Group name", - "项目内容": "Item content", - "创建新的预填组": "Create new pre-filled group", - "更新预填组": "Update pre-filled group", - "设置预填组的基本信息": "Set the basic information of the pre-filled group", - "请输入组名": "Please enter the group name", - "请输入组描述": "Please enter the group description", - "项目": "Item", - "输入项目名称,按回车添加": "Enter the item name, press Enter to add", - "键为端点类型,值为路径和方法对象": "The key is the endpoint type, the value is the path and method object", - "模型组": "Model group", - "标签组": "Tag group", - "端点组": "Endpoint group", - "供应商名称": "Vendor name", - "请输入供应商名称,如:OpenAI": "Please enter the vendor name, such as: OpenAI", - "请输入供应商描述": "Please enter the vendor description", - "供应商图标": "Vendor icon", - "请输入图标名称,如:OpenAI、Claude.Color": "Please enter the icon name, such as: OpenAI, Claude.Color", - "图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ": "The icon uses the @lobehub/icons library, such as: OpenAI, Claude.Color, supports chain parameters: OpenAI.Avatar.type={'platform'}, OpenRouter.Avatar.shape={'square'}, query all available icons please ", - "请点击我": "Please click me", - "精确": "Exact", - "前缀": "Prefix", - "后缀": "Suffix", - "包含": "Contains", - "全部供应商": "All vendors", - "筛选": "Filter", - "显示设置": "Display settings", - "可用令牌分组": "Available token groups", - "端点类型": "Endpoint type", - "全部分组": "All groups", - "全部类型": "All types", - "全部端点": "All endpoints", - "全部标签": "All tags", - "显示倍率": "Show ratio", - "表格视图": "Table view", - "模型的详细描述和基本特性": "Detailed description and basic characteristics of the model", - "API端点": "API endpoints", - "模型支持的接口端点信息": "Model supported API endpoint information", - "分组价格": "Group price", - "不同用户分组的价格信息": "Price information for different user groups", - "auto分组调用链路": "auto group call chain", - "查看所有可用的AI模型供应商,包括众多知名供应商的模型。": "View all available AI model suppliers, including models from many well-known suppliers.", - "包含来自未知或未标明供应商的AI模型,这些模型可能来自小型供应商或开源项目。": "Includes AI models from unknown or unmarked suppliers, which may come from small suppliers or open-source projects.", - "该供应商提供多种AI模型,适用于不同的应用场景。": "This supplier provides multiple AI models, suitable for different application scenarios.", - "未知供应商": "Unknown", - "共 {{count}} 个模型": "{{count}} models", - "倍率信息": "Ratio information", - "多密钥管理": "Multi-key management", - "总密钥数": "Total key count", - "随机模式": "Random mode", - "轮询模式": "Polling mode", - "手动禁用": "Manually disabled", - "自动禁用": "Auto disabled", - "暂无密钥数据": "No key data", - "请检查渠道配置或刷新重试": "Please check the channel configuration or refresh and try again", - "全部状态": "All status", - "索引": "Index", - "禁用原因": "Disable reason", - "禁用时间": "Disable time", - "确定要启用所有密钥吗?": "Are you sure you want to enable all keys?", - "确定要禁用所有的密钥吗?": "Are you sure you want to disable all keys?", - "确定要删除所有已自动禁用的密钥吗?": "Are you sure you want to delete all automatically disabled keys?", - "此操作不可撤销,将永久删除已自动禁用的密钥": "This operation cannot be undone, and all automatically disabled keys will be permanently deleted.", - "删除自动禁用密钥": "Delete auto disabled keys", - "确定要删除此密钥吗?": "Are you sure you want to delete this key?", - "此操作不可撤销,将永久删除该密钥": "This operation cannot be undone, and the key will be permanently deleted.", - "密钥已删除": "Key has been deleted", - "删除密钥失败": "Failed to delete key", - "图标": "Icon", - "模型图标": "Model icon", - "请输入图标名称": "Please enter the icon name", - "精确名称匹配": "Exact name matching", - "前缀名称匹配": "Prefix name matching", - "后缀名称匹配": "Suffix name matching", - "包含名称匹配": "Contains name matching", - "展开更多": "Expand more", - "已切换至最优倍率视图,每个模型使用其最低倍率分组": "Switched to the optimal ratio view, each model uses its lowest ratio group", - "两步验证设置": "Two-factor authentication settings", - "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "Two-factor authentication (2FA) provides additional security protection for your account. After enabling, you need to enter your password and the verification code generated by the authenticator application when logging in.", - "启用两步验证": "Enable two-factor authentication", - "禁用两步验证": "Disable two-factor authentication", - "启用两步验证后,登录时需要输入密码和验证器应用生成的验证码": "After enabling two-factor authentication, you need to enter your password and the verification code generated by the authenticator application when logging in", - "禁用两步验证后,登录时只需要输入密码": "After disabling two-factor authentication, you only need to enter your password when logging in", - "验证身份": "Verify identity", - "为了保护您的账户安全,请输入认证器验证码来确认身份": "To protect your account security, please enter the authenticator verification code to confirm your identity", - "输入认证器应用显示的6位数字验证码": "Enter the 6-digit verification code displayed on the authenticator application", - "新的备用恢复代码": "New backup recovery code", - "新的备用码已生成": "New backup code has been generated", - "我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "I have understood that disabling two-factor authentication will permanently delete all related settings and backup codes, this operation cannot be undone", - "账户已锁定": "Account locked", - "剩余备用码:": "Remaining backup codes: ", - "启用验证": "Enable Authentication", - "重新生成备用码": "Regenerate backup codes", - "设置两步验证": "Set up two-factor authentication", - "扫描二维码": "Scan QR code", - "使用认证器应用扫描二维码": "Scan QR code with authenticator app", - "保存备用码": "Save backup codes", - "保存备用码以备不时之需": "Save backup codes for emergencies", - "验证设置": "Verify setup", - "输入验证码完成设置": "Enter verification code to complete setup", - "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "Use an authenticator app (such as Google Authenticator, Microsoft Authenticator) to scan the QR code below:", - "或手动输入密钥:": "Or manually enter the secret:", - "备用恢复代码": "Backup recovery codes", - "复制所有代码": "Copy all codes", - "上一步": "Previous", - "下一步": "Next", - "完成设置并启用两步验证": "Complete setup and enable two-factor authentication", - "确认禁用": "Confirm disable", - "完成": "Complete", - "生成新的备用码": "Generate new backup codes", - "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "Warning: Disabling two-factor authentication will permanently delete your verification settings and all backup codes. This action is irreversible!", - "禁用后的影响:": "Impact after disabling:", - "降低您账户的安全性": "Reduce your account security", - "需要重新完整设置才能再次启用": "Need to set up again to re-enable", - "永久删除您的两步验证设置": "Permanently delete your two-factor authentication settings", - "永久删除所有备用码(包括未使用的)": "Permanently delete all backup codes (including unused ones)", - "请输入认证器验证码或备用码": "Please enter authenticator verification code or backup code", - "重新生成备用码将使现有的备用码失效,请确保您已保存了当前的备用码。": "Regenerating backup codes will invalidate existing backup codes. Please ensure you have saved the current backup codes.", - "请输入认证器验证码": "Please enter authenticator verification code", - "旧的备用码已失效,请保存新的备用码": "Old backup codes have been invalidated, please save the new backup codes", - "获取2FA状态失败": "Failed to get 2FA status", - "设置2FA失败": "Failed to set up 2FA", - "请输入验证码": "Please enter verification code", - "两步验证启用成功!": "Two-factor authentication enabled successfully!", - "启用2FA失败": "Failed to enable 2FA", - "请输入验证码或备用码": "Please enter verification code or backup code", - "请确认您已了解禁用两步验证的后果": "Please confirm that you understand the consequences of disabling two-factor authentication", - "两步验证已禁用": "Two-factor authentication has been disabled", - "禁用2FA失败": "Failed to disable 2FA", - "备用码重新生成成功": "Backup codes regenerated successfully", - "重新生成备用码失败": "Failed to regenerate backup codes", - "备用码已复制到剪贴板": "Backup codes copied to clipboard", - "账户管理": "Account management", - "账户绑定、安全设置和身份验证": "Account binding, security settings and identity verification", - "通知、价格和隐私相关设置": "Notification, price and privacy related settings", - "通知配置": "Notification configuration", - "只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求": "Only HTTPS is supported, the system will send notifications via POST, please ensure that the address can receive POST requests", - "密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性": "The key will be added to the request header as Bearer to verify the legitimacy of the webhook request", - "Webhook请求结构说明": "Webhook request structure description", - "通知类型 (quota_exceed: 额度预警)": "Notification type (quota_exceed: quota warning)", - "通知标题": "Notification title", - "通知内容,支持 {{value}} 变量占位符": "Notification content, supports {{value}} variable placeholders", - "按顺序替换content中的变量占位符": "Replace variable placeholders in content in order", - "Unix时间戳": "Unix timestamp", - "隐私设置": "Privacy settings", - "记录请求与错误日志IP": "Record request and error log IP", - "切换主题": "Switch Theme", - "浅色模式": "Light Mode", - "深色模式": "Dark Mode", - "自动模式": "Auto Mode", - "始终使用浅色主题": "Always use light theme", - "始终使用深色主题": "Always use dark theme", - "跟随系统主题设置": "Follow system theme", - "当前跟随系统": "Currently following system", - "深色": "Dark", - "浅色": "Light", - "点击复制模型名称": "Click to copy model name", - "已复制:{{name}}": "Copied: {{name}}", - "所有密钥已复制到剪贴板": "All keys have been copied to the clipboard", - "密钥已复制到剪贴板": "Key copied to clipboard", - "验证成功": "Verification successful", - "渠道密钥列表": "Channel key list", - "渠道密钥": "Channel key", - "共 {{count}} 个密钥": "{{count}} keys in total", - "复制全部": "Copy all", - "JSON格式密钥,请确保格式正确": "JSON format key, please ensure the format is correct", - "检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。": "Detected multiple keys, you can copy each key individually or click Copy All to get the complete content.", - "安全提醒": "Security reminder", - "请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。": "Keep key information secure, do not disclose to others. If there are security concerns, please change the key immediately.", - "安全验证": "Security verification", - "验证": "Verify", - "为了保护账户安全,请验证您的两步验证码。": "To protect account security, please verify your two-factor authentication code.", - "支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。": "Supports 6-digit TOTP verification code or 8-digit backup code, can be configured or viewed in `Personal Settings - Security Settings - Two-Factor Authentication Settings`.", - "获取密钥失败": "Failed to get key", - "查看密钥": "View key", - "查看渠道密钥": "View channel key", - "渠道密钥信息": "Channel key information", - "密钥获取成功": "Key acquisition successful", - "图片倍率": "Image ratio", - "音频倍率": "Audio ratio", - "音频补全倍率": "Audio completion ratio", - "图片输入相关的倍率设置,键为模型名称,值为倍率": "Image input related ratio settings, key is model name, value is ratio", - "音频输入相关的倍率设置,键为模型名称,值为倍率": "Audio input related ratio settings, key is model name, value is ratio", - "音频输出补全相关的倍率设置,键为模型名称,值为倍率": "Audio output completion related ratio settings, key is model name, value is ratio", - "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-image-1\": 2}": "A JSON text with model name as key and ratio as value, e.g.: {\"gpt-image-1\": 2}", - "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-audio-preview\": 16}": "A JSON text with model name as key and ratio as value, e.g.: {\"gpt-4o-audio-preview\": 16}", - "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-realtime\": 2}": "A JSON text with model name as key and ratio as value, e.g.: {\"gpt-4o-realtime\": 2}", - "顶栏管理": "Header Management", - "控制顶栏模块显示状态,全局生效": "Control header module display status, global effect", - "用户主页,展示系统信息": "User homepage, displaying system information", - "用户控制面板,管理账户": "User control panel for account management", - "模型广场": "Model Marketplace", - "模型定价,需要登录访问": "Model pricing, requires login to access", - "文档": "Documentation", - "系统文档和帮助信息": "System documentation and help information", - "关于系统的详细信息": "Detailed information about the system", - "重置为默认": "Reset to Default", - "保存设置": "Save Settings", - "已重置为默认配置": "Reset to default configuration", - "保存成功": "Saved successfully", - "保存失败,请重试": "Save failed, please try again", - "侧边栏管理(全局控制)": "Sidebar Management (Global Control)", - "全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用": "Global control of sidebar areas and functions, users cannot enable functions hidden by administrators", - "聊天区域": "Chat Area", - "操练场和聊天功能": "Playground and chat functions", - "操练场": "Playground", - "AI模型测试环境": "AI model testing environment", - "聊天": "Chat", - "聊天会话管理": "Chat session management", - "控制台区域": "Console Area", - "数据管理和日志查看": "Data management and log viewing", - "数据看板": "Dashboard", - "系统数据统计": "System data statistics", - "令牌管理": "Token Management", - "API令牌管理": "API token management", - "使用日志": "Usage Logs", - "API使用记录": "API usage records", - "绘图日志": "Drawing Logs", - "绘图任务记录": "Drawing task records", - "任务日志": "Task Logs", - "系统任务记录": "System task records", - "个人中心区域": "Personal Center Area", - "用户个人功能": "User personal functions", - "钱包管理": "Wallet Management", - "余额充值管理": "Balance recharge management", - "个人设置": "Personal Settings", - "个人信息设置": "Personal information settings", - "管理员区域": "Administrator Area", - "系统管理功能": "System management functions", - "渠道管理": "Channel Management", - "API渠道配置": "API channel configuration", - "模型管理": "Model Management", - "AI模型配置": "AI model configuration", - "兑换码管理": "Redemption Code Management", - "兑换码生成管理": "Redemption code generation management", - "用户管理": "User Management", - "用户账户管理": "User account management", - "系统设置": "System Settings", - "系统参数配置": "System parameter configuration", - "边栏设置": "Sidebar Settings", - "您可以个性化设置侧边栏的要显示功能": "You can customize the sidebar functions to display", - "保存边栏设置": "Save Sidebar Settings", - "侧边栏设置保存成功": "Sidebar settings saved successfully", - "需要登录访问": "Require Login", - "开启后未登录用户无法访问模型广场": "When enabled, unauthenticated users cannot access the model marketplace", - "参与官方同步": "Participate in official sync", - "关闭后,此模型将不会被\"同步官方\"自动覆盖或创建": "When turned off, this model will be skipped by Sync official (no auto create/overwrite)", - "同步": "Sync", - "同步向导": "Sync Wizard", - "选择方式": "Select method", - "选择同步来源": "Select sync source", - "选择语言": "Select language", - "选择同步语言": "Select sync language", - "请选择同步语言": "Please select sync language", - "从官方模型库同步": "Sync from official model library", - "官方模型同步": "Official models sync", - "从配置文件同步": "Sync from config file", - "配置文件同步": "Config file sync", - "开始同步": "Start sync", - "选择要覆盖的冲突项": "Select conflict items to overwrite", - "点击查看差异": "Click to view differences", - "无冲突项": "No conflict items", - "应用覆盖": "Apply overwrite", - "仅会覆盖你勾选的字段,未勾选的字段保持本地不变。": "Only selected fields will be overwritten, unselected fields remain unchanged.", - "本地": "Local", - "官方": "Official", - "模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:": "The model community needs everyone's contribution. If you find incorrect data or want to contribute new models, please visit:", - "是": "Yes", - "否": "No", - "原价": "Original price", - "优惠": "Discount", - "折": "% off", - "节省": "Save", - "今天": "Today", - "近 7 天": "Last 7 Days", - "本周": "This Week", - "本月": "This Month", - "近 30 天": "Last 30 Days", - "代理设置": "Proxy Settings", - "更新Worker设置": "Update Worker Settings", - "SSRF防护设置": "SSRF Protection Settings", - "配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全": "Configure Server-Side Request Forgery (SSRF) protection to secure internal network resources", - "SSRF防护详细说明": "SSRF protection prevents malicious users from using your server to access internal network resources. Configure whitelists for trusted domains/IPs and restrict allowed ports. Applies to file downloads, webhooks, and notifications.", - "启用SSRF防护(推荐开启以保护服务器安全)": "Enable SSRF Protection (Recommended for server security)", - "SSRF防护开关详细说明": "Master switch controls whether SSRF protection is enabled. When disabled, all SSRF checks are bypassed, allowing access to any URL. ⚠️ Only disable this feature in completely trusted environments.", - "允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)": "Allow access to private IP addresses (127.0.0.1, 192.168.x.x and other internal addresses)", - "私有IP访问详细说明": "⚠️ Security Warning: Enabling this allows access to internal network resources (localhost, private networks). Only enable if you need to access internal services and understand the security implications.", - "域名白名单": "Domain Whitelist", - "支持通配符格式,如:example.com, *.api.example.com": "Supports wildcard format, e.g.: example.com, *.api.example.com", - "域名白名单详细说明": "Whitelisted domains bypass all SSRF checks and are allowed direct access. Supports exact domains (example.com) or wildcards (*.api.example.com) for subdomains. When whitelist is empty, all domains go through SSRF validation.", - "输入域名后回车,如:example.com": "Enter domain and press Enter, e.g.: example.com", - "支持CIDR格式,如:8.8.8.8, 192.168.1.0/24": "Supports CIDR format, e.g.: 8.8.8.8, 192.168.1.0/24", - "IP白名单详细说明": "Controls which IP addresses are allowed access. Use single IPs (8.8.8.8) or CIDR notation (192.168.1.0/24). Empty whitelist allows all IPs (subject to private IP settings), non-empty whitelist only allows listed IPs.", - "输入IP地址后回车,如:8.8.8.8": "Enter IP address and press Enter, e.g.: 8.8.8.8", - "允许的端口": "Allowed Ports", - "支持单个端口和端口范围,如:80, 443, 8000-8999": "Supports single ports and port ranges, e.g.: 80, 443, 8000-8999", - "端口配置详细说明": "Restrict external requests to specific ports. Use single ports (80, 443) or ranges (8000-8999). Empty list allows all ports. Default includes common web ports.", - "输入端口后回车,如:80 或 8000-8999": "Enter port and press Enter, e.g.: 80 or 8000-8999", - "更新SSRF防护设置": "Update SSRF Protection Settings", - "对域名启用 IP 过滤(实验性)": "Enable IP filtering for domains (experimental)", - "域名IP过滤详细说明": "⚠️ This is an experimental option. A domain may resolve to multiple IPv4/IPv6 addresses. If enabled, ensure the IP filter list covers these addresses, otherwise access may fail.", - "域名黑名单": "Domain Blacklist", - "白名单": "Whitelist", - "黑名单": "Blacklist", - "选择充值套餐": "Choose a top-up package", - "Creem 设置": "Creem Setting", - "Creem 充值": "Creem Recharge", - "Creem 介绍": "Creem is the payment partner you always deserved, we strive for simplicity and straightforwardness on our APIs.", - "Creem Setting Tips": "Creem only supports preset fixed-amount products. These products and their prices need to be created and configured in advance on the Creem website, so custom dynamic amount top-ups are not supported. Configure the product name and price on Creem, obtain the Product Id, and then fill it in for the product below. Set the top-up amount and display price for this product in the new API.", - "API 密钥": "API Secret", - "Webhook 密钥": "Webhook Secret", - "测试模式": "Test Mode", - "Creem API 密钥,敏感信息不显示": "Creem API key, sensitive information not displayed", - "用于验证回调 new-api 的 webhook 请求的密钥,敏感信息不显示": "The key used to validate webhook requests for the callback new-api, sensitive information is not displayed.", - "启用后将使用 Creem Test Mode": "", - "展示价格": "Display Pricing", - "Recharge Quota": "Recharge Quota", - "产品配置": "Product Configuration", - "产品名称": "Product Name", - "产品ID": "Product ID", - "暂无产品配置": "No product configuration", - "更新 Creem 设置": "Update Creem Settings", - "编辑产品": "Edit Product", - "添加产品": "Add Product", - "例如:基础套餐": "e.g.: Basic Package", - "例如:prod_6I8rBerHpPxyoiU9WK4kot": "e.g.: prod_6I8rBerHpPxyoiU9WK4kot", - "货币": "Currency", - "欧元": "EUR", - "USD (美元)": "USD (US Dollar)", - "EUR (欧元)": "EUR (Euro)", - "例如:4.99": "e.g.: 4.99", - "例如:100000": "e.g.: 100000", - "请填写完整的产品信息": "Please fill in complete product information", - "产品ID已存在": "Product ID already exists", - "更新成功": "Update successful", - "更新失败": "Update failed" + "translation": { + " + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_one": " + Web search {{count}} time / 1K times * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_other": " + Web search {{count}} times / 1K times * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + 图片生成调用 {{symbol}}{{price}} / 1次 * {{ratioType}} {{ratio}}": " + Image generation call {{symbol}}{{price}} / 1 time * {{ratioType}} {{ratio}}", + " + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_one": " + File search {{count}} time / 1K times * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_other": " + File search {{count}} times / 1K times * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " 个模型设置相同的值": " models with the same value", + " 吗?": "?", + " 秒": "s", + ",时间:": ",time:", + ",点击更新": ", click Update", + "(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Currently only supports Epay interface, the default callback address is the server address above!)", + "(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "(Input {{input}} tokens / 1M tokens * {{symbol}}{{price}}", + "(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(Input {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + Audio input {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}", + "(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}": "(Input {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + Cache {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}", + "(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * {{symbol}}{{price}}": "(Input {{nonImageInput}} tokens + Image input {{imageInput}} tokens * {{imageRatio}} / 1M tokens * {{symbol}}{{price}}", + "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。": "The maximum value of [Maximum request count] and [Maximum request completion count] is 2147483647.", + "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。": "[Maximum request count] must be greater than or equal to 0, [Maximum request completion count] must be greater than or equal to 1.", + "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}": "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}", + "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}", + "© {{currentYear}}": "© {{currentYear}}", + "| 基于": " | Based on ", + "$/1M tokens": "$/1M tokens", + "0 - 最低": "0 - Lowest", + "0.002-1之间的小数": "Decimal between 0.002-1", + "0.1以上的小数": "Decimal above 0.1", + "10 - 最高": "10 - Highest", + "2 - 低": "2 - Low", + "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "After May 10, 2025, channels added do not need to remove the dot in the model name during deployment", + "360智脑": "360 AI Brain", + "5 - 正常(默认)": "5 - Normal (default)", + "8 - 高": "8 - High", + "AGPL v3.0协议": "AGPL v3.0 License", + "AI 对话": "AI Chat", + "AI模型测试环境": "AI model testing environment", + "AI模型配置": "AI model configuration", + "API Key 模式下不支持批量创建": "Batch creation not supported in API Key mode", + "API 地址和相关配置": "API URL and related configuration", + "API 密钥": "API Key", + "API 文档": "API Documentation", + "API 配置": "API Configuration", + "API令牌管理": "API token management", + "API使用记录": "API usage records", + "API信息": "API Information", + "API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)": "API information management, you can configure multiple API addresses for status display and load balancing (maximum 50)", + "API地址": "Base URL", + "API渠道配置": "API channel configuration", + "API端点": "API endpoints", + "Authorization callback URL 填": "Fill in the Authorization callback URL", + "Authorization Endpoint": "Authorization Endpoint", + "auto分组调用链路": "auto group call chain", + "Bark推送URL": "Bark Push URL", + "Bark推送URL必须以http://或https://开头": "Bark push URL must start with http:// or https://", + "Bark通知": "Bark notification", + "Changing batch type to:": "Changing batch type to:", + "Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Claude thinking adaptation BudgetTokens = MaxTokens * BudgetTokens percentage", + "Claude设置": "Claude settings", + "Claude请求头覆盖": "Claude request header override", + "Client ID": "Client ID", + "Client Secret": "Client Secret", + "common.changeLanguage": "Change Language", + "default为默认设置,可单独设置每个分类的安全等级": "\"default\" is the default setting, and each category can be set separately", + "default为默认设置,可单独设置每个模型的版本": "\"default\" is the default setting, and each model can be set separately", + "Dify渠道只适配chatflow和agent,并且agent不支持图片!": "Dify channel only supports chatflow and agent, and agent does not support images!", + "false": "false", + "Gemini安全设置": "Gemini safety settings", + "Gemini思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Gemini thinking adaptation BudgetTokens = MaxTokens * BudgetTokens percentage", + "Gemini思考适配设置": "Gemini thinking adaptation settings", + "Gemini版本设置": "Gemini version settings", + "Gemini设置": "Gemini settings", + "GitHub": "GitHub", + "GitHub Client ID": "GitHub Client ID", + "GitHub Client Secret": "GitHub Client Secret", + "GitHub ID": "GitHub ID", + "Gotify应用令牌": "Gotify application token", + "Gotify服务器地址": "Gotify server address", + "Gotify服务器地址必须以http://或https://开头": "Gotify server address must start with http:// or https://", + "Gotify通知": "Gotify notification", + "Homepage URL 填": "Fill in the Homepage URL", + "ID": "ID", + "IP": "IP", + "IP白名单": "IP whitelist", + "IP限制": "IP restrictions", + "IP黑名单": "IP blacklist", + "JSON": "JSON", + "JSON 模式支持手动输入或上传服务账号 JSON": "JSON mode supports manual input or upload service account JSON", + "JSON格式密钥,请确保格式正确": "JSON format key, please ensure the format is correct", + "JSON编辑": "JSON Editor", + "JSON解析错误:": "JSON parsing error:", + "Linux DO Client ID": "Linux DO Client ID", + "Linux DO Client Secret": "Linux DO Client Secret", + "LinuxDO": "LinuxDO", + "LinuxDO ID": "LinuxDO ID", + "Logo 图片地址": "Logo image address", + "Midjourney 任务记录": "Midjourney Task Records", + "MIT许可证": "MIT License", + "New API项目仓库地址:": "New API project repository address: ", + "OIDC": "OIDC", + "OIDC ID": "OIDC ID", + "Passkey": "Passkey", + "Passkey 已解绑": "Passkey removed", + "Passkey 已重置": "Passkey has been reset", + "Passkey 是基于 WebAuthn 标准的无密码身份验证方法,支持指纹、面容、硬件密钥等认证方式": "Passkey is a passwordless authentication method based on WebAuthn standard, supporting fingerprint, face recognition, hardware keys and other authentication methods", + "Passkey 注册失败,请重试": "Passkey registration failed. Please try again.", + "Passkey 注册成功": "Passkey registration successful", + "Passkey 登录": "Passkey Login", + "Ping间隔(秒)": "Ping Interval (seconds)", + "price_xxx 的商品价格 ID,新建产品后可获得": "Product price ID for price_xxx, available after creating new product", + "Reasoning Effort": "Reasoning Effort", + "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "The safety_identifier field helps OpenAI identify application users who may violate usage policies. Disabled by default to protect user privacy", + "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "The service_tier field is used to specify service level. Allowing pass-through may result in higher billing than expected. Disabled by default to avoid extra charges", + "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "Stripe key for sk_xxx or rk_xxx, sensitive information not displayed", + "SMTP 发送者邮箱": "SMTP Sender Email", + "SMTP 服务器地址": "SMTP Server Address", + "SMTP 端口": "SMTP Port", + "SMTP 访问凭证": "SMTP Access Credential", + "SMTP 账户": "SMTP Account", + "SSRF防护开关详细说明": "Master switch controls whether SSRF protection is enabled. When disabled, all SSRF checks are bypassed, allowing access to any URL. ⚠️ Only disable this feature in completely trusted environments.", + "SSRF防护设置": "SSRF Protection Settings", + "SSRF防护详细说明": "SSRF protection prevents malicious users from using your server to access internal network resources. Configure whitelists for trusted domains/IPs and restrict allowed ports. Applies to file downloads, webhooks, and notifications.", + "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "The store field authorizes OpenAI to store request data for product evaluation and optimization. Disabled by default. Enabling may cause Codex to malfunction", + "Stripe 设置": "Stripe Settings", + "Telegram": "Telegram", + "Telegram Bot Token": "Telegram Bot Token", + "Telegram Bot 名称": "Telegram Bot Name", + "Telegram ID": "Telegram ID", + "Token Endpoint": "Token Endpoint", + "true": "true", + "Turnstile Secret Key": "Turnstile Secret Key", + "Turnstile Site Key": "Turnstile Site Key", + "Unix时间戳": "Unix timestamp", + "Uptime Kuma地址": "Uptime Kuma Address", + "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Uptime Kuma monitoring category management, you can configure multiple monitoring categories for service status display (maximum 20)", + "URL链接": "URL Link", + "User Info Endpoint": "User Info Endpoint", + "Webhook 签名密钥": "Webhook Signature Key", + "Webhook地址": "Webhook URL", + "Webhook地址必须以https://开头": "Webhook URL must start with https://", + "Webhook请求结构说明": "Webhook request structure description", + "Webhook通知": "Webhook notification", + "Web搜索价格:{{symbol}}{{price}} / 1K 次": "Web Search Price: {{symbol}}{{price}} / 1K requests", + "WeChat Server 服务器地址": "WeChat Server Address", + "WeChat Server 访问凭证": "WeChat Server Access Credential", + "Well-Known URL": "Well-Known URL", + "Well-Known URL 必须以 http:// 或 https:// 开头": "Well-Known URL must start with http:// or https://", + "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示": "Webhook signature key for whsec_xxx, sensitive information not displayed", + "Worker地址": "Worker Address", + "Worker密钥": "Worker Key", + "一个月": "A month", + "一天": "One day", + "一小时": "One hour", + "一次调用消耗多少刀,优先级大于模型倍率": "How much USD one call costs, priority over model ratio", + "一行一个,不区分大小写": "One line per keyword, not case-sensitive", + "一行一个屏蔽词,不需要符号分割": "One line per sensitive word, no symbols are required", + "一键填充到 FluentRead": "One-click fill to FluentRead", + "上一个表单块": "Previous form block", + "上一步": "Previous", + "上次保存: ": "Last saved: ", + "上游倍率同步": "Upstream ratio synchronization", + "下一个表单块": "Next form block", + "下一步": "Next", + "下午好": "Good afternoon", + "不再提醒": "Do not remind again", + "不同用户分组的价格信息": "Price information for different user groups", + "不填则为模型列表第一个": "First model in list if empty", + "不建议使用": "Not recommended", + "不支持": "Not supported", + "不是合法的 JSON 字符串": "Not a valid JSON string", + "不更改": "Not change", + "不限制": "Unlimited", + "与本地相同": "Same as local", + "专属倍率": "Exclusive group ratio", + "两次输入的密码不一致": "The two passwords entered do not match", + "两次输入的密码不一致!": "The passwords entered twice are inconsistent!", + "两步验证": "Two-Factor Authentication", + "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "Two-factor authentication (2FA) provides additional security protection for your account. After enabling, you need to enter your password and the verification code generated by the authenticator application when logging in.", + "两步验证启用成功!": "Two-factor authentication enabled successfully!", + "两步验证已禁用": "Two-factor authentication has been disabled", + "两步验证设置": "Two-factor authentication settings", + "个": "individual", + "个人中心": "Personal center", + "个人中心区域": "Personal Center Area", + "个人信息设置": "Personal information settings", + "个人设置": "Personal Settings", + "个性化设置": "Personalization Settings", + "个性化设置左侧边栏的显示内容": "Personalize the display content of the left sidebar", + "个未配置模型": "models not configured", + "个模型": "models", + "中午好": "Good afternoon", + "为一个 JSON 对象,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}": "Is a JSON object, e.g.: {\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}", + "为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]": "Is a JSON array, e.g.: [10, 20, 50, 100, 200, 500]", + "为一个 JSON 文本": "Is a JSON text", + "为一个 JSON 文本,例如:": "Is a JSON text, e.g.:", + "为一个 JSON 文本,键为分组名称,值为倍率": "Is a JSON text with group name as key and ratio as value", + "为一个 JSON 文本,键为分组名称,值为分组描述": "Is a JSON text with group name as key and group description as value", + "为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 \"gpt-4-gizmo-*\": 0.1,一次消耗0.1刀": "Is a JSON text with model name as key and cost per call as value, e.g.: \"gpt-4-gizmo-*\": 0.1, costs $0.1 per call", + "为一个 JSON 文本,键为模型名称,值为倍率": "Is a JSON text with model name as key and ratio as value", + "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-audio-preview\": 16}": "A JSON text with model name as key and ratio as value, e.g.: {\"gpt-4o-audio-preview\": 16}", + "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-realtime\": 2}": "A JSON text with model name as key and ratio as value, e.g.: {\"gpt-4o-realtime\": 2}", + "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-image-1\": 2}": "A JSON text with model name as key and ratio as value, e.g.: {\"gpt-image-1\": 2}", + "为一个 JSON 文本,键为组名称,值为倍率": "Is a JSON text with group name as key and ratio as value", + "为了保护账户安全,请验证您的两步验证码。": "To protect account security, please verify your two-factor authentication code.", + "为了保护账户安全,请验证您的身份。": "To protect account security, please verify your identity.", + "为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https": "If empty, defaults to server address. Multiple Origins separated by commas, e.g.: https://newapi.pro,https://newapi.com. Note: cannot contain [], must use https", + "主页链接填": "Enter homepage link", + "之前的所有日志": "All previous logs", + "二步验证已重置": "Two-factor authentication has been reset", + "仅会覆盖你勾选的字段,未勾选的字段保持本地不变。": "Only selected fields will be overwritten, unselected fields remain unchanged.", + "仅供参考,以实际扣费为准": "For reference only, actual deduction shall prevail", + "仅保存": "Save Only", + "仅修改展示粒度,统计精确到小时": "Only modify display granularity, statistics accurate to the hour", + "仅密钥": "Only key", + "仅对自定义模型有效": "Only effective for custom models", + "仅当自动禁用开启时有效,关闭后不会自动禁用该渠道": "Only effective when automatic disabling is enabled, after closing, the channel will not be automatically disabled", + "仅支持": "Only supports", + "仅支持 JSON 文件": "Only JSON files are supported", + "仅支持 JSON 文件,支持多文件": "Only JSON files are supported, multiple files are supported", + "仅支持 OpenAI 接口格式": "Only OpenAI interface format is supported", + "仅显示矛盾倍率": "Only show conflicting ratios", + "仅用于开发环境,生产环境应使用 HTTPS": "For development only, use HTTPS in production", + "仅重置配置": "Reset configuration only", + "今日关闭": "Close Today", + "从官方模型库同步": "Sync from official model library", + "从认证器应用中获取验证码,或使用备用码": "Get verification code from authenticator app, or use backup code", + "从配置文件同步": "Sync from config file", + "代理地址": "Proxy address", + "代理设置": "Proxy Settings", + "代码已复制到剪贴板": "Code copied to clipboard", + "令牌": "Tokens", + "令牌分组": "Token grouping", + "令牌分组,默认为用户的分组": "Token group, default is your group", + "令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!", + "令牌名称": "Token Name", + "令牌已重置并已复制到剪贴板": "Token has been reset and copied to clipboard", + "令牌更新成功!": "Token updated successfully!", + "令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制": "The quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account", + "令牌管理": "Token Management", + "以下上游数据可能不可信:": "The following upstream data may not be reliable: ", + "以下文件解析失败,已忽略:{{list}}": "The following files failed to parse and have been ignored: {{list}}", + "以及": "and", + "仪表盘设置": "Dashboard Settings", + "价格": "Pricing", + "价格:${{price}} * {{ratioType}}:{{ratio}}": "Price: ${{price}} * {{ratioType}}: {{ratio}}", + "价格设置": "Price Settings", + "价格设置方式": "Pricing configuration method", + "任务 ID": "Task ID", + "任务ID": "Task ID", + "任务日志": "Task Logs", + "任务状态": "Status", + "任务记录": "Task Records", + "企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选": "Enterprise accounts have special return format and require special handling. If not an enterprise account, do not check this option", + "优先级": "Priority", + "优惠": "Discount", + "低于此额度时将发送邮件提醒用户": "Email reminder will be sent when quota falls below this", + "余额": "Balance", + "余额充值管理": "Balance recharge management", + "你似乎并没有修改什么": "You seem to have not modified anything", + "使用 GitHub 继续": "Continue with GitHub", + "使用 JSON 对象格式,格式为:{\"组名\": [最多请求次数, 最多请求完成次数]}": "Use JSON object format, format: {\"group_name\": [max_requests, max_completions]}", + "使用 LinuxDO 继续": "Continue with LinuxDO", + "使用 OIDC 继续": "Continue with OIDC", + "使用 Passkey 实现免密且更安全的登录体验": "Use Passkey for password-free and more secure login experience", + "使用 Passkey 登录": "Sign in with Passkey", + "使用 Passkey 验证": "Verify with Passkey", + "使用 微信 继续": "Continue with WeChat", + "使用 用户名 注册": "Sign up with Username", + "使用 邮箱或用户名 登录": "Sign in with Email or Username", + "使用ID排序": "Sort by ID", + "使用日志": "Usage Logs", + "使用模式": "Usage mode", + "使用统计": "Usage Statistics", + "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "Use an authenticator app (such as Google Authenticator, Microsoft Authenticator) to scan the QR code below:", + "使用认证器应用扫描二维码": "Scan QR code with authenticator app", + "例如 €, £, Rp, ₩, ₹...": "For example, €, £, Rp, ₩, ₹...", + "例如 https://docs.newapi.pro": "E.g., https://docs.newapi.pro", + "例如:": "For example:", + "例如: socks5://user:pass@host:port": "e.g.: socks5://user:pass@host:port", + "例如:0001": "e.g.: 0001", + "例如:1000": "e.g.: 1000", + "例如:2,就是最低充值2$": "e.g.: 2, means minimum top-up is $2", + "例如:2000": "e.g.: 2000", + "例如:7,就是7元/美金": "e.g.: 7, means 7 yuan per USD", + "例如:example.com": "e.g.: example.com", + "例如:https://yourdomain.com": "e.g.: https://yourdomain.com", + "例如:preview": "e.g.: preview", + "例如发卡网站的购买链接": "E.g., purchase link from card issuing website", + "供应商": "Provider", + "供应商介绍": "Provider introduction", + "供应商信息:": "Provider information:", + "供应商创建成功!": "Provider created successfully!", + "供应商删除成功": "Provider deleted successfully", + "供应商名称": "Provider name", + "供应商图标": "Provider icon", + "供应商更新成功!": "Provider updated successfully!", + "侧边栏管理(全局控制)": "Sidebar Management (Global Control)", + "侧边栏设置保存成功": "Sidebar settings saved successfully", + "保存": "Save", + "保存 GitHub OAuth 设置": "Save GitHub OAuth Settings", + "保存 Linux DO OAuth 设置": "Save Linux DO OAuth Settings", + "保存 OIDC 设置": "Save OIDC Settings", + "保存 Passkey 设置": "Save Passkey Settings", + "保存 SMTP 设置": "Save SMTP Settings", + "保存 Telegram 登录设置": "Save Telegram Login Settings", + "保存 Turnstile 设置": "Save Turnstile Settings", + "保存 WeChat Server 设置": "Save WeChat Server Settings", + "保存分组倍率设置": "Save group ratio settings", + "保存备用码": "Save backup codes", + "保存备用码以备不时之需": "Save backup codes for emergencies", + "保存失败": "Save failed", + "保存失败,请重试": "Save failed, please try again", + "保存失败:": "Save failed:", + "保存屏蔽词过滤设置": "Save sensitive word filtering settings", + "保存成功": "Saved successfully", + "保存数据看板设置": "Save data dashboard settings", + "保存日志设置": "Save log settings", + "保存模型倍率设置": "Save model ratio settings", + "保存模型速率限制": "Save model rate limit settings", + "保存监控设置": "Save Monitoring Settings", + "保存绘图设置": "Save drawing settings", + "保存聊天设置": "Save chat settings", + "保存设置": "Save Settings", + "保存通用设置": "Save General Settings", + "保存邮箱域名白名单设置": "Save Email Domain Whitelist Settings", + "保存额度设置": "Save Quota Settings", + "修复数据库一致性": "Fix database consistency", + "修改为": "Modify to", + "修改子渠道优先级": "Modify sub-channel priority", + "修改子渠道权重": "Modify sub-channel weight", + "修改密码": "Change password", + "修改绑定": "Modify binding", + "倍率": "Ratio", + "倍率信息": "Ratio information", + "倍率是为了方便换算不同价格的模型": "The magnification is to facilitate the conversion of models with different prices.", + "倍率模式": "Ratio Mode", + "倍率类型": "Ratio type", + "停止测试": "Stop Testing", + "允许 AccountFilter 参数": "Allow AccountFilter parameter", + "允许 HTTP 协议图片请求(适用于自部署代理)": "Allow HTTP protocol image requests (for self-deployed proxies)", + "允许 safety_identifier 透传": "Allow safety_identifier Pass-through", + "允许 service_tier 透传": "Allow service_tier Pass-through", + "允许 Turnstile 用户校验": "Allow Turnstile user verification", + "允许不安全的 Origin(HTTP)": "Allow insecure Origin (HTTP)", + "允许回调(会泄露服务器 IP 地址)": "Allow callback (will leak server IP address)", + "允许在 Stripe 支付中输入促销码": "Allow entering promotion codes during Stripe checkout", + "允许新用户注册": "Allow new user registration", + "允许的 Origins": "Allowed Origins", + "允许的IP,一行一个,不填写则不限制": "Allowed IPs, one per line, not filled in means no restrictions", + "允许的端口": "Allowed Ports", + "允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)": "Allow access to private IP addresses (127.0.0.1, 192.168.x.x and other internal addresses)", + "允许通过 GitHub 账户登录 & 注册": "Allow login & registration via GitHub account", + "允许通过 Linux DO 账户登录 & 注册": "Allow login & registration via Linux DO account", + "允许通过 OIDC 进行登录": "Allow login via OIDC", + "允许通过 Passkey 登录 & 认证": "Allow login & authentication via Passkey", + "允许通过 Telegram 进行登录": "Allow login via Telegram", + "允许通过密码进行注册": "Allow registration via password", + "允许通过密码进行登录": "Allow login via password", + "允许通过微信登录 & 注册": "Allow login & registration via WeChat", + "元": "CNY", + "充值": "Top Up", + "充值价格(x元/美金)": "Top Up price (x yuan/dollar)", + "充值价格显示": "Top Up price", + "充值分组倍率": "Top Up group ratio", + "充值分组倍率不是合法的 JSON 字符串": "Top Up group ratio is not a valid JSON string", + "充值数量": "Top Up quantity", + "充值数量,最低 ": "Top Up quantity, minimum", + "充值数量不能小于": "The top up amount cannot be less than", + "充值方式设置": "Top Up method settings", + "充值方式设置不是合法的 JSON 字符串": "Top Up method settings is not a valid JSON string", + "充值确认": "Top Up confirmation", + "充值账单": "Top Up Bills", + "充值金额折扣配置": "Top Up amount discount configuration", + "充值金额折扣配置不是合法的 JSON 对象": "Top Up amount discount configuration is not a valid JSON object", + "充值链接": "Top Up Link", + "充值额度": "Top Up Quota", + "兑换人ID": "Redeemer ID", + "兑换成功!": "Redemption successful!", + "兑换码充值": "Redemption code recharge", + "兑换码创建成功": "Redemption Code Created", + "兑换码创建成功,是否下载兑换码?": "Redemption code created successfully. Do you want to download it?", + "兑换码创建成功!": "Redemption code created successfully!", + "兑换码将以文本文件的形式下载,文件名为兑换码的名称。": "The redemption code will be downloaded as a text file, with the filename being the redemption code name.", + "兑换码更新成功!": "Redemption code updated successfully!", + "兑换码生成管理": "Redemption code generation management", + "兑换码管理": "Redemption Code Management", + "兑换额度": "Redeem", + "全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用": "Global control of sidebar areas and functions, users cannot enable functions hidden by administrators", + "全局设置": "Global Settings", + "全选": "Select all", + "全部": "All", + "全部供应商": "All vendors", + "全部分组": "All groups", + "全部标签": "All tags", + "全部模型": "All Models", + "全部状态": "All status", + "全部端点": "All endpoints", + "全部类型": "All types", + "公告": "Announcement", + "公告内容": "Notice Content", + "公告已更新": "Notice updated", + "公告更新失败": "Notice update failed", + "公告类型": "Notice Type", + "共": "Total", + "共 {{count}} 个密钥_one": "{{count}} key", + "共 {{count}} 个密钥_other": "{{count}} keys", + "共 {{count}} 个模型": "{{count}} models", + "共 {{total}} 项,当前显示 {{start}}-{{end}} 项": "{{total}} items total, showing {{start}}-{{end}} items", + "关": "close", + "关于": "About", + "关于我们": "About Us", + "关于系统的详细信息": "Detailed information about the system", + "关于项目": "About Project", + "关键字(id或者名称)": "Keyword (id or name)", + "关闭": "Close", + "关闭侧边栏": "Close sidebar", + "关闭公告": "Close Notice", + "关闭后,此模型将不会被“同步官方”自动覆盖或创建": "After closing, this model will not be automatically overwritten or created by \"Sync Official\"", + "关闭弹窗,已停止批量测试": "Dialog closed, batch testing stopped", + "其他": "Other", + "其他注册选项": "Other registration options", + "其他登录选项": "Other login options", + "其他设置": "Other Settings", + "内容": "Content", + "内容较大,已启用性能优化模式": "Content is large, performance optimization mode enabled", + "内容较大,部分功能可能受限": "Content is large, some features may be limited", + "最低": "lowest", + "最低充值美元数量": "Minimum recharge dollar amount", + "最后使用时间": "Last used time", + "最后请求": "Last request", + "准备完成初始化": "Ready to complete initialization", + "分类名称": "Category Name", + "分组": "Group", + "分组与模型定价设置": "Group and Model Pricing Settings", + "分组价格": "Group price", + "分组倍率": "Group ratio", + "分组倍率设置": "Group ratio settings", + "分组倍率设置,可以在此处新增分组或修改现有分组的倍率,格式为 JSON 字符串,例如:{\"vip\": 0.5, \"test\": 1},表示 vip 分组的倍率为 0.5,test 分组的倍率为 1": "Group ratio settings, you can add new groups or modify existing group ratios here, format as JSON string, e.g.: {\"vip\": 0.5, \"test\": 1}, indicating vip group ratio is 0.5, test group ratio is 1", + "分组特殊倍率": "Group special ratio", + "分组设置": "Group settings", + "分组速率配置优先级高于全局速率限制。": "Group rate configuration priority is higher than global rate limit.", + "分组速率限制": "Group rate limit", + "分钟": "minutes", + "切换为Assistant角色": "Switch to Assistant role", + "切换为System角色": "Switch to System role", + "切换为单密钥模式": "Switch to single key mode", + "切换主题": "Switch Theme", + "划转到余额": "Transfer to balance", + "划转邀请额度": "Transfer invitation quota", + "划转金额最低为": "The minimum transfer amount is", + "划转额度": "Transfer amount", + "列设置": "Column settings", + "创建令牌默认选择auto分组,初始令牌也将设为auto(否则留空,为用户默认分组)": "Create token with auto group by default, initial token will also be set to auto (otherwise leave blank for user default group)", + "创建失败": "Creation failed", + "创建成功": "Creation successful", + "创建新用户账户": "Create new user account", + "创建新的令牌": "Create New Token", + "创建新的兑换码": "Create a new redemption code", + "创建新的模型": "Create new model", + "创建新的渠道": "Create New Channel", + "创建新的预填组": "Create new pre-filled group", + "创建时间": "Creation Time", + "创建用户": "Create User", + "初始化失败,请重试": "Initialization failed, please retry", + "初始化系统": "Initialize system", + "删除": "Delete", + "删除失败": "Delete failed", + "删除密钥失败": "Failed to delete key", + "删除成功": "Delete successful", + "删除所选": "Delete Selected", + "删除所选令牌": "Delete selected token", + "删除所选通道": "Delete selected channels", + "删除禁用密钥失败": "Failed to delete disabled keys", + "删除禁用通道": "Delete disabled channels", + "删除自动禁用密钥": "Delete auto disabled keys", + "删除账户": "Delete Account", + "删除账户确认": "Delete Account Confirmation", + "刷新": "Refresh", + "刷新失败": "Refresh failed", + "前缀": "Prefix", + "剩余备用码:": "Remaining backup codes: ", + "剩余额度": "Remaining quota", + "剩余额度/总额度": "Remaining/Total", + "剩余额度$": "Remaining quota $", + "功能特性": "Features", + "加入预填组": "Join Pre-filled Group", + "加载中...": "Loading...", + "加载供应商信息失败": "Failed to load supplier information", + "加载关于内容失败...": "Failed to load about content...", + "加载分组失败": "Failed to load groups", + "加载失败": "Load failed", + "加载模型信息失败": "Failed to load model information", + "加载模型失败": "Failed to load models", + "加载用户协议内容失败...": "Failed to load user agreement content...", + "加载账单失败": "Failed to load bills", + "加载隐私政策内容失败...": "Failed to load privacy policy content...", + "包含": "Contains", + "包含来自未知或未标明供应商的AI模型,这些模型可能来自小型供应商或开源项目。": "Includes AI models from unknown or unmarked suppliers, which may come from small suppliers or open-source projects.", + "包括失败请求的次数,0代表不限制": "Including failed request times, 0 means no limit", + "匹配类型": "Matching type", + "区域": "Region", + "历史消耗": "Consumption", + "原价": "Original price", + "原因:": "Reason: ", + "原密码": "Original Password", + "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Deduplication completed: {{before}} keys before deduplication, {{after}} keys after deduplication", + "参与官方同步": "Participate in official sync", + "参数": "parameter", + "参数值": "Parameter value", + "参数覆盖": "Parameters override", + "参照生视频": "Reference video generation", + "友情链接": "Friendly links", + "发布日期": "Publish Date", + "发布时间": "Publish Time", + "取消": "Cancel", + "取消全选": "Deselect all", + "变换": "Transform", + "变焦": "zoom", + "只包括请求成功的次数": "Only include successful request times", + "只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求": "Only HTTPS is supported, the system will send notifications via POST, please ensure that the address can receive POST requests", + "只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录": "Only when the user sets IP recording, the IP recording of request and error type logs will be performed", + "可信": "Reliable", + "可在设置页面设置关于内容,支持 HTML & Markdown": "The About content can be set on the settings page, supporting HTML & Markdown", + "可用令牌分组": "Available token groups", + "可用分组": "Available groups", + "可用模型": "Available models", + "可用端点类型": "Supported endpoint types", + "可用邀请额度": "Available invitation quota", + "可视化": "Visualization", + "可视化倍率设置": "Visual model ratio settings", + "可视化编辑": "Visual editing", + "可选,公告的补充说明": "Optional, additional information for the notice", + "可选值": "Optional value", + "同时重置消息": "Reset messages simultaneously", + "同步": "Sync", + "同步向导": "Sync Wizard", + "同步失败": "Synchronization failed", + "同步成功": "Synchronization successful", + "同步接口": "Synchronization interface", + "名称": "Name", + "名称+密钥": "Name + key", + "名称不能为空": "Name cannot be empty", + "名称匹配类型": "Name matching type", + "后端请求失败": "Backend request failed", + "后缀": "Suffix", + "否": "No", + "启动时间": "Startup Time", + "启用": "Enable", + "启用 Prompt 检查": "Enable Prompt check", + "启用2FA失败": "Failed to enable Two-Factor Authentication", + "启用Claude思考适配(-thinking后缀)": "Enable Claude thinking adaptation (-thinking suffix)", + "启用Gemini思考后缀适配": "Enable Gemini thinking suffix adaptation", + "启用Ping间隔": "Enable Ping interval", + "启用SMTP SSL": "Enable SMTP SSL", + "启用SSRF防护(推荐开启以保护服务器安全)": "Enable SSRF Protection (Recommended for server security)", + "启用全部": "Enable all", + "启用密钥失败": "Failed to enable key", + "启用屏蔽词过滤功能": "Enable sensitive word filtering function", + "启用所有密钥失败": "Failed to enable all keys", + "启用数据看板(实验性)": "Enable data dashboard (experimental)", + "启用用户模型请求速率限制(可能会影响高并发性能)": "Enable user model request rate limit (may affect high concurrency performance)", + "启用绘图功能": "Enable drawing function", + "启用请求体透传功能": "Enable request body pass-through functionality", + "启用请求透传": "Enable request pass-through", + "启用额度消费日志记录": "Enable quota consumption logging", + "启用验证": "Enable Authentication", + "周": "week", + "和": "and", + "响应": "Response", + "响应时间": "Response time", + "商品价格 ID": "Product Price ID", + "回答内容": "Answer Content", + "回调 URL 填": "Callback URL Fill", + "回调地址": "Callback address", + "固定价格": "Fixed Price", + "固定价格(每次)": "Fixed Price (per use)", + "固定价格值": "Fixed Price Value", + "图像生成": "Image Generation", + "图标": "Icon", + "图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ": "The icon uses the @lobehub/icons library, such as: OpenAI, Claude.Color, supports chain parameters: OpenAI.Avatar.type={'platform'}, OpenRouter.Avatar.shape={'square'}, query all available icons please ", + "图混合": "Blend", + "图片生成调用:{{symbol}}{{price}} / 1次": "Image generation call: {{symbol}}{{price}} / 1 time", + "图片输入: {{imageRatio}}": "Image input: {{imageRatio}}", + "图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})": "Image input price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (Image ratio: {{imageRatio}})", + "图片输入倍率(仅部分模型支持该计费)": "Image input ratio (only supported by some models for billing)", + "图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费": "Ratio settings related to image input, key is model name, value is ratio, only supported by some models for billing", + "图生文": "Describe", + "图生视频": "Image to Video", + "在Gotify服务器创建应用后获得的令牌,用于发送通知": "Token obtained after creating an application on the Gotify server, used to send notifications", + "在Gotify服务器的应用管理中创建新应用": "Create a new application in the Gotify server's application management", + "在找兑换码?": "Looking for a redemption code? ", + "在此输入 Logo 图片地址": "Enter the Logo image URL here", + "在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code", + "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面": "Enter new about content here, support Markdown", + "在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码": "Enter the new footer here, leave blank to use the default footer, supports HTML code.", + "在此输入用户协议内容,支持 Markdown & HTML 代码": "Enter user agreement content here, supports Markdown & HTML code", + "在此输入系统名称": "Enter the system name here", + "在此输入隐私政策内容,支持 Markdown & HTML 代码": "Enter privacy policy content here, supports Markdown & HTML code", + "在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "Enter the home page content here, supports Markdown", + "域名IP过滤详细说明": "⚠️ This is an experimental option. A domain may resolve to multiple IPv4/IPv6 addresses. If enabled, ensure the IP filter list covers these addresses, otherwise access may fail.", + "域名白名单": "Domain Whitelist", + "域名黑名单": "Domain Blacklist", + "基本信息": "Basic Information", + "填入": "Fill", + "填入所有模型": "Fill in all models", + "填入模板": "Fill Template", + "填入相关模型": "Fill Related Models", + "填写Gotify服务器的完整URL地址": "Fill in the complete URL address of the Gotify server", + "填写带https的域名,逗号分隔": "Fill in domains with https, separated by commas", + "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "After filling in the user agreement content, users will be required to check that they have read the user agreement during registration", + "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "After filling in the privacy policy content, users will be required to check that they have read the privacy policy during registration", + "备份支持": "Backup support", + "备份状态": "Backup state", + "备注": "Remark", + "备用恢复代码": "Backup recovery codes", + "备用码已复制到剪贴板": "Backup codes copied to clipboard", + "备用码重新生成成功": "Backup codes regenerated successfully", + "复制": "Copy", + "复制代码": "Copy code", + "复制令牌": "Copy token", + "复制全部": "Copy all", + "复制名称": "Copy name", + "复制失败": "Copy failed", + "复制失败,请手动复制": "Copy failed, please copy manually", + "复制已选": "Copy selected", + "复制应用的令牌(Token)并填写到上方的应用令牌字段": "Copy the application token and fill it in the application token field above", + "复制成功": "Copy successful", + "复制所有代码": "Copy all codes", + "复制所有模型": "Copy all models", + "复制所选令牌": "Copy selected token", + "复制所选兑换码到剪贴板": "Copy selected redemption codes to clipboard", + "复制渠道的所有信息": "Copy all information for a channel", + "外接设备": "External device", + "多密钥渠道操作项目组": "Multi-key channel operation project group", + "多密钥管理": "Multi-key management", + "多种充值方式,安全便捷": "Multiple recharge methods, safe and convenient", + "天": "day", + "天前": "days ago", + "失败": "Failed", + "失败原因": "Failure reason", + "失败时自动禁用通道": "Automatically disable channel on failure", + "失败重试次数": "Failed retry times", + "奖励说明": "Reward description", + "如:大带宽批量分析图片推荐": "e.g. Large bandwidth batch analysis of image recommendations", + "如:香港线路": "e.g. Hong Kong line", + "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "If you are connecting to upstream One API or New API forwarding projects, please use OpenAI type. Do not use this type unless you know what you are doing.", + "如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "If the user request contains a system prompt, this setting will be appended to the user's system prompt", + "始终使用浅色主题": "Always use light theme", + "始终使用深色主题": "Always use dark theme", + "字段透传控制": "Field Pass-through Control", + "存在重复的键名:": "Duplicate key names exist:", + "安全提醒": "Security reminder", + "安全设置": "Security Settings", + "安全验证": "Security verification", + "安全验证级别": "Security Verification Level", + "安装指南": "Installation Guide", + "完成": "Complete", + "完成初始化": "Complete initialization", + "完成设置并启用两步验证": "Complete setup and enable two-factor authentication", + "完整的 Base URL,支持变量{model}": "Complete Base URL, supports variable {model}", + "官方": "Official", + "官方文档": "Official documentation", + "官方模型同步": "Official models sync", + "定价模式": "Pricing Mode", + "定时测试所有通道": "Periodically test all channels", + "定期更改密码可以提高账户安全性": "Regularly changing your password can improve account security", + "实付": "Actual payment", + "实付金额": "Actual payment amount", + "实付金额:": "Actual payment amount: ", + "实际模型": "Actual model", + "实际请求体": "Actual request body", + "密码": "Password", + "密码修改成功!": "Password changed successfully!", + "密码已复制到剪贴板:": "Password has been copied to clipboard: ", + "密码已重置并已复制到剪贴板:": "Password has been reset and copied to clipboard: ", + "密码管理": "Password Management", + "密码重置": "Password Reset", + "密码重置完成": "Password reset completed", + "密码重置确认": "Password Reset Confirmation", + "密码长度至少为8个字符": "Password must be at least 8 characters long", + "密钥": "Key", + "密钥(编辑模式下,保存的密钥不会显示)": "Key (in edit mode, saved keys will not be displayed)", + "密钥去重": "Key deduplication", + "密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性": "The key will be added to the request header as Bearer to verify the legitimacy of the webhook request", + "密钥已删除": "Key has been deleted", + "密钥已启用": "Key has been enabled", + "密钥已复制到剪贴板": "Key copied to clipboard", + "密钥已禁用": "Key has been disabled", + "密钥文件 (.json)": "Key file (.json)", + "密钥更新模式": "Key update mode", + "密钥格式": "Key format", + "密钥格式无效,请输入有效的 JSON 格式密钥": "Invalid key format, please enter a valid JSON format key", + "密钥聚合模式": "Key aggregation mode", + "密钥获取成功": "Key acquisition successful", + "密钥输入方式": "Key input method", + "密钥预览": "Key preview", + "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "For official channels, the new-api has a built-in address. Unless it is a third-party proxy site or a special Azure access address, there is no need to fill it in", + "对域名启用 IP 过滤(实验性)": "Enable IP filtering for domains (experimental)", + "对外运营模式": "Default mode", + "导入": "Import", + "导入的配置将覆盖当前设置,是否继续?": "The imported configuration will overwrite the current settings, continue?", + "导入配置": "Import configuration", + "导入配置失败: ": "Failed to import configuration: ", + "导出": "Export", + "导出配置": "Export configuration", + "导出配置失败: ": "Failed to export configuration: ", + "将 reasoning_content 转换为 标签拼接到内容中": "Convert reasoning_content to tags and append to content", + "将为选中的 ": "Will set for selected ", + "将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "Only the first key file will be retained, and the remaining files will be removed. Continue?", + "将删除": "Deleting", + "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "This will delete all used, disabled, and expired redemption codes, this operation cannot be undone.", + "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "This will clear all saved configurations and restore default settings, this operation cannot be undone. Continue?", + "将清除选定时间之前的所有日志": "This will clear all logs before the selected time", + "小时": "Hour", + "尚未使用": "Not used yet", + "局部重绘-提交": "Vary Region", + "屏蔽词列表": "Sensitive word list", + "屏蔽词过滤设置": "Sensitive word filtering settings", + "展开": "Expand", + "展开更多": "Expand more", + "左侧边栏个人设置": "Personal settings in left sidebar", + "已为 {{count}} 个模型设置{{type}}_one": "Set {{type}} for {{count}} model", + "已为 {{count}} 个模型设置{{type}}_other": "Set {{type}} for {{count}} models", + "已为 ${count} 个渠道设置标签!": "Set tags for ${count} channels!", + "已修复 ${success} 个通道,失败 ${fails} 个通道。": "Fixed ${success} channels, failed ${fails} channels.", + "已停止批量测试": "Stopped batch testing", + "已关闭后续提醒": "Subsequent notifications turned off", + "已切换为Assistant角色": "Switched to Assistant role", + "已切换为System角色": "Switched to System role", + "已切换至最优倍率视图,每个模型使用其最低倍率分组": "Switched to the optimal ratio view, each model uses its lowest ratio group", + "已初始化": "Initialized", + "已删除 {{count}} 个令牌!": "Deleted {{count}} tokens!", + "已删除 {{count}} 条失效兑换码_one": "Deleted {{count}} expired redemption code", + "已删除 {{count}} 条失效兑换码_other": "Deleted {{count}} expired redemption codes", + "已删除 ${data} 个通道!": "Deleted ${data} channels!", + "已删除所有禁用渠道,共计 ${data} 个": "Deleted all disabled channels, total ${data}", + "已删除消息及其回复": "Deleted message and its replies", + "已发送到 Fluent": "Sent to Fluent", + "已取消 Passkey 注册": "Passkey registration cancelled", + "已启用": "Enabled", + "已启用 Passkey,无需密码即可登录": "Passkey enabled, login without password", + "已启用所有密钥": "All keys have been enabled", + "已备份": "Backed up", + "已复制": "Copied", + "已复制 ${count} 个模型": "Copied ${count} models", + "已复制:": "Copied:", + "已复制:{{name}}": "Copied: {{name}}", + "已复制到剪切板": "Copied to clipboard", + "已复制到剪贴板": "Copied to clipboard", + "已复制到剪贴板!": "Copied to clipboard!", + "已复制模型名称": "Model name copied", + "已成功开始测试所有已启用通道,请刷新页面查看结果。": "Successfully started testing all enabled channels. Please refresh page to view results.", + "已提交": "Submitted", + "已新增 {{count}} 个模型:{{list}}_one": "Added {{count}} model: {{list}}", + "已新增 {{count}} 个模型:{{list}}_other": "Added {{count}} models: {{list}}", + "已更新完毕所有已启用通道余额!": "Updated quota for all enabled channels!", + "已有保存的配置": "There are saved configurations", + "已有的模型": "Existing models", + "已有账户?": "Already have an account?", + "已注销": "Logged out", + "已添加到白名单": "Added to whitelist", + "已清空测试结果": "Cleared test results", + "已用/剩余": "Used/Remaining", + "已用额度": "Quota used", + "已禁用": "Disabled", + "已禁用所有密钥": "Disabled all keys", + "已绑定": "Bound", + "已绑定渠道": "Bound channels", + "已耗尽": "Exhausted", + "已过期": "Expired", + "已选择 {{count}} 个模型_one": "Selected {{count}} model", + "已选择 {{count}} 个模型_other": "Selected {{count}} models", + "已选择 {{selected}} / {{total}}": "Selected {{selected}} / {{total}}", + "已选择 ${count} 个渠道": "Selected ${count} channels", + "已重置为默认配置": "Reset to default configuration", + "常见问答": "FAQ", + "常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)": "FAQ management, providing answers to common questions for users (maximum 50, display latest 20 on the front end)", + "平台": "platform", + "平均RPM": "Average RPM", + "平均TPM": "Average TPM", + "平移": "Pan", + "应用同步": "Apply synchronization", + "应用更改": "Apply changes", + "应用覆盖": "Apply overwrite", + "建立连接时发生错误": "Error occurred while establishing connection", + "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "It is recommended to use MySQL or PostgreSQL databases in production environments, or ensure that the SQLite database file is mapped to the persistent storage of the host machine.", + "开": "open", + "开启之后会清除用户提示词中的": "After enabling, the user prompt will be cleared", + "开启之后将上游地址替换为服务器地址": "After enabling, the upstream address will be replaced with the server address", + "开启后,仅\"消费\"和\"错误\"日志将记录您的客户端IP地址": "After enabling, only \"consumption\" and \"error\" logs will record your client IP address", + "开启后,将定期发送ping数据保持连接活跃": "After enabling, ping data will be sent periodically to keep the connection active", + "开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启": "When enabled, all requests will be directly forwarded to the upstream without any processing (redirects and channel adaptation will also be disabled). Please enable with caution.", + "开启后不限制:必须设置模型倍率": "After enabling, no limit: must set model ratio", + "开启后未登录用户无法访问模型广场": "When enabled, unauthenticated users cannot access the model marketplace", + "开启批量操作": "Enable batch selection", + "开始同步": "Start sync", + "开始批量测试 ${count} 个模型,已清空上次结果...": "Starting batch test of ${count} models, cleared previous results...", + "开始时间": "start time", + "弱变换": "High Variation", + "强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)": "Force format responses to OpenAI standard format (Only for OpenAI channel types)", + "强制格式化": "Force format", + "强制要求": "Mandatory requirement", + "强变换": "Low Variation", + "当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道": "When the upstream channel returns an error containing these keywords (not case-sensitive), automatically disable the channel", + "当前余额": "Current balance", + "当前值": "Current value", + "当前分组为 auto,会自动选择最优分组,当一个组不可用时自动降级到下一个组(熔断机制)": "The current group is auto, it will automatically select the optimal group, and automatically downgrade to the next group when a group is unavailable (breakage mechanism)", + "当前时间": "Current time", + "当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。": "Current Midjourney callback is not enabled, some projects may not be able to obtain drawing results, which can be enabled in the operation settings.", + "当前查看的分组为:{{group}},倍率为:{{ratio}}": "Current group: {{group}}, ratio: {{ratio}}", + "当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。": "The current model list is the longest one among all channel model lists under this tag, not the union of all channels. Please note that this may cause some channel models to be lost.", + "当前版本": "Current version", + "当前计费": "Current billing", + "当前设备不支持 Passkey": "Passkey is not supported on this device", + "当前设置类型: ": "Current setting type: ", + "当前跟随系统": "Currently following system", + "当剩余额度低于此数值时,系统将通过选择的方式发送通知": "When the remaining quota is lower than this value, the system will send a notification through the selected method", + "当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用": "Accept calls even if the model has no price settings, use only when you trust the website, which may incur high costs", + "当运行通道全部测试时,超过此时间将自动禁用通道": "When running all channel tests, the channel will be automatically disabled when this time is exceeded", + "待使用收益": "Proceeds to be used", + "微信": "WeChat", + "微信公众号二维码图片链接": "WeChat Public Account QR Code Image Link", + "微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "Scan WeChat QR code to follow official account, enter \"verification code\" to get code (valid for 3 minutes)", + "微信扫码登录": "WeChat scan code to log in", + "微信账户绑定成功!": "WeChat account bound successfully!", + "必须是有效的 JSON 字符串数组,例如:[\"g1\",\"g2\"]": "Must be a valid JSON string array, for example: [\"g1\",\"g2\"]", + "忘记密码?": "Forgot password?", + "快速开始": "Quick Start", + "思考中...": "Thinking...", + "思考内容转换": "Thinking content conversion", + "思考过程": "Thinking process", + "思考适配 BudgetTokens 百分比": "Thinking adaptation BudgetTokens percentage", + "思考预算占比": "Thinking budget ratio", + "性能指标": "Performance Indicators", + "总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}": "Total price: text price {{textPrice}} + audio price {{audioPrice}} = {{symbol}}{{total}}", + "总密钥数": "Total key count", + "总收益": "total revenue", + "总计": "Total", + "总额度": "Total quota", + "您可以个性化设置侧边栏的要显示功能": "You can customize the sidebar functions to display", + "您无权访问此页面,请联系管理员": "You do not have permission to access this page. Please contact the administrator.", + "您正在使用 MySQL 数据库。MySQL 是一个可靠的关系型数据库管理系统,适合生产环境使用。": "You are using the MySQL database. MySQL is a reliable relational database management system, suitable for production environments.", + "您正在使用 PostgreSQL 数据库。PostgreSQL 是一个功能强大的开源关系型数据库系统,提供了出色的可靠性和数据完整性,适合生产环境使用。": "You are using the PostgreSQL database. PostgreSQL is a powerful open-source relational database system that provides excellent reliability and data integrity, suitable for production environments.", + "您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!": "You are using the SQLite database. If you are running in a container environment, please ensure that the database file persistence mapping is correctly set, otherwise all data will be lost after container restart!", + "您正在删除自己的帐户,将清空所有数据且不可恢复": "You are deleting your account. All data will be cleared and cannot be recovered.", + "您的数据将安全地存储在本地计算机上。所有配置、用户信息和使用记录都会自动保存,关闭应用后不会丢失。": "Your data will be securely stored on your local computer. All configurations, user information, and usage records will be automatically saved and will not be lost when the application is closed.", + "您确定要取消密码登录功能吗?这可能会影响用户的登录方式。": "Are you sure you want to disable the password login feature? This may affect users' login methods.", + "您需要先启用两步验证或 Passkey 才能执行此操作": "You need to enable two-factor authentication or Passkey before you can perform this operation", + "您需要先启用两步验证或 Passkey 才能查看敏感信息。": "You need to enable two-factor authentication or Passkey before you can view sensitive information.", + "想起来了?": "Remember?", + "成功": "Success", + "成功兑换额度:": "Successful redemption amount:", + "成功时自动启用通道": "Enable channel when successful", + "我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "I have understood that disabling two-factor authentication will permanently delete all related settings and backup codes, this operation cannot be undone", + "我已阅读并同意": "I have read and agree to", + "或": "or", + "或其兼容new-api-worker格式的其他版本": "or other versions compatible with new-api-worker format", + "或手动输入密钥:": "Or manually enter the secret:", + "所有上游数据均可信": "All upstream data is reliable", + "所有密钥已复制到剪贴板": "All keys have been copied to the clipboard", + "所有编辑均为覆盖操作,留空则不更改": "All edits are overwrite operations, leaving blank will not change", + "手动禁用": "Manually disabled", + "手动编辑": "Manual editing", + "手动输入": "Manual input", + "打开侧边栏": "Open sidebar", + "执行中": "processing", + "扫描二维码": "Scan QR code", + "批量创建": "Batch Create", + "批量创建时会在名称后自动添加随机后缀": "When creating in batches, a random suffix will be automatically added to the name", + "批量创建模式下仅支持文件上传,不支持手动输入": "Batch creation mode only supports file upload, manual input is not supported", + "批量删除": "Batch Delete", + "批量删除令牌": "Batch delete token", + "批量删除失败": "Batch deletion failed", + "批量删除模型": "Batch delete models", + "批量操作": "Batch Operations", + "批量测试${count}个模型": "Batch test ${count} models", + "批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}": "Batch testing completed! Success: ${success}, Fail: ${fail}, Total: ${total}", + "批量测试已停止": "Batch testing stopped", + "批量测试过程中发生错误: ": "An error occurred during batch testing: ", + "批量设置": "Batch Setting", + "批量设置成功": "Batch setting successful", + "批量设置标签": "Batch set tag", + "批量设置模型参数": "Batch Set Model Parameters", + "折": "% off", + "按K显示单位": "Display in K", + "按价格设置": "Set by price", + "按倍率类型筛选": "Filter by ratio type", + "按倍率设置": "Set by ratio", + "按次计费": "Pay per view", + "按量计费": "Pay as you go", + "按顺序替换content中的变量占位符": "Replace variable placeholders in content in order", + "换脸": "Face swap", + "授权,需在遵守": " and must be used in compliance with the ", + "授权失败": "Authorization failed", + "排队中": "Queuing", + "接受未设置价格模型": "Accept models without price settings", + "接口凭证": "Interface credentials", + "控制台": "Console", + "控制台区域": "Console Area", + "控制顶栏模块显示状态,全局生效": "Control header module display status, global effect", + "推荐:用户可以选择是否使用指纹等验证": "Recommended: Users can choose whether to use fingerprint verification", + "推荐使用(用户可选)": "Recommended (user optional)", + "描述": "Description", + "提交": "Submit", + "提交时间": "Submission time", + "提交结果": "Results", + "提升": "Promote", + "提示": "Prompt", + "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Prompt {{input}} tokens / 1M tokens * {{symbol}}{{price}} + Completion {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Prompt {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + Cache {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + Cache creation {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + Completion {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "提示:如需备份数据,只需复制上述目录即可": "Tip: To back up data, simply copy the directory above", + "提示:链接中的{key}将被替换为API密钥,{address}将被替换为服务器地址": "Tip: {key} in the link will be replaced with the API key, {address} will be replaced with the server address", + "提示价格:{{symbol}}{{price}} / 1M tokens": "Prompt price: {{symbol}}{{price}} / 1M tokens", + "提示缓存倍率": "Prompt cache ratio", + "搜索供应商": "Search vendor", + "搜索关键字": "Search keywords", + "搜索无结果": "No results found", + "搜索条件": "Search Conditions", + "搜索模型": "Search models", + "搜索模型...": "Search models...", + "搜索模型名称": "Search model name", + "搜索模型失败": "Search model failed", + "搜索渠道名称或地址": "Search channel name or address", + "搜索聊天应用名称": "Search chat app name", + "操作": "Actions", + "操作失败": "Operation failed", + "操作失败,请重试": "Operation failed, please retry", + "操作成功完成!": "Operation completed successfully!", + "操作暂时被禁用": "Operation temporarily disabled", + "操练场": "Playground", + "操练场和聊天功能": "Playground and chat functions", + "支付地址": "Payment address", + "支付宝": "Alipay", + "支付方式": "Payment method", + "支付设置": "Payment Settings", + "支付请求失败": "Payment request failed", + "支付金额": "Payment Amount", + "支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。": "Supports 6-digit TOTP verification code or 8-digit backup code, can be configured or viewed in `Personal Settings - Security Settings - Two-Factor Authentication Settings`.", + "支持CIDR格式,如:8.8.8.8, 192.168.1.0/24": "Supports CIDR format, e.g.: 8.8.8.8, 192.168.1.0/24", + "支持HTTP和HTTPS,填写Gotify服务器的完整URL地址": "Supports HTTP and HTTPS, enter the complete URL of the Gotify server", + "支持HTTP和HTTPS,模板变量: {{title}} (通知标题), {{content}} (通知内容)": "Supports HTTP and HTTPS, template variables: {{title}} (notification title), {{content}} (notification content)", + "支持众多的大模型供应商": "Supporting various LLM providers", + "支持单个端口和端口范围,如:80, 443, 8000-8999": "Supports single ports and port ranges, e.g.: 80, 443, 8000-8999", + "支持变量:": "Supported variables:", + "支持备份": "Supported", + "支持搜索用户的 ID、用户名、显示名称和邮箱地址": "Support searching for user ID, username, display name, and email address", + "支持的图像模型": "Supported image models", + "支持通配符格式,如:example.com, *.api.example.com": "Supports wildcard format, e.g.: example.com, *.api.example.com", + "收益": "Earnings", + "收益统计": "Income statistics", + "收起": "Collapse", + "收起侧边栏": "Collapse sidebar", + "收起内容": "Collapse content", + "放大": "Upscalers", + "放大编辑": "Expand editor", + "敏感信息不会发送到前端显示": "Sensitive information will not be displayed in the frontend", + "数据存储位置:": "Data storage location:", + "数据库信息": "Database Information", + "数据库检查": "Database Check", + "数据库类型": "Database Type", + "数据库警告": "Database warning", + "数据格式错误": "Data format error", + "数据看板": "Dashboard", + "数据看板更新间隔": "Data dashboard update interval", + "数据看板设置": "Data dashboard settings", + "数据看板默认时间粒度": "Data dashboard default time granularity", + "数据管理和日志查看": "Data management and log viewing", + "文件上传": "File upload", + "文件搜索价格:{{symbol}}{{price}} / 1K 次": "File search price: {{symbol}}{{price}} / 1K times", + "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "Text prompt {{input}} tokens / 1M tokens * {{symbol}}{{price}} + Text completion {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", + "文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "Text prompt {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + Cache {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + Text completion {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", + "文字输入": "Text input", + "文字输出": "text output", + "文心一言": "ERNIE Bot", + "文档": "Documentation", + "文档地址": "Document Link", + "文生视频": "Text-to-video", + "新增供应商": "Add vendor", + "新密码": "New Password", + "新密码需要和原密码不一致!": "New password must be different from the old password!", + "新建": "Create", + "新建数量": "New quantity", + "新建组": "New group", + "新格式(支持条件判断与json自定义):": "New format (supports conditional judgment and JSON customization):", + "新格式模板": "New format template", + "新版本": "New Version", + "新用户使用邀请码奖励额度": "New user invitation code bonus quota", + "新用户初始额度": "Initial quota for new users", + "新的备用恢复代码": "New backup recovery code", + "新的备用码已生成": "New backup code has been generated", + "新获取的模型": "New models", + "新额度:": "New quota: ", + "无": "None", + "无冲突项": "No conflict items", + "无效的重置链接,请重新发起密码重置请求": "Invalid reset link, please initiate a new password reset request", + "无法发起 Passkey 注册": "Unable to initiate Passkey registration", + "无法复制到剪贴板,请手动复制": "Unable to copy to clipboard, please copy manually", + "无邀请人": "No Inviter", + "无限制": "Unlimited", + "无限额度": "Unlimited quota", + "日志清理失败:": "Log cleanup failed:", + "日志类型": "Log type", + "日志设置": "Log settings", + "日志详情": "Log details", + "旧格式(直接覆盖):": "Old format (direct override):", + "旧格式模板": "Old format template", + "旧的备用码已失效,请保存新的备用码": "Old backup codes have been invalidated, please save the new backup codes", + "早上好": "Good morning", + "时间": "Time", + "时间粒度": "Time granularity", + "易支付商户ID": "Epay merchant ID", + "易支付商户密钥": "Epay merchant key", + "是": "Yes", + "是否为企业账户": "Is it an enterprise account?", + "是否同时重置对话消息?选择\"是\"将清空所有对话记录并恢复默认示例;选择\"否\"将保留当前对话记录。": "Reset conversation messages at the same time? Selecting \"Yes\" will clear all conversation records and restore default examples; selecting \"No\" will retain current conversation records.", + "是否将该订单标记为成功并为用户入账?": "Mark this order as successful and credit the user?", + "是否自动禁用": "Whether to automatically disable", + "是否要求指纹/面容等生物识别": "Whether to require fingerprint/face recognition", + "显示倍率": "Show ratio", + "显示最新20条": "Display latest 20", + "显示名称": "Display Name", + "显示完整内容": "Show full content", + "显示操作项": "Show actions", + "显示更多": "Show more", + "显示第": "Showing", + "显示设置": "Display settings", + "显示调试": "Show debug", + "晚上好": "Good evening", + "普通用户": "Normal User", + "智能体ID": "Agent ID", + "智能熔断": "Smart fallback", + "智谱": "Zhipu AI", + "暂无API信息": "No API information", + "暂无保存的配置": "No saved configuration", + "暂无充值记录": "No recharge records", + "暂无公告": "No Notice", + "暂无匹配模型": "No matching model", + "暂无可用的支付方式,请联系管理员配置": "No payment methods available, please contact administrator for configuration", + "暂无响应数据": "No response data", + "暂无密钥数据": "No key data", + "暂无差异化倍率显示": "No differential ratio display", + "暂无常见问答": "No FAQ", + "暂无成功模型": "No successful models", + "暂无数据": "No data", + "暂无数据,点击下方按钮添加键值对": "No data, click the button below to add key-value pairs", + "暂无模型描述": "No model description", + "暂无监控数据": "No monitoring data", + "暂无系统公告": "No system notice", + "暂无缺失模型": "No missing models", + "暂无请求数据": "No request data", + "暂无项目": "No projects", + "暂无预填组": "No prefilled groups", + "暴露倍率接口": "Expose ratio API", + "更多": "Expand more", + "更多信息请参考": "For more information, please refer to", + "更多参数请参考": "For more parameters, please refer to", + "更好的价格,更好的稳定性,只需要将模型基址替换为:": "Better price, better stability, no subscription required, just replace the model BASE URL with: ", + "更新": "Update", + "更新 Stripe 设置": "Update Stripe settings", + "更新SSRF防护设置": "Update SSRF Protection Settings", + "更新Worker设置": "Update Worker Settings", + "更新令牌信息": "Update Token Information", + "更新兑换码信息": "Update redemption code information", + "更新失败": "Update failed", + "更新成功": "Update successful", + "更新所有已启用通道余额": "Update balance for all enabled channels", + "更新支付设置": "Update payment settings", + "更新时间": "Update time", + "更新服务器地址": "Update Server Address", + "更新模型信息": "Update model information", + "更新渠道信息": "Update Channel Information", + "更新预填组": "Update pre-filled group", + "服务可用性": "Service Status", + "服务器地址": "Server Address", + "服务显示名称": "Service Display Name", + "未发现新增模型": "No new models were added", + "未发现重复密钥": "No duplicate keys found", + "未启动": "No start", + "未启用": "Not Enabled", + "未命名": "Unnamed", + "未备份": "Not backed up", + "未开始": "Not Started", + "未找到匹配的模型": "No matching model found", + "未找到差异化倍率,无需同步": "No differential ratio found, no synchronization is required", + "未提交": "Not submitted", + "未检测到 Fluent 容器": "Fluent container not detected", + "未检测到 FluentRead(流畅阅读),请确认扩展已启用": "FluentRead (smooth reading) not detected, please confirm the extension is enabled", + "未测试": "Not tested", + "未登录或登录已过期,请重新登录": "Not logged in or login has expired, please log in again", + "未知": "unknown", + "未知供应商": "Unknown", + "未知模型": "Unknown model", + "未知渠道": "Unknown channel", + "未知状态": "Unknown status", + "未知类型": "Unknown type", + "未知身份": "Unknown Identity", + "未绑定": "Not bound", + "未获取到授权码": "Authorization code not obtained", + "未设置": "Not set", + "未设置倍率模型": "Models without ratio settings", + "未配置模型": "No model configured", + "未配置的模型列表": "Models not configured", + "本地": "Local", + "本地数据存储": "Local data storage", + "本设备:手机指纹/面容,外接:USB安全密钥": "Built-in: phone fingerprint/face, External: USB security key", + "本设备内置": "Built-in device", + "本项目根据": "This project is licensed under the ", + "权重": "Weight", + "权限设置": "Permission Settings", + "条": "items", + "条 - 第": "to", + "条,共": "of", + "条日志已清理!": "logs have been cleared!", + "查看": "Check", + "查看图片": "View pictures", + "查看密钥": "View key", + "查看当前可用的所有模型": "View all available models", + "查看所有可用的AI模型供应商,包括众多知名供应商的模型。": "View all available AI model suppliers, including models from many well-known suppliers.", + "查看渠道密钥": "View channel key", + "查询": "Query", + "标签": "Label", + "标签不能为空!": "Label cannot be empty!", + "标签信息": "Tag Information", + "标签名称": "Tag Name", + "标签的基本配置": "Tag basic configuration", + "标签组": "Tag group", + "标签聚合": "Tag aggregation", + "标签聚合模式": "Enable tag mode", + "标识颜色": "Identifier color", + "根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含": "Find model metadata based on model name and matching rules, priority: exact > prefix > suffix > contains", + "格式示例:": "Format example:", + "检查更新": "Check for updates", + "检测到 FluentRead(流畅阅读)": "FluentRead (smooth reading) detected", + "检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。": "Detected multiple keys, you can copy each key individually or click Copy All to get the complete content.", + "检测到该消息后有AI回复,是否删除后续回复并重新生成?": "AI reply detected after this message, delete subsequent replies and regenerate?", + "检测必须等待绘图成功才能进行放大等操作": "Detection must wait for drawing to succeed before performing zooming and other operations", + "模型": "Model", + "模型: {{ratio}}": "Model: {{ratio}}", + "模型专用区域": "Model-specific area", + "模型价格": "Model price", + "模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}": "Model price {{symbol}}{{price}}, {{ratioType}} {{ratio}}", + "模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "Model price: {{symbol}}{{price}} * {{ratioType}}: {{ratio}} = {{symbol}}{{total}}", + "模型倍率": "Model ratio", + "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}": "Model ratio {{modelRatio}}, cache ratio {{cacheRatio}}, completion ratio {{completionRatio}}, {{ratioType}} {{ratio}}", + "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次": "Model ratio {{modelRatio}}, cache ratio {{cacheRatio}}, completion ratio {{completionRatio}}, {{ratioType}} {{ratio}}, Web search called {{webSearchCallCount}} times", + "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}": "Model ratio {{modelRatio}}, cache ratio {{cacheRatio}}, completion ratio {{completionRatio}}, image input ratio {{imageRatio}}, {{ratioType}} {{ratio}}", + "模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}": "Model ratio {{modelRatio}}, completion ratio {{completionRatio}}, cache ratio {{cacheRatio}}, cache creation ratio {{cacheCreationRatio}}, {{ratioType}} {{ratio}}", + "模型倍率值": "Model Ratio Value", + "模型倍率和补全倍率": "Model Ratio and Completion Ratio", + "模型倍率和补全倍率同时设置": "Both model ratio and completion ratio are set", + "模型倍率设置": "Model ratio settings", + "模型关键字": "model keyword", + "模型列表已复制到剪贴板": "Model list copied to clipboard", + "模型列表已更新": "Model list updated", + "模型创建成功!": "Model created successfully!", + "模型名称": "Model Name", + "模型名称已存在": "Model name already exists", + "模型固定价格": "Model price per call", + "模型图标": "Model icon", + "模型定价,需要登录访问": "Model pricing, requires login to access", + "模型广场": "Model Marketplace", + "模型支持的接口端点信息": "Model supported API endpoint information", + "模型数据分析": "Model Data Analysis", + "模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!", + "模型更新成功!": "Model updated successfully!", + "模型消耗分布": "Model consumption distribution", + "模型消耗趋势": "Model consumption trend", + "模型版本": "Model version", + "模型的详细描述和基本特性": "Detailed description and basic characteristics of the model", + "模型相关设置": "Model related settings", + "模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:": "The model community needs everyone's contribution. If you find incorrect data or want to contribute new models, please visit:", + "模型管理": "Model Management", + "模型组": "Model group", + "模型补全倍率(仅对自定义模型有效)": "Model completion ratio (only effective for custom models)", + "模型请求速率限制": "Model request rate limit", + "模型调用次数占比": "Model call ratio", + "模型调用次数排行": "Model call ranking", + "模型选择和映射设置": "Model selection and mapping settings", + "模型配置": "Model Configuration", + "模型重定向": "Model mapping", + "模型限制列表": "Model restrictions list", + "模板示例": "Template example", + "模糊搜索模型名称": "Fuzzy search model name", + "次": "times", + "欢迎使用,请完成以下设置以开始使用系统": "Welcome! Please complete the following settings to start using the system", + "正在处理大内容...": "Processing large content...", + "正在提交": "Submitting", + "正在构造请求体预览...": "Constructing request body preview...", + "正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)": "Testing model ${current} - ${end} (total ${total})", + "正在跳转...": "Redirecting...", + "此代理仅用于图片请求转发,Webhook通知发送等,AI API请求仍然由服务器直接发出,可在渠道设置中单独配置代理": "This proxy is only used for image request forwarding, webhook notification sending, etc. AI API requests are still sent directly by the server, and proxy can be configured separately in channel settings", + "此修改将不可逆": "This modification will be irreversible", + "此操作不可恢复,请仔细确认时间后再操作!": "This operation cannot be recovered, please confirm the time carefully before proceeding!", + "此操作不可撤销,将永久删除已自动禁用的密钥": "This operation cannot be undone, and all automatically disabled keys will be permanently deleted.", + "此操作不可撤销,将永久删除该密钥": "This operation cannot be undone, and the key will be permanently deleted.", + "此操作不可逆,所有数据将被永久删除": "This operation is irreversible, all data will be permanently deleted", + "此操作将启用用户账户": "This operation will enable the user account", + "此操作将提升用户的权限级别": "This operation will elevate the user's permission level", + "此操作将禁用用户账户": "This operation will disable the user account", + "此操作将禁用该用户当前的两步验证配置,下次登录将不再强制输入验证码,直到用户重新启用。": "This will disable the user's current two-factor setup. No verification code will be required until they enable it again.", + "此操作将解绑用户当前的 Passkey,下次登录需要重新注册。": "This will detach the user's current Passkey. They will need to register again on next login.", + "此操作将降低用户的权限级别": "This operation will reduce the user's permission level", + "此支付方式最低充值金额为": "Minimum recharge amount for this payment method is", + "此设置用于系统内部计算,默认值500000是为了精确到6位小数点设计,不推荐修改。": "This setting is used for internal system calculations. The default value of 500000 is designed for 6 decimal places precision, modification is not recommended.", + "此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除": "This page only shows models without price or ratio settings. After setting, they will be automatically removed from the list", + "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "Read-only, user's personal settings, and cannot be modified directly", + "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:": "This is optional, used to modify the model name in the request body, it's a JSON string, the key is the model name in the request, and the value is the model name to be replaced, for example:", + "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改": "This is optional, used to modify the model name in the request body, as a JSON string, the key is the model name in the request, the value is the model name to be replaced, leaving blank will not change", + "此项可选,用于复写返回的状态码,仅影响本地判断,不修改返回到上游的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:": "Optional, used to override returned status codes, only affects local judgment, does not modify status code returned upstream, e.g. rewriting Claude channel's 400 error to 500 (for retry). Do not abuse this feature. Example:", + "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数": "This is optional, used to override request parameters. Overriding stream parameter is not supported.", + "此项可选,用于覆盖请求头参数": "This is optional, used to override request header parameters.", + "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "Optional for API calls through custom API address, do not add /v1 and / at the end", + "每隔多少分钟测试一次所有通道": "How many minutes between testing all channels", + "永不过期": "Never expires", + "永久删除您的两步验证设置": "Permanently delete your two-factor authentication settings", + "永久删除所有备用码(包括未使用的)": "Permanently delete all backup codes (including unused ones)", + "没有可用令牌用于填充": "No available tokens for filling", + "没有可用模型": "No available models", + "没有找到匹配的模型": "No matching model found", + "没有未设置的模型": "No unconfigured models", + "没有模型可以复制": "No models to copy", + "没有账户?": "No account? ", + "注 册": "Sign Up", + "注册": "Sign up", + "注册 Passkey": "Register Passkey", + "注意": "Note", + "注意:JSON中重复的键只会保留最后一个同名键的值": "Note: In JSON, duplicate keys will only keep the value of the last key with the same name", + "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "Note: For non-Chat API, please make sure to enter the correct API address, otherwise it may not work", + "注销": "Logout", + "注销成功!": "Logout successful!", + "流": "stream", + "浅色": "Light", + "浅色模式": "Light Mode", + "测试": "Test", + "测试中": "Testing", + "测试中...": "Testing...", + "测试单个渠道操作项目组": "Test a single channel operation project group", + "测试失败": "Test failed", + "测试所有渠道的最长响应时间": "Maximum response time for testing all channels", + "测试所有通道": "Test all channels", + "测速": "Speed Test", + "消息优先级": "Message priority", + "消息优先级,范围0-10,默认为5": "Message priority, range 0-10, default is 5", + "消息已删除": "Message deleted", + "消息已复制到剪贴板": "Message copied to clipboard", + "消息已更新": "Message updated", + "消息已编辑": "Message edited", + "消耗分布": "Consumption distribution", + "消耗趋势": "Consumption trend", + "消耗额度": "Used Quota", + "消费": "Consume", + "深色": "Dark", + "深色模式": "Dark Mode", + "添加": "Add", + "添加API": "Add API", + "添加令牌": "Create token", + "添加兑换码": "Add redemption code", + "添加公告": "Add Notice", + "添加分类": "Add Category", + "添加成功": "Added successfully", + "添加模型": "Add model", + "添加模型区域": "Add model region", + "添加渠道": "Add channel", + "添加用户": "Add user", + "添加聊天配置": "Add chat configuration", + "添加键值对": "Add key-value pair", + "添加问答": "Add FAQ", + "添加额度": "Add quota", + "清空重定向": "Clear redirect", + "清除历史日志": "Clear historical logs", + "清除失效兑换码": "Clear invalid redemption codes", + "清除所有模型": "Clear all models", + "渠道": "Channel", + "渠道 ID": "Channel ID", + "渠道ID,名称,密钥,API地址": "Channel ID, name, key, Base URL", + "渠道优先级": "Channel Priority", + "渠道信息": "Channel information", + "渠道创建成功!": "Channel created successfully!", + "渠道复制失败": "Channel copy failed", + "渠道复制失败: ": "Channel copy failed:", + "渠道复制成功": "Channel copy successful", + "渠道密钥": "Channel key", + "渠道密钥信息": "Channel key information", + "渠道密钥列表": "Channel key list", + "渠道更新成功!": "Channel updated successfully!", + "渠道权重": "Channel Weight", + "渠道标签": "Channel Tag", + "渠道模型信息不完整": "Channel model information is incomplete", + "渠道的基本配置信息": "Channel basic configuration information", + "渠道的模型测试": "Channel Model Test", + "渠道的高级配置选项": "Advanced channel configuration options", + "渠道管理": "Channel Management", + "渠道额外设置": "Channel extra settings", + "源地址": "Source address", + "演示站点": "Demo Site", + "演示站点模式": "Demo site mode", + "点击上传文件或拖拽文件到这里": "Click to upload file or drag and drop file here", + "点击下方按钮通过 Telegram 完成绑定": "Click the button below to complete binding via Telegram", + "点击复制模型名称": "Click to copy model name", + "点击查看差异": "Click to view differences", + "点击此处": "click here", + "点击预览视频": "Click to preview video", + "点击验证按钮,使用您的生物特征或安全密钥": "Click the verification button and use your biometrics or security key", + "版权所有": "All rights reserved", + "状态": "Status", + "状态码复写": "Status Code Override", + "状态筛选": "Status filter", + "状态页面Slug": "Status Page Slug", + "生成令牌": "Generate Token", + "生成数量": "Generate quantity", + "生成数量必须大于0": "Generation quantity must be greater than 0", + "生成新的备用码": "Generate new backup codes", + "生成歌词": "Generate lyrics", + "生成音乐": "generate music", + "用于API调用的身份验证令牌,请妥善保管": "Authentication token for API calls, please keep it safe", + "用于配置网络代理,支持 socks5 协议": "Used to configure network proxy, supports socks5 protocol", + "用以支持基于 WebAuthn 的无密码登录注册": "Support WebAuthn-based passwordless login and registration", + "用以支持用户校验": "To support user verification", + "用以支持系统的邮件发送": "To support the system email sending", + "用以支持通过 GitHub 进行登录注册": "To support login & registration via GitHub", + "用以支持通过 Linux DO 进行登录注册": "To support login & registration via Linux DO", + "用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP": "To support login via OIDC, such as Okta, Auth0 and other IdPs compatible with OIDC protocol", + "用以支持通过 Telegram 进行登录注册": "To support login & registration via Telegram", + "用以支持通过微信进行登录注册": "To support login & registration via WeChat", + "用以防止恶意用户利用临时邮箱批量注册": "To prevent malicious users from bulk registration using temporary email addresses", + "用户": "User", + "用户个人功能": "User personal functions", + "用户主页,展示系统信息": "User homepage, displaying system information", + "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "User priority: If the user specifies a system prompt in the request, the user's setting will be used first", + "用户信息": "User information", + "用户信息更新成功!": "User information updated successfully!", + "用户分组": "Your default group", + "用户分组和额度管理": "User Group and Quota Management", + "用户分组配置": "User group configuration", + "用户协议": "User Agreement", + "用户协议已更新": "User agreement updated", + "用户协议更新失败": "User agreement update failed", + "用户可选分组": "User selectable groups", + "用户名": "Username", + "用户名或邮箱": "Username or email", + "用户名称": "User Name", + "用户控制面板,管理账户": "User control panel for account management", + "用户新建令牌时可选的分组,格式为 JSON 字符串,例如:{\"vip\": \"VIP 用户\", \"test\": \"测试\"},表示用户可以选择 vip 分组和 test 分组": "User selectable groups when creating tokens, in JSON string format, for example: {\"vip\": \"VIP User\", \"test\": \"Test\"}, indicating that users can choose vip group and test group", + "用户每周期最多请求完成次数": "User max successful request times per period", + "用户每周期最多请求次数": "User max request times per period", + "用户注册时看到的网站名称,比如'我的网站'": "Website name users see during registration, e.g. 'My Website'", + "用户的基本账户信息": "User basic account information", + "用户管理": "User Management", + "用户组": "User group", + "用户账户创建成功!": "User account created successfully!", + "用户账户管理": "User account management", + "用时/首字": "Time/first word", + "留空则使用账号绑定的邮箱": "If left blank, the email address bound to the account will be used", + "留空则使用默认端点;支持 {path, method}": "Leave blank to use the default endpoint; supports {path, method}", + "留空则默认使用服务器地址,注意不能携带http://或者https://": "If left blank, the server address will be used by default. Note that http:// or https:// should not be included", + "登 录": "Log In", + "登录": "Sign in", + "登录成功!": "Login successful!", + "登录过期,请重新登录!": "Login expired, please log in again!", + "白名单": "Whitelist", + "的前提下使用。": "for use under the following conditions:", + "监控设置": "Monitoring Settings", + "目标用户:{{username}}": "Target user: {{username}}", + "相关项目": "Related Projects", + "相当于删除用户,此修改将不可逆": "Equivalent to deleting the user, this modification is irreversible", + "矛盾": "Conflict", + "知识库 ID": "Knowledge Base ID", + "确定": "OK", + "确定?": "Sure?", + "确定删除此组?": "Confirm delete this group?", + "确定导入": "Confirm import", + "确定是否要修复数据库一致性?": "Are you sure you want to repair database consistency?", + "确定是否要删除所选通道?": "Are you sure you want to delete the selected channels?", + "确定是否要删除此令牌?": "Are you sure you want to delete this token?", + "确定是否要删除此兑换码?": "Are you sure you want to delete this redemption code?", + "确定是否要删除此模型?": "Are you sure you want to delete this model?", + "确定是否要删除此渠道?": "Are you sure you want to delete this channel?", + "确定是否要删除禁用通道?": "Are you sure you want to delete the disabled channel?", + "确定是否要复制此渠道?": "Are you sure you want to copy this channel?", + "确定是否要注销此用户?": "Are you sure you want to deactivate this user?", + "确定清除所有失效兑换码?": "Are you sure you want to clear all invalid redemption codes?", + "确定要修改所有子渠道优先级为 ": "Confirm to modify all sub-channel priorities to ", + "确定要修改所有子渠道权重为 ": "Confirm to modify all sub-channel weights to ", + "确定要删除供应商 \"{{name}}\" 吗?此操作不可撤销。": "Are you sure you want to delete supplier \"{{name}}\"? This operation is irreversible.", + "确定要删除所有已自动禁用的密钥吗?": "Are you sure you want to delete all automatically disabled keys?", + "确定要删除所选的 {{count}} 个令牌吗?_one": "Are you sure you want to delete the selected {{count}} token?", + "确定要删除所选的 {{count}} 个令牌吗?_other": "Are you sure you want to delete the selected {{count}} tokens?", + "确定要删除所选的 {{count}} 个模型吗?_one": "Are you sure you want to delete the selected {{count}} model?", + "确定要删除所选的 {{count}} 个模型吗?_other": "Are you sure you want to delete the selected {{count}} models?", + "确定要删除此API信息吗?": "Are you sure you want to delete this API information?", + "确定要删除此公告吗?": "Are you sure you want to delete this notice?", + "确定要删除此分类吗?": "Are you sure you want to delete this category?", + "确定要删除此密钥吗?": "Are you sure you want to delete this key?", + "确定要删除此问答吗?": "Are you sure you want to delete this FAQ?", + "确定要删除这条消息吗?": "Are you sure you want to delete this message?", + "确定要启用所有密钥吗?": "Are you sure you want to enable all keys?", + "确定要启用此用户吗?": "Are you sure you want to enable this user?", + "确定要提升此用户吗?": "Are you sure you want to promote this user?", + "确定要更新所有已启用通道余额吗?": "Are you sure you want to update the balance of all enabled channels?", + "确定要测试所有通道吗?": "Are you sure you want to test all channels?", + "确定要禁用所有的密钥吗?": "Are you sure you want to disable all keys?", + "确定要禁用此用户吗?": "Are you sure you want to disable this user?", + "确定要降级此用户吗?": "Are you sure you want to demote this user?", + "确定重置": "Confirm reset", + "确定重置模型倍率吗?": "Confirm to reset model ratio?", + "确认": "Confirm", + "确认冲突项修改": "Confirm conflict item modification", + "确认删除": "Confirm deletion", + "确认取消密码登录": "Confirm cancel password login", + "确认密码": "Confirm Password", + "确认导入配置": "Confirm import configuration", + "确认新密码": "Confirm new password", + "确认清除历史日志": "Confirm clear historical logs", + "确认禁用": "Confirm disable", + "确认补单": "Confirm Order Completion", + "确认解绑": "Confirm Unbind", + "确认解绑 Passkey": "Confirm Unbind Passkey", + "确认设置并完成初始化": "Confirm settings and complete initialization", + "确认重置 Passkey": "Confirm Passkey Reset", + "确认重置两步验证": "Confirm Two-Factor Reset", + "确认重置密码": "Confirm Password Reset", + "示例": "Example", + "示例:{\"default\": [200, 100], \"vip\": [0, 1000]}。": "Example: {\"default\": [200, 100], \"vip\": [0, 1000]}.", + "视频": "Video", + "禁用": "Disable", + "禁用 store 透传": "Disable store Pass-through", + "禁用2FA失败": "Failed to disable Two-Factor Authentication", + "禁用两步验证": "Disable two-factor authentication", + "禁用全部": "Disable all", + "禁用原因": "Disable reason", + "禁用后的影响:": "Impact after disabling:", + "禁用密钥失败": "Failed to disable key", + "禁用所有密钥失败": "Failed to disable all keys", + "禁用时间": "Disable time", + "私有IP访问详细说明": "⚠️ Security Warning: Enabling this allows access to internal network resources (localhost, private networks). Only enable if you need to access internal services and understand the security implications.", + "私有部署地址": "Private Deployment Address", + "秒": "Second", + "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "Removal of One API copyright mark must first be authorized. Project maintenance requires a lot of effort. If this project is meaningful to you, please actively support it.", + "窗口处理": "window handling", + "窗口等待": "window wait", + "站点额度展示类型及汇率": "Site quota display type and exchange rate", + "端口配置详细说明": "Restrict external requests to specific ports. Use single ports (80, 443) or ranges (8000-8999). Empty list allows all ports. Default includes common web ports.", + "端点": "Endpoint", + "端点映射": "Endpoint mapping", + "端点类型": "Endpoint type", + "端点组": "Endpoint group", + "第三方账户绑定状态(只读)": "Third-party account binding status (read-only)", + "等价金额:": "Equivalent Amount: ", + "等待中": "Waiting", + "等待获取邮箱信息...": "Waiting to get email information...", + "筛选": "Filter", + "管理": "Manage", + "管理你的 LinuxDO OAuth App": "Manage your LinuxDO OAuth App", + "管理员": "Admin", + "管理员区域": "Administrator Area", + "管理员暂时未设置任何关于内容": "The administrator has not set any custom About content yet", + "管理员未开启Stripe充值!": "Administrator has not enabled Stripe recharge!", + "管理员未开启在线充值!": "The administrator has not enabled online recharge!", + "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "The administrator has not enabled the online recharge function, please contact the administrator to enable it or recharge with a redemption code.", + "管理员未设置用户可选分组": "Administrator has not set user-selectable groups", + "管理员设置了外部链接,点击下方按钮访问": "Administrator has set up external links, click the button below to access", + "管理员账号": "Admin account", + "管理员账号已经初始化过,请继续设置其他参数": "The admin account has already been initialized, please continue to set other parameters", + "管理模型、标签、端点等预填组": "Manage model, tag, endpoint, etc. pre-filled groups", + "类型": "Type", + "精确": "Exact", + "系统": "System", + "系统令牌已复制到剪切板": "System token copied to clipboard", + "系统任务记录": "System task records", + "系统信息": "System Information", + "系统公告": "System Notice", + "系统公告管理,可以发布系统通知和重要消息(最多100个,前端显示最新20条)": "System notice management, you can publish system notices and important messages (maximum 100, display latest 20 on the front end)", + "系统初始化": "System initialization", + "系统初始化失败,请重试": "System initialization failed, please try again", + "系统初始化成功,正在跳转...": "System initialization successful, redirecting...", + "系统参数配置": "System parameter configuration", + "系统名称": "System Name", + "系统名称已更新": "System name updated", + "系统名称更新失败": "System name update failed", + "系统提示覆盖": "System prompt override", + "系统提示词": "System Prompt", + "系统提示词拼接": "System prompt append", + "系统数据统计": "System data statistics", + "系统文档和帮助信息": "System documentation and help information", + "系统消息": "System message", + "系统管理功能": "System management functions", + "系统设置": "System Settings", + "系统访问令牌": "System Access Token", + "约": "Approximately", + "索引": "Index", + "紧凑列表": "Compact list", + "线路描述": "Route description", + "组列表": "Group list", + "组名": "Group name", + "组织": "Organization", + "组织,不填则为默认组织": "Organization, default if empty", + "绑定": "Bind", + "绑定 Telegram": "Bind Telegram", + "绑定信息": "Binding Information", + "绑定微信账户": "Bind WeChat Account", + "绑定成功!": "Binding successful!", + "绑定邮箱地址": "Bind Email Address", + "结束时间": "End Time", + "结果图片": "Result", + "绘图": "Drawing", + "绘图任务记录": "Drawing task records", + "绘图日志": "Drawing Logs", + "绘图设置": "Drawing settings", + "统计Tokens": "Statistical Tokens", + "统计次数": "Statistical count", + "统计额度": "Statistical quota", + "继续": "Continue", + "缓存 Tokens": "Cache Tokens", + "缓存: {{cacheRatio}}": "Cache: {{cacheRatio}}", + "缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Cache price: {{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (Cache ratio: {{cacheRatio}})", + "缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Cache price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (Cache ratio: {{cacheRatio}})", + "缓存倍率": "Cache ratio", + "缓存创建 Tokens": "Cache Creation Tokens", + "缓存创建: {{cacheCreationRatio}}": "Cache creation: {{cacheCreationRatio}}", + "缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "Cache creation price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (Cache creation ratio: {{cacheCreationRatio}})", + "编辑": "Edit", + "编辑API": "Edit API", + "编辑供应商": "Edit Provider", + "编辑公告": "Edit Notice", + "编辑公告内容": "Edit announcement content", + "编辑分类": "Edit Category", + "编辑成功": "Edit Successful", + "编辑标签": "Edit Tag", + "编辑模型": "Edit Model", + "编辑模式": "Edit Mode", + "编辑用户": "Edit User", + "编辑聊天配置": "Edit Chat Configuration", + "编辑问答": "Edit FAQ", + "缩词": "Shorten", + "缺省 MaxTokens": "Default MaxTokens", + "网站地址": "Website Address", + "网站域名标识": "Website Domain ID", + "网络错误": "Network Error", + "置信度": "Confidence", + "聊天": "Chat", + "聊天会话管理": "Chat session management", + "聊天区域": "Chat Area", + "聊天应用名称": "Chat Application Name", + "聊天应用名称已存在,请使用其他名称": "Chat application name already exists, please use another name", + "聊天设置": "Chat settings", + "聊天配置": "Chat configuration", + "聊天链接配置错误,请联系管理员": "Chat link configuration error, please contact administrator", + "联系我们": "Contact Us", + "腾讯混元": "Hunyuan", + "自动分组auto,从第一个开始选择": "Auto grouping auto, select from the first one", + "自动检测": "Auto Detect", + "自动模式": "Auto Mode", + "自动测试所有通道间隔时间": "Auto test interval for all channels", + "自动禁用": "Auto disabled", + "自动禁用关键词": "Automatic disable keywords", + "自动选择": "Auto Select", + "自定义充值数量选项": "Custom Recharge Amount Options", + "自定义充值数量选项不是合法的 JSON 数组": "Custom recharge amount options is not a valid JSON array", + "自定义变焦-提交": "Custom Zoom-Submit", + "自定义模型名称": "Custom model name", + "自定义货币": "Custom currency", + "自定义货币符号": "Custom currency symbol", + "自用模式": "Self-use mode", + "自适应列表": "Adaptive list", + "节省": "Save", + "花费": "Spend", + "花费时间": "Time spent", + "若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置": "If your OIDC Provider supports Discovery Endpoint, you can only fill in the OIDC Well-Known URL, and the system will automatically obtain the OIDC configuration", + "获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确": "Failed to get OIDC configuration, please check network status and whether the Well-Known URL is correct", + "获取 OIDC 配置成功!": "OIDC configuration obtained successfully!", + "获取2FA状态失败": "Failed to get Two-Factor Authentication status", + "获取初始化状态失败": "Failed to get initialization status", + "获取启用模型失败": "Failed to get enabled models", + "获取启用模型失败:": "Failed to get enabled models:", + "获取密钥": "Get Key", + "获取密钥失败": "Failed to get key", + "获取密钥状态失败": "Failed to get key status", + "获取未配置模型失败": "Failed to get unconfigured models", + "获取模型列表": "Get Model List", + "获取模型列表失败": "Failed to retrieve model list", + "获取渠道失败:": "Failed to get channels: ", + "获取组列表失败": "Failed to get group list", + "获取金额失败": "Failed to get amount", + "获取验证码": "Get Verification Code", + "补全": "Completion", + "补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "Completion price: {{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (Completion ratio: {{completionRatio}})", + "补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens": "Completion price: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens", + "补全倍率": "Completion ratio", + "补全倍率值": "Completion Ratio Value", + "补单": "Complete Order", + "补单失败": "Failed to complete order", + "补单成功": "Order completed successfully", + "表单引用错误,请刷新页面重试": "Form reference error, please refresh the page and try again", + "表格视图": "Table view", + "覆盖模式:将完全替换现有的所有密钥": "Overwrite mode: completely replace all existing keys", + "覆盖现有密钥": "Overwrite existing key", + "角色": "Role", + "解析响应数据时发生错误": "An error occurred while parsing response data", + "解析密钥文件失败: {{msg}}": "Failed to parse key file: {{msg}}", + "解绑 Passkey": "Remove Passkey", + "解绑后将无法使用 Passkey 登录,确定要继续吗?": "After unbinding, you will not be able to login with Passkey. Are you sure you want to continue?", + "计费类型": "Billing type", + "计费过程": "Billing process", + "订单号": "Order No.", + "讯飞星火": "Spark Desk", + "记录请求与错误日志IP": "Record request and error log IP", + "设备类型偏好": "Device Type Preference", + "设置 Logo": "Set Logo", + "设置2FA失败": "Failed to set up Two-Factor Authentication", + "设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}": "Set discounts for different recharge amounts, where the key is the recharge amount and the value is the discount rate, for example: {\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}", + "设置两步验证": "Set up two-factor authentication", + "设置令牌可用额度和数量": "Set token available quota and quantity", + "设置令牌的基本信息": "Set token basic information", + "设置令牌的访问限制": "Set token access restrictions", + "设置保存失败": "Settings save failed", + "设置保存成功": "Settings saved successfully", + "设置兑换码的基本信息": "Set redemption code basic information", + "设置兑换码的额度和数量": "Set redemption code quota and quantity", + "设置公告": "Set notice", + "设置关于": "Set about", + "设置已保存": "Settings saved", + "设置模型的基本信息": "Set the basic information of the model", + "设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱": "Set the email address for receiving quota warning notifications, if not set, the email address bound to the account will be used", + "设置用户协议": "Set user agreement", + "设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]": "Set recharge amount options that users can choose from, for example: [10, 20, 50, 100, 200, 500]", + "设置管理员登录信息": "Set administrator login information", + "设置类型": "Setting Type", + "设置系统名称": "Set system name", + "设置过短会影响数据库性能": "Setting too short will affect database performance", + "设置隐私政策": "Set privacy policy", + "设置页脚": "Set Footer", + "设置预填组的基本信息": "Set the basic information of the pre-filled group", + "设置首页内容": "Set home page content", + "设置默认地区和特定模型的专用地区": "Set default region and dedicated regions for specific models", + "设计与开发由": "Designed & Developed by", + "访问限制": "Access Restrictions", + "该供应商提供多种AI模型,适用于不同的应用场景。": "This supplier provides multiple AI models, suitable for different application scenarios.", + "该分类下没有可用模型": "No available models under this category", + "该域名已存在于白名单中": "The domain already exists in the whitelist", + "该数据可能不可信,请谨慎使用": "This data may not be reliable, please use with caution", + "该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置": "This server address will affect the payment callback address and the address displayed on the default homepage, please ensure correct configuration", + "该模型存在固定价格与倍率计费方式冲突,请确认选择": "The model has a fixed price and ratio billing method conflict, please confirm the selection", + "详情": "Details", + "语音输入": "Voice input", + "语音输出": "Voice output", + "说明": "Description", + "说明:": "Instructions:", + "说明信息": "Description", + "请上传密钥文件": "Please upload the key file", + "请上传密钥文件!": "Please upload the key file!", + "请为渠道命名": "Please name the channel", + "请先填写服务器地址": "Please fill in the server address first", + "请先输入密钥": "Please enter the key first", + "请先选择同步渠道": "Please select the synchronization channel first", + "请先选择模型!": "Please select a model first!", + "请先选择要删除的令牌!": "Please select the token to be deleted!", + "请先选择要删除的通道!": "Please select the channel you want to delete first!", + "请先选择要设置标签的渠道!": "Please select the channel to set tags for first!", + "请先选择需要批量设置的模型": "Please select models for batch setting first", + "请先阅读并同意用户协议和隐私政策": "Please read and agree to the user agreement and privacy policy first", + "请再次输入新密码": "Please enter the new password again", + "请前往个人设置 → 安全设置进行配置。": "Please go to Personal Settings → Security Settings to configure.", + "请勿过度信任此功能,IP可能被伪造": "Do not over-trust this feature, IP can be spoofed", + "请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit Group ratios in system settings to add new groups:", + "请填写完整的管理员账号信息": "Please fill in the complete administrator account information", + "请填写密钥": "Please enter the key", + "请填写渠道名称和渠道密钥!": "Please enter channel name and key!", + "请填写部署地区": "Please fill in the deployment region", + "请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。": "Keep key information secure, do not disclose to others. If there are security concerns, please change the key immediately.", + "请检查渠道配置或刷新重试": "Please check the channel configuration or refresh and try again", + "请检查表单填写是否正确": "Please check if the form is filled out correctly", + "请检查输入": "Please check your input", + "请求发生错误": "An error occurred with the request", + "请求发生错误: ": "An error occurred with the request: ", + "请求后端接口失败:": "Failed to request the backend interface: ", + "请求失败": "Request failed", + "请求头覆盖": "Request header override", + "请求并计费模型": "Request and charge model", + "请求路径": "Request path", + "请求时长: ${time}s": "Request time: ${time}s", + "请求次数": "Number of Requests", + "请求结束后多退少补": "Adjust after request completion", + "请求预扣费额度": "Pre-deduction quota for requests", + "请点击我": "Please click me", + "请确认以下设置信息,点击\"初始化系统\"开始配置": "Please confirm the following settings information, click \"Initialize system\" to start configuration", + "请确认您已了解禁用两步验证的后果": "Please confirm that you understand the consequences of disabling two-factor authentication", + "请确认管理员密码": "Please confirm the admin password", + "请稍后几秒重试,Turnstile 正在检查用户环境!": "Please try again in a few seconds, Turnstile is checking the user environment!", + "请联系管理员在系统设置中配置API信息": "Please contact the administrator to configure API information in the system settings.", + "请联系管理员在系统设置中配置Uptime": "Please contact the administrator to configure Uptime in the system settings.", + "请联系管理员在系统设置中配置公告信息": "Please contact the administrator to configure notice information in the system settings.", + "请联系管理员在系统设置中配置常见问答": "Please contact the administrator to configure FAQ information in the system settings.", + "请联系管理员配置聊天链接": "Please contact the administrator to configure the chat link", + "请至少选择一个令牌!": "Please select at least one token!", + "请至少选择一个兑换码!": "Please select at least one redemption code!", + "请至少选择一个模型": "Please select at least one model", + "请至少选择一个模型!": "Please select at least one model!", + "请至少选择一个渠道": "Please select at least one channel", + "请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "Please enter AZURE_OPENAI_ENDPOINT, e.g.: https://docs-test-001.openai.azure.com", + "请输入 JSON 格式的密钥内容,例如:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}": "Please enter the key content in JSON format, for example:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}", + "请输入 OIDC 的 Well-Known URL": "Please enter the Well-Known URL for OIDC", + "请输入6位验证码或8位备用码": "Please enter a 6-digit verification code or 8-digit backup code", + "请输入API地址": "Please enter the API address", + "请输入API地址!": "Please enter the API address!", + "请输入Bark推送URL": "Please enter Bark push URL", + "请输入Bark推送URL,例如: https://api.day.app/yourkey/{{title}}/{{content}}": "Please enter Bark push URL, e.g.: https://api.day.app/yourkey/{{title}}/{{content}}", + "请输入Gotify应用令牌": "Please enter Gotify application token", + "请输入Gotify服务器地址": "Please enter Gotify server address", + "请输入Gotify服务器地址,例如: https://gotify.example.com": "Please enter Gotify server address, e.g.: https://gotify.example.com", + "请输入Uptime Kuma地址": "Please enter the Uptime Kuma address", + "请输入Uptime Kuma服务地址,如:https://status.example.com": "Please enter the Uptime Kuma service address, such as: https://status.example.com", + "请输入URL链接": "Please enter the URL link", + "请输入Webhook地址": "Please enter the Webhook address", + "请输入Webhook地址,例如: https://example.com/webhook": "Please enter the Webhook URL, e.g.: https://example.com/webhook", + "请输入你的账户名以确认删除!": "Please enter your account name to confirm deletion!", + "请输入供应商名称": "Please enter the vendor name", + "请输入供应商名称,如:OpenAI": "Please enter the vendor name, such as: OpenAI", + "请输入供应商描述": "Please enter the vendor description", + "请输入兑换码": "Please enter the redemption code", + "请输入兑换码!": "Please enter the redemption code!", + "请输入公告内容": "Please enter the notice content", + "请输入公告内容(支持 Markdown/HTML)": "Please enter the notice content (supports Markdown/HTML)", + "请输入分类名称": "Please enter category name", + "请输入分类名称,如:OpenAI、Claude等": "Please enter the category name, such as: OpenAI, Claude, etc.", + "请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com": "Please enter the path before /suno, usually the domain, e.g.: https://api.example.com", + "请输入原密码": "Please enter the original password", + "请输入原密码!": "Please enter the original password!", + "请输入名称": "Please enter a name", + "请输入回答内容": "Please enter the answer content", + "请输入回答内容(支持 Markdown/HTML)": "Please enter the answer content (supports Markdown/HTML)", + "请输入图标名称": "Please enter the icon name", + "请输入填充值": "Please enter a value", + "请输入备注(仅管理员可见)": "Please enter a remark (only visible to administrators)", + "请输入完整的 JSON 格式密钥内容": "Please enter the complete JSON format key content", + "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Please enter complete URL, e.g.: https://api.openai.com/v1/chat/completions", + "请输入完整的URL链接": "Please enter the complete URL link", + "请输入密码": "Please enter password", + "请输入密钥": "Please enter the key", + "请输入密钥,一行一个": "Please enter the key, one per line", + "请输入密钥!": "Please enter the key!", + "请输入您的密码": "Please enter your password", + "请输入您的用户名以确认删除": "Please enter your username to confirm deletion", + "请输入您的用户名或邮箱地址": "Please enter your username or email address", + "请输入您的邮箱地址": "Please enter your email address", + "请输入您的问题...": "Please enter your question...", + "请输入数值": "Enter a value", + "请输入数字": "Please enter a number", + "请输入新密码": "Please enter the new password", + "请输入新密码!": "Please enter the new password!", + "请输入新建数量": "Please enter the quantity to create", + "请输入新标签,留空则解散标签": "Please enter a new tag, leave blank to dissolve the tag", + "请输入新的剩余额度": "Please enter the new remaining quota", + "请输入新的密码,最短 8 位": "Please enter a new password, at least 8 characters", + "请输入新的显示名称": "Please enter a new display name", + "请输入新的用户名": "Please enter a new username", + "请输入显示名称": "Please enter display name", + "请输入有效的数字": "Please enter a valid number", + "请输入标签名称": "Please enter the tag name", + "请输入模型倍率": "Enter model ratio", + "请输入模型倍率和补全倍率": "Please enter model ratio and completion ratio", + "请输入模型名称": "Please enter the model name", + "请输入模型名称,如:gpt-4": "Please enter the model name, such as: gpt-4", + "请输入模型描述": "Please enter the model description", + "请输入消息内容...": "Please enter the message content...", + "请输入状态页面Slug": "Please enter the status page Slug", + "请输入状态页面的Slug,如:my-status": "Please enter the slug for the status page, such as: my-status", + "请输入生成数量": "Please enter the quantity to generate", + "请输入用户名": "Please enter username", + "请输入私有部署地址,格式为:https://fastgpt.run/api/openapi": "Please enter private deployment address, format: https://fastgpt.run/api/openapi", + "请输入管理员密码": "Please enter the admin password", + "请输入管理员用户名": "Please enter the admin username", + "请输入线路描述": "Please enter the route description", + "请输入组名": "Please enter the group name", + "请输入组描述": "Please enter the group description", + "请输入组织org-xxx": "Please enter organization org-xxx", + "请输入聊天应用名称": "Please enter chat application name", + "请输入补全倍率": "Enter completion ratio", + "请输入要设置的标签名称": "Please enter the tag name to be set", + "请输入认证器验证码": "Please enter authenticator verification code", + "请输入认证器验证码或备用码": "Please enter authenticator verification code or backup code", + "请输入说明": "Please enter the description", + "请输入邮箱!": "Please enter your email!", + "请输入邮箱地址": "Please enter the email address", + "请输入邮箱验证码!": "Please enter the email verification code!", + "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "Please enter the deployment region, for example: us-central1\nSupports using model mapping format\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}", + "请输入问题标题": "Please enter the question title", + "请输入预警阈值": "Please enter alert threshold", + "请输入预警额度": "Please enter alert quota", + "请输入额度": "Please enter the quota", + "请输入验证码": "Please enter verification code", + "请输入验证码或备用码": "Please enter verification code or backup code", + "请输入默认 API 版本,例如:2025-04-01-preview": "Please enter default API version, e.g.: 2025-04-01-preview.", + "请选择API地址": "Please select API address", + "请选择你的复制方式": "Please select your copy method", + "请选择使用模式": "Please select the usage mode", + "请选择分组": "Please select a group", + "请选择发布日期": "Please select the publish date", + "请选择可以使用该渠道的分组": "Please select groups that can use this channel", + "请选择可以使用该渠道的分组,留空则不更改": "Please select the groups that can use this channel, leaving blank will not change", + "请选择同步语言": "Please select sync language", + "请选择名称匹配类型": "Please select the name matching type", + "请选择多密钥使用策略": "Please select multi-key usage policy", + "请选择密钥更新模式": "Please select key update mode", + "请选择密钥格式": "Please select key format", + "请选择日志记录时间": "Please select log record time", + "请选择模型": "Please select model", + "请选择模型。": "Please select model.", + "请选择消息优先级": "Please select message priority", + "请选择渠道类型": "Please select channel type", + "请选择组类型": "Please select group type", + "请选择该令牌支持的模型,留空支持所有模型": "Select models supported by the token, leave blank to support all models", + "请选择该渠道所支持的模型": "Please select the model supported by this channel", + "请选择该渠道所支持的模型,留空则不更改": "Please select the models supported by the channel, leaving blank will not change", + "请选择过期时间": "Please select expiration time", + "请选择通知方式": "Please select notification method", + "调用次数": "Call Count", + "调用次数分布": "Models call distribution", + "调用次数排行": "Models call ranking", + "调试信息": "Debug information", + "谨慎": "Cautious", + "警告": "Warning", + "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔": "Warning: After enabling keep-alive, if the channel fails after keep-alive data has been written, the system cannot retry. If you must enable it, it is recommended to set the Ping interval as large as possible", + "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "Warning: Disabling two-factor authentication will permanently delete your verification settings and all backup codes. This action is irreversible!", + "豆包": "Doubao", + "账单": "Bills", + "账户充值": "Account recharge", + "账户已删除!": "Account has been deleted!", + "账户已锁定": "Account locked", + "账户数据": "Account Data", + "账户管理": "Account management", + "账户绑定": "Account Binding", + "账户绑定、安全设置和身份验证": "Account binding, security settings and identity verification", + "账户统计": "Account statistics", + "货币单位": "Currency Unit", + "购买兑换码": "Buy redemption code", + "资源消耗": "Resource Consumption", + "起始时间": "Start Time", + "超级管理员": "Super Admin", + "超级管理员未设置充值链接!": "Super administrator has not set the recharge link!", + "跟随系统主题设置": "Follow system theme", + "跳转": "Jump", + "轮询": "Polling", + "轮询模式": "Polling mode", + "轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能": "Polling mode must be used with Redis and memory cache functions, otherwise the performance will be significantly reduced and the polling function will not be implemented", + "输入": "Input", + "输入 OIDC 的 Authorization Endpoint": "Enter OIDC Authorization Endpoint", + "输入 OIDC 的 Client ID": "Enter OIDC Client ID", + "输入 OIDC 的 Token Endpoint": "Enter OIDC Token Endpoint", + "输入 OIDC 的 Userinfo Endpoint": "Enter OIDC Userinfo Endpoint", + "输入IP地址后回车,如:8.8.8.8": "Enter IP address and press Enter, e.g.: 8.8.8.8", + "输入JSON对象": "Enter JSON Object", + "输入价格": "Enter Price", + "输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}": "Enter price: {{symbol}}{{price}} / 1M tokens{{audioPrice}}", + "输入你注册的 LinuxDO OAuth APP 的 ID": "Enter the ID of your registered LinuxDO OAuth APP", + "输入你的账户名{{username}}以确认删除": "Enter your account name{{username}} to confirm deletion", + "输入域名后回车": "Enter domain and press Enter", + "输入域名后回车,如:example.com": "Enter domain and press Enter, e.g.: example.com", + "输入密码,最短 8 位,最长 20 位": "Enter password, at least 8 characters and up to 20 characters", + "输入数字": "Enter Number", + "输入标签或使用\",\"分隔多个标签": "Enter tags or use \",\" to separate multiple tags", + "输入模型倍率": "Enter model ratio", + "输入每次价格": "Enter per-use price", + "输入端口后回车,如:80 或 8000-8999": "Enter port and press Enter, e.g.: 80 or 8000-8999", + "输入系统提示词,用户的系统提示词将优先于此设置": "Enter system prompt, user's system prompt will take priority over this setting", + "输入自定义模型名称": "Enter Custom Model Name", + "输入补全价格": "Enter Completion Price", + "输入补全倍率": "Enter completion ratio", + "输入要添加的邮箱域名": "Enter the email domain to add", + "输入认证器应用显示的6位数字验证码": "Enter the 6-digit verification code displayed on the authenticator application", + "输入邮箱地址": "Enter Email Address", + "输入项目名称,按回车添加": "Enter the item name, press Enter to add", + "输入验证码": "Enter Verification Code", + "输入验证码完成设置": "Enter verification code to complete setup", + "输出": "Output", + "输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}": "Output {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}", + "输出价格": "Output Price", + "输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "Output price: {{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (Completion ratio: {{completionRatio}})", + "边栏设置": "Sidebar Settings", + "过期时间": "Expiration time", + "过期时间不能早于当前时间!": "Expiration time cannot be earlier than the current time!", + "过期时间快捷设置": "Expiration time quick settings", + "过期时间格式错误!": "Expiration time format error!", + "运营设置": "Operation Settings", + "返回登录": "Return to Login", + "这是重复键中的最后一个,其值将被使用": "This is the last one among duplicate keys, and its value will be used", + "进度": "Progress", + "进行中": "Ongoing", + "进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "When performing this operation, it may cause channel access errors. Please only use it when there is a problem with the database.", + "连接保活设置": "Connection Keep-alive Settings", + "连接已断开": "Connection Disconnected", + "追加到现有密钥": "Append to existing key", + "追加模式:将新密钥添加到现有密钥列表末尾": "Append mode: add new keys to the end of the existing key list", + "追加模式:新密钥将添加到现有密钥列表的末尾": "Append mode: new keys will be added to the end of the existing key list", + "退出": "Quit", + "适用于个人使用的场景,不需要设置模型价格": "Suitable for personal use, no need to set model price.", + "适用于为多个用户提供服务的场景": "Suitable for scenarios where multiple users are provided.", + "适用于展示系统功能的场景,提供基础功能演示": "Suitable for scenarios where the system functions are displayed, providing basic feature demonstrations.", + "适配 -thinking、-thinking-预算数字 和 -nothinking 后缀": "Adapt to -thinking, -thinking-budget number, and -nothinking suffixes", + "选择充值额度": "Select recharge amount", + "选择分组": "Select group", + "选择同步来源": "Select sync source", + "选择同步渠道": "Select synchronization channel", + "选择同步语言": "Select sync language", + "选择成功": "Selection successful", + "选择支付方式": "Select payment method", + "选择支持的认证设备类型": "Choose supported authentication device types", + "选择方式": "Select method", + "选择时间": "Select Time", + "选择模型": "Select model", + "选择模型供应商": "Select model vendor", + "选择模型后可一键填充当前选中令牌(或本页第一个令牌)。": "After selecting a model, you can fill the current selected token (or the first token on this page) with one click.", + "选择模型开始对话": "Select a model to start the conversation", + "选择端点类型": "Select Endpoint Type", + "选择系统运行模式": "Select system running mode", + "选择组类型": "Please select group type", + "选择要覆盖的冲突项": "Select conflict items to overwrite", + "选择语言": "Select language", + "选择过期时间(可选,留空为永久)": "Select expiration time (optional, leave blank for permanent)", + "透传请求体": "Pass through body", + "通义千问": "Qwen", + "通用设置": "General Settings", + "通知": "Notice", + "通知、价格和隐私相关设置": "Notification, price and privacy related settings", + "通知内容": "Notification content", + "通知内容,支持 {{value}} 变量占位符": "Notification content, supports {{value}} variable placeholders", + "通知方式": "Notification method", + "通知标题": "Notification title", + "通知类型 (quota_exceed: 额度预警)": "Notification type (quota_exceed: quota warning)", + "通知邮箱": "Notification email", + "通知配置": "Notification configuration", + "通过划转功能将奖励额度转入到您的账户余额中": "Transfer the reward amount to your account balance through the transfer function", + "通过密码注册时需要进行邮箱验证": "Email verification is required when registering via password", + "通道 ${name} 余额更新成功!": "Channel ${name} quota updated successfully!", + "通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test successful, model ${model} took ${time.toFixed(2)} seconds.", + "通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test successful, took ${time.toFixed(2)} seconds.", + "速率限制设置": "Rate limit settings", + "邀请": "Invitations", + "邀请人": "Inviter", + "邀请人数": "Number of people invited", + "邀请信息": "Invitation information", + "邀请奖励": "Invite reward", + "邀请好友注册,好友充值后您可获得相应奖励": "Invite friends to register, and you can get the corresponding reward after the friend recharges", + "邀请好友获得额外奖励": "Invite friends to get additional rewards", + "邀请新用户奖励额度": "Referral bonus quota", + "邀请的好友越多,获得的奖励越多": "The more friends you invite, the more rewards you will get", + "邀请码": "Invitation code", + "邀请获得额度": "Invitation quota", + "邀请链接": "Invitation link", + "邀请链接已复制到剪切板": "Invitation link has been copied to clipboard", + "邮件通知": "Email notification", + "邮箱": "Email", + "邮箱地址": "Email address", + "邮箱域名格式不正确,请输入有效的域名,如 gmail.com": "The email domain format is incorrect. Please enter a valid domain such as gmail.com", + "邮箱域名白名单格式不正确": "The email domain whitelist format is incorrect", + "邮箱账户绑定成功!": "Email account bound successfully!", + "部分保存失败": "Some settings failed to save", + "部分保存失败,请重试": "Partial saving failed, please try again", + "部分渠道测试失败:": "Some channels failed to test: ", + "部署地区": "Deployment Region", + "配置": "Configure", + "配置 GitHub OAuth App": "Configure GitHub OAuth App", + "配置 Linux DO OAuth": "Configure Linux DO OAuth", + "配置 OIDC": "Configure OIDC", + "配置 Passkey": "Configure Passkey", + "配置 SMTP": "Configure SMTP", + "配置 Telegram 登录": "Configure Telegram Login", + "配置 Turnstile": "Configure Turnstile", + "配置 WeChat Server": "Configure WeChat Server", + "配置和消息已全部重置": "Configuration and messages have been completely reset", + "配置导入成功": "Configuration imported successfully", + "配置已导出到下载文件夹": "Configuration has been exported to the download folder", + "配置已重置,对话消息已保留": "Configuration has been reset, conversation messages have been retained", + "配置文件同步": "Config file sync", + "配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全": "Configure Server-Side Request Forgery (SSRF) protection to secure internal network resources", + "配置登录注册": "Configure Login/Registration", + "配置说明": "Configuration instructions", + "配置邮箱域名白名单": "Configure email domain whitelist", + "重复提交": "Duplicate submission", + "重复的键名": "Duplicate key name", + "重复的键名,此值将被后面的同名键覆盖": "Duplicate key name, this value will be overridden by the subsequent key with the same name", + "重定向 URL 填": "Redirect URL fill", + "重新发送": "Resend", + "重新生成": "Regenerate", + "重新生成备用码": "Regenerate backup codes", + "重新生成备用码失败": "Failed to regenerate backup codes", + "重新生成备用码将使现有的备用码失效,请确保您已保存了当前的备用码。": "Regenerating backup codes will invalidate existing backup codes. Please ensure you have saved the current backup codes.", + "重绘": "Vary", + "重置": "Reset", + "重置 2FA": "Reset Two-Factor Authentication", + "重置 Passkey": "Reset Passkey", + "重置为默认": "Reset to Default", + "重置模型倍率": "Reset model ratio", + "重置选项": "Reset options", + "重置邮件发送成功,请检查邮箱!": "The reset email was sent successfully, please check your email!", + "重置配置": "Reset configuration", + "重试": "Retry", + "钱包管理": "Wallet Management", + "链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1": "The {key} in the link will be automatically replaced with sk-xxxx, the {address} will be automatically replaced with the server address in system settings, and the end will not have / and /v1", + "错误": "Error", + "键为分组名称,值为另一个 JSON 对象,键为分组名称,值为该分组的用户的特殊分组倍率,例如:{\"vip\": {\"default\": 0.5, \"test\": 1}},表示 vip 分组的用户在使用default分组的令牌时倍率为0.5,使用test分组时倍率为1": "The key is the group name, and the value is another JSON object. The key is the group name, and the value is the special group ratio for users in that group. For example: {\"vip\": {\"default\": 0.5, \"test\": 1}} means that users in the vip group have a ratio of 0.5 when using tokens from the default group, and a ratio of 1 when using tokens from the test group", + "键为原状态码,值为要复写的状态码,仅影响本地判断": "The key is the original status code, and the value is the status code to override, only affects local judgment", + "键为端点类型,值为路径和方法对象": "The key is the endpoint type, the value is the path and method object", + "键为请求中的模型名称,值为要替换的模型名称": "Key is the model name in the request, value is the model name to replace", + "键名": "Key name", + "问题标题": "Question Title", + "队列中": "In queue", + "降低您账户的安全性": "Reduce your account security", + "降级": "Demote", + "限制周期": "Limit period", + "限制周期统一使用上方配置的“限制周期”值。": "The limit period uniformly uses the \"limit period\" value configured above.", + "隐私政策": "Privacy Policy", + "隐私政策已更新": "Privacy policy updated", + "隐私政策更新失败": "Privacy policy update failed", + "隐私设置": "Privacy settings", + "隐藏操作项": "Hide actions", + "隐藏调试": "Hide debug", + "随机": "Random", + "随机模式": "Random mode", + "零一万物": "Yi", + "需要安全验证": "Security verification required", + "需要添加的额度(支持负数)": "Need to add quota (supports negative numbers)", + "需要登录访问": "Require Login", + "需要重新完整设置才能再次启用": "Need to set up again to re-enable", + "非必要,不建议启用模型限制": "Not necessary, model restrictions are not recommended", + "非流": "not stream", + "音频倍率(仅部分模型支持该计费)": "Audio ratio (only supported by some models for billing)", + "音频提示 {{input}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} = {{symbol}}{{total}}": "Audio prompt {{input}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + Audio completion {{completion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} = {{symbol}}{{total}}", + "音频提示价格:{{symbol}}{{price}} * {{audioRatio}} = {{symbol}}{{total}} / 1M tokens (音频倍率: {{audioRatio}})": "Audio prompt price: {{symbol}}{{price}} * {{audioRatio}} = {{symbol}}{{total}} / 1M tokens (Audio ratio: {{audioRatio}})", + "音频补全价格:{{symbol}}{{price}} * {{audioRatio}} * {{audioCompRatio}} = {{symbol}}{{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})": "Audio completion price: {{symbol}}{{price}} * {{audioRatio}} * {{audioCompRatio}} = {{symbol}}{{total}} / 1M tokens (Audio completion ratio: {{audioCompRatio}})", + "音频补全倍率(仅部分模型支持该计费)": "Audio completion ratio (only supported by some models for this billing)", + "音频输入相关的倍率设置,键为模型名称,值为倍率": "Audio input related ratio settings, key is model name, value is ratio", + "音频输出补全相关的倍率设置,键为模型名称,值为倍率": "Audio output completion related ratio settings, key is model name, value is ratio", + "页脚": "Footer", + "页面未找到,请检查您的浏览器地址是否正确": "Page not found, please check if your browser address is correct", + "顶栏管理": "Header Management", + "项目": "Project", + "项目内容": "Project content", + "项目操作按钮组": "Project action button group", + "预填组管理": "Pre-filled group", + "预览失败": "Preview failed", + "预览更新": "Preview update", + "预览请求体": "Preview request body", + "预警阈值必须为正数": "Warning threshold must be a positive number", + "频率限制的周期(分钟)": "Rate limit period (minutes)", + "颜色": "Color", + "额度": "Quota", + "额度必须大于0": "Quota must be greater than 0", + "额度提醒阈值": "Quota reminder threshold", + "额度查询接口返回令牌额度而非用户额度": "Displays token quota instead of user quota", + "额度设置": "Quota Settings", + "额度预警阈值": "Quota warning threshold", + "首尾生视频": "Head-tail generated video", + "首页": "Home", + "首页内容": "Home Page Content", + "验证": "Verify", + "验证 Passkey": "Verify Passkey", + "验证失败,请重试": "Verification failed, please try again", + "验证成功": "Verification successful", + "验证数据库连接状态": "Verify database connection status", + "验证码": "Verification Code", + "验证码发送成功,请检查邮箱!": "The verification code was sent successfully, please check your email!", + "验证设置": "Verify setup", + "验证身份": "Verify identity", + "验证配置错误": "Verification configuration error", + "高级设置": "Advanced Settings", + "黑名单": "Blacklist", + "默认": "Default", + "默认 API 版本": "Default API Version", + "默认 Responses API 版本,为空则使用上方版本": "Default Responses API version, if empty, uses the version above", + "默认使用系统名称": "Default uses system name", + "默认区域": "Default region", + "默认区域,如: us-central1": "Default region, e.g.: us-central1", + "默认折叠侧边栏": "Default collapse sidebar", + "默认测试模型": "Default Test Model", + "默认补全倍率": "Default completion ratio", + "选择充值套餐": "Choose a top-up package", + "Creem 设置": "Creem Setting", + "Creem 充值": "Creem Recharge", + "Creem 介绍": "Creem is the payment partner you always deserved, we strive for simplicity and straightforwardness on our APIs.", + "Creem Setting Tips": "Creem only supports preset fixed-amount products. These products and their prices need to be created and configured in advance on the Creem website, so custom dynamic amount top-ups are not supported. Configure the product name and price on Creem, obtain the Product Id, and then fill it in for the product below. Set the top-up amount and display price for this product in the new API.", + "Webhook 密钥": "Webhook Secret", + "测试模式": "Test Mode", + "Creem API 密钥,敏感信息不显示": "Creem API key, sensitive information not displayed", + "用于验证回调 new-api 的 webhook 请求的密钥,敏感信息不显示": "The key used to validate webhook requests for the callback new-api, sensitive information is not displayed.", + "启用后将使用 Creem Test Mode": "", + "展示价格": "Display Pricing", + "Recharge Quota": "Recharge Quota", + "产品配置": "Product Configuration", + "产品名称": "Product Name", + "产品ID": "Product ID", + "暂无产品配置": "No product configuration", + "更新 Creem 设置": "Update Creem Settings", + "编辑产品": "Edit Product", + "添加产品": "Add Product", + "例如:基础套餐": "e.g.: Basic Package", + "例如:prod_6I8rBerHpPxyoiU9WK4kot": "e.g.: prod_6I8rBerHpPxyoiU9WK4kot", + "货币": "Currency", + "欧元": "EUR", + "USD (美元)": "USD (US Dollar)", + "EUR (欧元)": "EUR (Euro)", + "例如:4.99": "e.g.: 4.99", + "例如:100000": "e.g.: 100000", + "请填写完整的产品信息": "Please fill in complete product information", + "产品ID已存在": "Product ID already exists" + } } \ No newline at end of file diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json new file mode 100644 index 000000000..27789a191 --- /dev/null +++ b/web/src/i18n/locales/fr.json @@ -0,0 +1,2085 @@ +{ + "translation": { + " + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_one": " + Recherche Web {{count}} fois / 1K fois * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_many": " + Recherche Web {{count}} fois / 1K fois * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_other": " + Recherche Web {{count}} fois / 1K fois * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + 图片生成调用 {{symbol}}{{price}} / 1次 * {{ratioType}} {{ratio}}": " + Appel de génération d'image {{symbol}}{{price}} / 1 fois * {{ratioType}} {{ratio}}", + " + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_one": " + Recherche de fichiers {{count}} fois / 1K fois * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_many": " + Recherche de fichiers {{count}} fois / 1K fois * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_other": " + Recherche de fichiers {{count}} fois / 1K fois * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " 个模型设置相同的值": " modèles avec la même valeur", + " 吗?": " ?", + " 秒": "s", + ",时间:": ", time:", + ",点击更新": ", cliquez sur Mettre à jour", + "(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Actuellement, seule l'interface Epay est prise en charge, l'adresse du serveur ci-dessus est utilisée par défaut comme adresse de rappel !)", + "(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "(Entrée {{input}} tokens / 1M tokens * {{symbol}}{{price}}", + "(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(Entrée {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + Entrée audio {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}", + "(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}": "(Entrée {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + Cache {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}", + "(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * {{symbol}}{{price}}": "(Entrée {{nonImageInput}} tokens + Entrée image {{imageInput}} tokens * {{imageRatio}} / 1M tokens * {{symbol}}{{price}}", + "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。": "La valeur maximale de [Nombre maximal de requêtes] et [Nombre maximal d'achèvements de requêtes] est 2147483647.", + "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。": "[Nombre maximal de requêtes] doit être supérieur ou égal à 0, [Nombre maximal d'achèvements de requêtes] doit être supérieur ou égal à 1.", + "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}": "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}", + "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}", + "© {{currentYear}}": "© {{currentYear}}", + "| 基于": " | Basé sur ", + "$/1M tokens": "$/1M tokens", + "0 - 最低": "0 - La plus basse", + "0.002-1之间的小数": "Décimal entre 0,002-1", + "0.1以上的小数": "Décimal supérieur à 0,1", + "10 - 最高": "10 - La plus haute", + "2 - 低": "2 - Basse", + "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "Après le 10 mai 2025, les canaux ajoutés n'ont plus besoin de supprimer le point dans le nom du modèle lors du déploiement", + "360智脑": "360 AI Brain", + "5 - 正常(默认)": "5 - Normale (par défaut)", + "8 - 高": "8 - Haute", + "AGPL v3.0协议": "Licence AGPL v3.0", + "AI 对话": "Conversation IA", + "AI模型测试环境": "Environnement de test de modèle d'IA", + "AI模型配置": "Configuration du modèle d'IA", + "API Key 模式下不支持批量创建": "Création en lot non prise en charge en mode clé API", + "API 地址和相关配置": "URL de l'API et configuration associée", + "API 密钥": "Clé API", + "API 文档": "Documentation de l'API", + "API 配置": "Configuration de l'API", + "API令牌管理": "Gestion des jetons d'API", + "API使用记录": "Enregistrements d'utilisation de l'API", + "API信息": "Informations sur l'API", + "API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)": "Gestion des informations de l'API, vous pouvez configurer plusieurs adresses d'API pour l'affichage de l'état et l'équilibrage de charge (maximum 50)", + "API地址": "URL de base", + "API渠道配置": "Configuration du canal de l'API", + "API端点": "Points de terminaison de l'API", + "Authorization callback URL 填": "Remplir l'URL de rappel d'autorisation", + "Authorization Endpoint": "Point de terminaison d'autorisation", + "auto分组调用链路": "Chaîne d'appels de groupe auto", + "Bark推送URL": "URL de notification Bark", + "Bark推送URL必须以http://或https://开头": "L'URL de notification Bark doit commencer par http:// ou https://", + "Bark通知": "Notification Bark", + "Changing batch type to:": "Changement du type de lot en :", + "Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Adaptation de la pensée Claude BudgetTokens = MaxTokens * BudgetTokens pourcentage", + "Claude设置": "Paramètres Claude", + "Claude请求头覆盖": "Remplacement de l'en-tête de la requête Claude", + "Client ID": "ID client", + "Client Secret": "Secret client", + "common.changeLanguage": "Changer de langue", + "default为默认设置,可单独设置每个分类的安全等级": "\"default\" est le paramètre par défaut, et chaque catégorie peut être définie séparément", + "default为默认设置,可单独设置每个模型的版本": "\"default\" est le paramètre par défaut, et chaque modèle peut être défini séparément", + "Dify渠道只适配chatflow和agent,并且agent不支持图片!": "Le canal Dify ne prend en charge que chatflow et agent, et l'agent ne prend pas en charge les images !", + "false": "faux", + "Gemini安全设置": "Paramètres de sécurité Gemini", + "Gemini思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Adaptation de la pensée Gemini BudgetTokens = MaxTokens * BudgetTokens pourcentage", + "Gemini思考适配设置": "Paramètres d'adaptation de la pensée Gemini", + "Gemini版本设置": "Paramètres de version Gemini", + "Gemini设置": "Paramètres Gemini", + "GitHub": "GitHub", + "GitHub Client ID": "ID client GitHub", + "GitHub Client Secret": "Secret client GitHub", + "GitHub ID": "ID GitHub", + "Gotify应用令牌": "Jeton d'application Gotify", + "Gotify服务器地址": "Adresse du serveur Gotify", + "Gotify服务器地址必须以http://或https://开头": "L'adresse du serveur Gotify doit commencer par http:// ou https://", + "Gotify通知": "Notification Gotify", + "Homepage URL 填": "Remplir l'URL de la page d'accueil", + "ID": "ID", + "IP": "IP", + "IP白名单": "Liste blanche d'adresses IP", + "IP限制": "Restrictions d'IP", + "IP黑名单": "Liste noire d'adresses IP", + "JSON": "JSON", + "JSON 模式支持手动输入或上传服务账号 JSON": "Le mode JSON prend en charge la saisie manuelle ou le téléchargement du JSON du compte de service", + "JSON格式密钥,请确保格式正确": "Clé au format JSON, veuillez vous assurer que le format est correct", + "JSON编辑": "Édition JSON", + "JSON解析错误:": "Erreur d'analyse JSON :", + "Linux DO Client ID": "ID client Linux DO", + "Linux DO Client Secret": "Secret client Linux DO", + "LinuxDO": "LinuxDO", + "LinuxDO ID": "ID LinuxDO", + "Logo 图片地址": "Adresse de l'image du logo", + "Midjourney 任务记录": "Enregistrements de tâches Midjourney", + "MIT许可证": "Licence MIT", + "New API项目仓库地址:": "Adresse du référentiel du projet New API : ", + "OIDC": "OIDC", + "OIDC ID": "ID OIDC", + "Passkey": "Passkey", + "Passkey 已解绑": "Passkey délié", + "Passkey 已重置": "Le Passkey a été réinitialisé", + "Passkey 是基于 WebAuthn 标准的无密码身份验证方法,支持指纹、面容、硬件密钥等认证方式": "Passkey est une méthode d'authentification sans mot de passe basée sur la norme WebAuthn, prenant en charge les empreintes digitales, la reconnaissance faciale, les clés matérielles et d'autres méthodes d'authentification", + "Passkey 注册失败,请重试": "L'enregistrement du Passkey a échoué. Veuillez réessayer.", + "Passkey 注册成功": "Enregistrement du Passkey réussi", + "Passkey 登录": "Connexion avec Passkey", + "Ping间隔(秒)": "Intervalle de ping (secondes)", + "price_xxx 的商品价格 ID,新建产品后可获得": "ID de prix du produit price_xxx, peut être obtenu après la création d'un nouveau produit", + "Reasoning Effort": "Effort de raisonnement", + "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "Le champ safety_identifier aide OpenAI à identifier les utilisateurs d'applications susceptibles de violer les politiques d'utilisation. Désactivé par défaut pour protéger la confidentialité des utilisateurs", + "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "Le champ service_tier est utilisé pour spécifier le niveau de service. Permettre le passage peut entraîner une facturation plus élevée que prévu. Désactivé par défaut pour éviter des frais supplémentaires", + "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "Clé secrète Stripe sk_xxx ou rk_xxx, les informations sensibles ne sont pas affichées", + "SMTP 发送者邮箱": "Adresse e-mail de l'expéditeur SMTP", + "SMTP 服务器地址": "Adresse du serveur SMTP", + "SMTP 端口": "Port SMTP", + "SMTP 访问凭证": "Informations d'identification d'accès SMTP", + "SMTP 账户": "Compte SMTP", + "SSRF防护开关详细说明": "L'interrupteur principal contrôle si la protection SSRF est activée. Lorsqu'elle est désactivée, toutes les vérifications SSRF sont contournées, autorisant l'accès à n'importe quelle URL. ⚠️ Ne désactivez cette fonctionnalité que dans des environnements entièrement fiables.", + "SSRF防护设置": "Paramètres de protection SSRF", + "SSRF防护详细说明": "La protection SSRF empêche les utilisateurs malveillants d'utiliser votre serveur pour accéder aux ressources du réseau interne. Configurez des listes blanches pour les domaines/IP de confiance et limitez les ports autorisés. S'applique aux téléchargements de fichiers, aux webhooks et aux notifications.", + "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "Le champ store autorise OpenAI à stocker les données de requête pour l'évaluation et l'optimisation du produit. Désactivé par défaut. L'activation peut causer un dysfonctionnement de Codex", + "Stripe 设置": "Paramètres Stripe", + "Telegram": "Telegram", + "Telegram Bot Token": "Jeton du bot Telegram", + "Telegram Bot 名称": "Nom du bot Telegram", + "Telegram ID": "ID Telegram", + "Token Endpoint": "Point de terminaison du jeton", + "true": "vrai", + "Turnstile Secret Key": "Clé secrète Turnstile", + "Turnstile Site Key": "Clé du site Turnstile", + "Unix时间戳": "Horodatage Unix", + "Uptime Kuma地址": "Adresse Uptime Kuma", + "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Gestion des catégories de surveillance Uptime Kuma, vous pouvez configurer plusieurs catégories de surveillance pour l'affichage de l'état du service (maximum 20)", + "URL链接": "Lien URL", + "User Info Endpoint": "Point de terminaison des informations utilisateur", + "Webhook 签名密钥": "Clé de signature Webhook", + "Webhook地址": "URL du Webhook", + "Webhook地址必须以https://开头": "L'adresse Webhook doit commencer par https://", + "Webhook请求结构说明": "Description de la structure de la requête Webhook", + "Webhook通知": "Notification par Webhook", + "Web搜索价格:{{symbol}}{{price}} / 1K 次": "Prix de recherche Web : {{symbol}}{{price}} / 1K fois", + "WeChat Server 服务器地址": "Adresse du serveur WeChat Server", + "WeChat Server 访问凭证": "Informations d'identification d'accès au serveur WeChat", + "Well-Known URL": "URL bien connue", + "Well-Known URL 必须以 http:// 或 https:// 开头": "L'URL bien connue doit commencer par http:// ou https://", + "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示": "Clé de signature Webhook whsec_xxx, les informations sensibles ne sont pas affichées", + "Worker地址": "Adresse du Worker", + "Worker密钥": "Clé du Worker", + "一个月": "Un mois", + "一天": "Un jour", + "一小时": "Une heure", + "一次调用消耗多少刀,优先级大于模型倍率": "Combien de USD coûte un appel, priorité sur le ratio de modèle", + "一行一个,不区分大小写": "Un mot-clé par ligne, insensible à la casse", + "一行一个屏蔽词,不需要符号分割": "Un mot sensible par ligne, aucun symbole n'est requis", + "一键填充到 FluentRead": "Remplissage en un clic vers FluentRead", + "上一个表单块": "Bloc de formulaire précédent", + "上一步": "Précédent", + "上次保存: ": "Dernier enregistrement : ", + "上游倍率同步": "Synchronisation du ratio en amont", + "下一个表单块": "Bloc de formulaire suivant", + "下一步": "Suivant", + "下午好": "Bon après-midi", + "不再提醒": "Ne plus rappeler", + "不同用户分组的价格信息": "Informations sur les prix pour différents groupes d'utilisateurs", + "不填则为模型列表第一个": "Premier modèle de la liste si vide", + "不建议使用": "Non recommandé", + "不支持": "Non pris en charge", + "不是合法的 JSON 字符串": "N'est pas une chaîne JSON valide", + "不更改": "Ne pas changer", + "不限制": "Illimité", + "与本地相同": "Identique au local", + "专属倍率": "Ratio de groupe exclusif", + "两次输入的密码不一致": "Les deux mots de passe saisis ne correspondent pas", + "两次输入的密码不一致!": "Les mots de passe saisis deux fois sont incohérents !", + "两步验证": "Authentification à deux facteurs", + "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "L'authentification à deux facteurs (2FA) offre une protection de sécurité supplémentaire à votre compte. Après l'avoir activée, vous devez saisir votre mot de passe et le code de vérification généré par l'application d'authentification lorsque vous vous connectez.", + "两步验证启用成功!": "Authentification à deux facteurs activée avec succès !", + "两步验证已禁用": "L'authentification à deux facteurs a été désactivée", + "两步验证设置": "Paramètres d'authentification à deux facteurs", + "个": " individuel", + "个人中心": "Centre personnel", + "个人中心区域": "Zone du centre personnel", + "个人信息设置": "Paramètres des informations personnelles", + "个人设置": "Paramètres personnels", + "个性化设置": "Paramètres de personnalisation", + "个性化设置左侧边栏的显示内容": "Personnaliser le contenu affiché dans la barre latérale gauche", + "个未配置模型": "modèles non configurés", + "个模型": "modèles", + "中午好": "Bon midi", + "为一个 JSON 对象,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}": "Est un objet JSON, par exemple : {\"100\": 0,95, \"200\": 0,9, \"500\": 0,85}", + "为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]": "Est un tableau JSON, par exemple : [10, 20, 50, 100, 200, 500]", + "为一个 JSON 文本": "Est un texte JSON", + "为一个 JSON 文本,例如:": "Est un texte JSON, par exemple :", + "为一个 JSON 文本,键为分组名称,值为倍率": "Est un texte JSON, la clé est le nom du groupe, la valeur est le ratio", + "为一个 JSON 文本,键为分组名称,值为分组描述": "Est un texte JSON, la clé est le nom du groupe, la valeur est la description du groupe", + "为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 \"gpt-4-gizmo-*\": 0.1,一次消耗0.1刀": "Est un texte JSON, la clé est le nom du modèle, la valeur est le coût d'un appel en dollars, par exemple \"gpt-4-gizmo-*\" : 0,1, un appel coûte 0,1 dollar", + "为一个 JSON 文本,键为模型名称,值为倍率": "est un texte JSON, la clé est le nom du modèle et la valeur est le ratio", + "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-audio-preview\": 16}": "Un texte JSON avec le nom du modèle comme clé et le ratio comme valeur, par exemple : {\"gpt-4o-audio-preview\": 16}", + "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-realtime\": 2}": "Un texte JSON avec le nom du modèle comme clé et le ratio comme valeur, par exemple : {\"gpt-4o-realtime\": 2}", + "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-image-1\": 2}": "Un texte JSON avec le nom du modèle comme clé et le ratio comme valeur, par exemple : {\"gpt-image-1\": 2}", + "为一个 JSON 文本,键为组名称,值为倍率": "Est un texte JSON, la clé est le nom du groupe, la valeur est le ratio", + "为了保护账户安全,请验证您的两步验证码。": "Pour protéger la sécurité du compte, veuillez vérifier votre code d'authentification à deux facteurs.", + "为了保护账户安全,请验证您的身份。": "Pour protéger la sécurité de votre compte, veuillez vérifier votre identité.", + "为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https": "Si vide, l'adresse du serveur est utilisée par défaut, plusieurs Origines sont séparées par des virgules, par exemple https://newapi.pro,https://newapi.com, attention ne pas inclure [], utiliser https", + "主页链接填": "Remplir le lien de la page d'accueil", + "之前的所有日志": "Tous les journaux précédents", + "二步验证已重置": "L'authentification à deux facteurs a été réinitialisée", + "仅会覆盖你勾选的字段,未勾选的字段保持本地不变。": "Seuls les champs sélectionnés seront remplacés, les champs non sélectionnés restent inchangés.", + "仅供参考,以实际扣费为准": "Pour référence uniquement, la déduction réelle prévaudra", + "仅保存": "Enregistrer uniquement", + "仅修改展示粒度,统计精确到小时": "Modifier uniquement la granularité d'affichage, statistiques précises à l'heure près", + "仅密钥": "Clé uniquement", + "仅对自定义模型有效": "Uniquement efficace pour les modèles personnalisés", + "仅当自动禁用开启时有效,关闭后不会自动禁用该渠道": "Efficace uniquement lorsque la désactivation automatique est activée, après la fermeture, le canal ne sera pas automatiquement désactivé", + "仅支持": "Seulement prend en charge", + "仅支持 JSON 文件": "Seuls les fichiers JSON sont pris en charge", + "仅支持 JSON 文件,支持多文件": "Seuls les fichiers JSON sont pris en charge, plusieurs fichiers sont pris en charge", + "仅支持 OpenAI 接口格式": "Seul le format d'interface OpenAI est pris en charge", + "仅显示矛盾倍率": "Afficher uniquement les ratios contradictoires", + "仅用于开发环境,生产环境应使用 HTTPS": "Pour le développement uniquement, utilisez HTTPS en production", + "仅重置配置": "Réinitialiser uniquement la configuration", + "今日关闭": "Fermer aujourd'hui", + "从官方模型库同步": "Synchroniser depuis la bibliothèque de modèles officielle", + "从认证器应用中获取验证码,或使用备用码": "Obtenez le code de vérification à partir de l'application d'authentification ou utilisez un code de secours", + "从配置文件同步": "Synchroniser depuis un fichier de configuration", + "代理地址": "Adresse du proxy", + "代理设置": "Paramètres du proxy", + "代码已复制到剪贴板": "Le code a été copié dans le presse-papiers", + "令牌": "Jeton", + "令牌分组": "Regroupement de jetons", + "令牌分组,默认为用户的分组": "Groupe de jetons, par défaut le groupe de l'utilisateur", + "令牌创建成功,请在列表页面点击复制获取令牌!": "Jeton créé avec succès, veuillez cliquer sur copier sur la page de liste pour obtenir le jeton !", + "令牌名称": "Nom du jeton", + "令牌已重置并已复制到剪贴板": "Le jeton a été réinitialisé et copié dans le presse-papiers", + "令牌更新成功!": "Jeton mis à jour avec succès !", + "令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制": "Le quota du jeton est uniquement utilisé pour limiter l'utilisation maximale du quota du jeton lui-même, et l'utilisation réelle est limitée par le quota restant du compte", + "令牌管理": "Gestion des jetons", + "以下上游数据可能不可信:": "Les données en amont suivantes peuvent ne pas être fiables : ", + "以下文件解析失败,已忽略:{{list}}": "L'analyse des fichiers suivants a échoué, ignorés : {{list}}", + "以及": "et", + "仪表盘设置": "Paramètres du tableau de bord", + "价格": "Tarifs", + "价格:${{price}} * {{ratioType}}:{{ratio}}": "Prix : ${{price}} * {{ratioType}} : {{ratio}}", + "价格设置": "Paramètres de prix", + "价格设置方式": "Méthode de configuration des prix", + "任务 ID": "ID de la tâche", + "任务ID": "ID de la tâche", + "任务日志": "Journaux de tâches", + "任务状态": "Statut de la tâche", + "任务记录": "Enregistrements de tâches", + "企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选": "Les comptes d'entreprise ont un format de retour spécial et nécessitent un traitement particulier. Si ce n'est pas un compte d'entreprise, veuillez ne pas cocher cette case.", + "优先级": "Priorité", + "优惠": "Remise", + "低于此额度时将发送邮件提醒用户": "Un rappel par e-mail sera envoyé lorsque le quota tombera en dessous de ce seuil", + "余额": "Solde", + "余额充值管理": "Gestion de la recharge du solde", + "你似乎并没有修改什么": "Vous ne semblez rien avoir modifié", + "使用 GitHub 继续": "Continuer avec GitHub", + "使用 JSON 对象格式,格式为:{\"组名\": [最多请求次数, 最多请求完成次数]}": "Utiliser le format d'objet JSON, au format : {\"nom du groupe\": [nombre maximal de requêtes, nombre maximal d'achèvements de requêtes]}", + "使用 LinuxDO 继续": "Continuer avec LinuxDO", + "使用 OIDC 继续": "Continuer avec OIDC", + "使用 Passkey 实现免密且更安全的登录体验": "Utilisez Passkey pour une expérience de connexion sans mot de passe et plus sécurisée.", + "使用 Passkey 登录": "Se connecter avec Passkey", + "使用 Passkey 验证": "Vérifier avec Passkey", + "使用 微信 继续": "Continuer avec WeChat", + "使用 用户名 注册": "S'inscrire avec un nom d'utilisateur", + "使用 邮箱或用户名 登录": "Connectez-vous avec votre e-mail ou votre nom d'utilisateur", + "使用ID排序": "Trier par ID", + "使用日志": "Journaux d'utilisation", + "使用模式": "Mode d'utilisation", + "使用统计": "Statistiques d'utilisation", + "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "Utilisez une application d'authentification (telle que Google Authenticator, Microsoft Authenticator) pour scanner le code QR ci-dessous :", + "使用认证器应用扫描二维码": "Scanner le code QR avec l'application d'authentification", + "例如 €, £, Rp, ₩, ₹...": "Par exemple, €, £, Rp, ₩, ₹...", + "例如 https://docs.newapi.pro": "Par exemple, https://docs.newapi.pro", + "例如:": "Par exemple :", + "例如: socks5://user:pass@host:port": "par exemple : socks5://user:pass@host:port", + "例如:0001": "Par exemple : 0001", + "例如:1000": "Par exemple : 1000", + "例如:2,就是最低充值2$": "Par exemple : 2, c'est-à-dire un minimum de 2$ de recharge", + "例如:2000": "Par exemple : 2000", + "例如:7,就是7元/美金": "Par exemple : 7, c'est-à-dire 7 yuans/dollar", + "例如:example.com": "ex: example.com", + "例如:https://yourdomain.com": "Par exemple : https://yourdomain.com", + "例如:preview": "Par exemple : preview", + "例如发卡网站的购买链接": "Par exemple, lien d'achat sur un site d'émission de cartes", + "供应商": "Fournisseur", + "供应商介绍": "Présentation du fournisseur", + "供应商信息:": "Informations sur le fournisseur :", + "供应商创建成功!": "Fournisseur créé avec succès !", + "供应商删除成功": "Fournisseur supprimé avec succès", + "供应商名称": "Nom du fournisseur", + "供应商图标": "Icône du fournisseur", + "供应商更新成功!": "Fournisseur mis à jour avec succès !", + "侧边栏管理(全局控制)": "Gestion de la barre latérale (contrôle global)", + "侧边栏设置保存成功": "Paramètres de la barre latérale enregistrés avec succès", + "保存": "Enregistrer", + "保存 GitHub OAuth 设置": "Enregistrer les paramètres GitHub OAuth", + "保存 Linux DO OAuth 设置": "Enregistrer les paramètres Linux DO OAuth", + "保存 OIDC 设置": "Enregistrer les paramètres OIDC", + "保存 Passkey 设置": "Enregistrer les paramètres Passkey", + "保存 SMTP 设置": "Enregistrer les paramètres SMTP", + "保存 Telegram 登录设置": "Enregistrer les paramètres de connexion Telegram", + "保存 Turnstile 设置": "Enregistrer les paramètres Turnstile", + "保存 WeChat Server 设置": "Enregistrer les paramètres du serveur WeChat", + "保存分组倍率设置": "Enregistrer les paramètres de ratio de groupe", + "保存备用码": "Enregistrer les codes de sauvegarde", + "保存备用码以备不时之需": "Enregistrez les codes de sauvegarde pour les urgences", + "保存失败": "Échec de l'enregistrement", + "保存失败,请重试": "Échec de l'enregistrement, veuillez réessayer", + "保存失败:": "Échec de l'enregistrement :", + "保存屏蔽词过滤设置": "Enregistrer les paramètres de filtrage des mots sensibles", + "保存成功": "Enregistré avec succès", + "保存数据看板设置": "Enregistrer les paramètres du tableau de bord des données", + "保存日志设置": "Enregistrer les paramètres du journal", + "保存模型倍率设置": "Enregistrer les paramètres de ratio de modèle", + "保存模型速率限制": "Enregistrer les paramètres de limite de débit de modèle", + "保存监控设置": "Enregistrer les paramètres de surveillance", + "保存绘图设置": "Enregistrer les paramètres de dessin", + "保存聊天设置": "Enregistrer les paramètres de discussion", + "保存设置": "Enregistrer les paramètres", + "保存通用设置": "Enregistrer les paramètres généraux", + "保存邮箱域名白名单设置": "Enregistrer les paramètres de liste blanche des domaines de messagerie", + "保存额度设置": "Enregistrer les paramètres de quota", + "修复数据库一致性": "Réparer la cohérence de la base de données", + "修改为": "Modifier en", + "修改子渠道优先级": "Modifier la priorité du sous-canal", + "修改子渠道权重": "Modifier le poids du sous-canal", + "修改密码": "Changer le mot de passe", + "修改绑定": "Modifier la liaison", + "倍率": "Ratio", + "倍率信息": "Informations sur le ratio", + "倍率是为了方便换算不同价格的模型": "Le ratio sert à faciliter la conversion de modèles à des prix différents.", + "倍率模式": "Mode de ratio", + "倍率类型": "Type de ratio", + "停止测试": "Arrêter le test", + "允许 AccountFilter 参数": "Autoriser le paramètre AccountFilter", + "允许 HTTP 协议图片请求(适用于自部署代理)": "Autoriser les requêtes d'images via le protocole HTTP (applicable aux proxies auto-déployés)", + "允许 safety_identifier 透传": "Autoriser le passage de safety_identifier", + "允许 service_tier 透传": "Autoriser le passage de service_tier", + "允许 Turnstile 用户校验": "Autoriser la vérification des utilisateurs Turnstile", + "允许不安全的 Origin(HTTP)": "Autoriser une origine non sécurisée (HTTP)", + "允许回调(会泄露服务器 IP 地址)": "Autoriser le rappel (divulguera l'adresse IP du serveur)", + "允许在 Stripe 支付中输入促销码": "Autoriser la saisie de codes promotionnels lors du paiement Stripe", + "允许新用户注册": "Autoriser l'inscription de nouveaux utilisateurs", + "允许的 Origins": "Origines autorisées", + "允许的IP,一行一个,不填写则不限制": "Adresses IP autorisées, une par ligne, non remplies signifie aucune restriction", + "允许的端口": "Ports autorisés", + "允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)": "Autoriser l'accès aux adresses IP privées (127.0.0.1, 192.168.x.x et autres adresses de réseau interne)", + "允许通过 GitHub 账户登录 & 注册": "Autoriser la connexion & l'inscription via le compte GitHub", + "允许通过 Linux DO 账户登录 & 注册": "Autoriser la connexion & l'inscription via le compte Linux DO", + "允许通过 OIDC 进行登录": "Autoriser la connexion via OIDC", + "允许通过 Passkey 登录 & 认证": "Autoriser la connexion et l'authentification via Passkey", + "允许通过 Telegram 进行登录": "Autoriser la connexion via Telegram", + "允许通过密码进行注册": "Autoriser l'inscription via mot de passe", + "允许通过密码进行登录": "Autoriser la connexion via mot de passe", + "允许通过微信登录 & 注册": "Autoriser la connexion & l'inscription via WeChat", + "元": "CNY", + "充值": "Recharger", + "充值价格(x元/美金)": "Prix de recharge (x yuans/dollar)", + "充值价格显示": "Prix de recharge", + "充值分组倍率": "Ratio de groupe de recharge", + "充值分组倍率不是合法的 JSON 字符串": "Le ratio de groupe de recharge n'est pas une chaîne JSON valide", + "充值数量": "Quantité de recharge", + "充值数量,最低 ": "Quantité de recharge, minimum ", + "充值数量不能小于": "Le montant de la recharge ne peut pas être inférieur à", + "充值方式设置": "Paramètres de la méthode de recharge", + "充值方式设置不是合法的 JSON 字符串": "Les paramètres de la méthode de recharge ne sont pas une chaîne JSON valide", + "充值确认": "Confirmation de la recharge", + "充值账单": "Factures de recharge", + "充值金额折扣配置": "Configuration des remises sur le montant de recharge", + "充值金额折扣配置不是合法的 JSON 对象": "La configuration des remises sur le montant de recharge n'est pas un objet JSON valide", + "充值链接": "Lien de recharge", + "充值额度": "Quota de recharge", + "兑换人ID": "ID du demandeur", + "兑换成功!": "Échange réussi !", + "兑换码充值": "Recharge par code d'échange", + "兑换码创建成功": "Code d'échange créé", + "兑换码创建成功,是否下载兑换码?": "Code d'échange créé avec succès. Voulez-vous le télécharger ?", + "兑换码创建成功!": "Code d'échange créé avec succès !", + "兑换码将以文本文件的形式下载,文件名为兑换码的名称。": "Le code d'échange sera téléchargé sous forme de fichier texte, le nom de fichier étant le nom du code d'échange.", + "兑换码更新成功!": "Code d'échange mis à jour avec succès !", + "兑换码生成管理": "Gestion de la génération de codes d'échange", + "兑换码管理": "Gestion des codes d'échange", + "兑换额度": "Utiliser", + "全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用": "Contrôle global des zones et des fonctions de la barre latérale, les utilisateurs ne peuvent pas activer les fonctions masquées par les administrateurs", + "全局设置": "Paramètres globaux", + "全选": "Tout sélectionner", + "全部": "Tous", + "全部供应商": "Tous les fournisseurs", + "全部分组": "Tous les groupes", + "全部标签": "Toutes les étiquettes", + "全部模型": "Tous les modèles", + "全部状态": "Tous les statuts", + "全部端点": "Tous les points de terminaison", + "全部类型": "Tous les types", + "公告": "Annonce", + "公告内容": "Contenu de l'avis", + "公告已更新": "Avis mis à jour", + "公告更新失败": "Échec de la mise à jour de l'avis", + "公告类型": "Type d'avis", + "共": "Total", + "共 {{count}} 个密钥_one": "{{count}} clé au total", + "共 {{count}} 个密钥_many": "{{count}} clés au total", + "共 {{count}} 个密钥_other": "{{count}} clés au total", + "共 {{count}} 个模型": "{{count}} modèles", + "共 {{total}} 项,当前显示 {{start}}-{{end}} 项": "Total {{total}} éléments, affichage actuel {{start}}-{{end}} éléments", + "关": "Fermer", + "关于": "À propos", + "关于我们": "À propos de nous", + "关于系统的详细信息": "Informations détaillées sur le système", + "关于项目": "À propos du projet", + "关键字(id或者名称)": "Mot-clé (id ou nom)", + "关闭": "Fermer", + "关闭侧边栏": "Fermer la barre latérale", + "关闭公告": "Fermer l'avis", + "关闭后,此模型将不会被“同步官方”自动覆盖或创建": "Après fermeture, ce modèle ne sera pas automatiquement remplacé ou créé par \"Synchroniser depuis la bibliothèque de modèles officielle\"", + "关闭弹窗,已停止批量测试": "Fermer la fenêtre popup, le test par lots a été arrêté", + "其他": "Autre", + "其他注册选项": "Autres options d'inscription", + "其他登录选项": "Autres options de connexion", + "其他设置": "Autres paramètres", + "内容": "Contenu", + "内容较大,已启用性能优化模式": "Le contenu est volumineux, le mode d'optimisation des performances a été activé", + "内容较大,部分功能可能受限": "Le contenu est volumineux, certaines fonctionnalités peuvent être limitées", + "最低": "Le plus bas", + "最低充值美元数量": "Montant minimum de recharge en dollars", + "最后使用时间": "Dernière utilisation", + "最后请求": "Dernière requête", + "准备完成初始化": "Prêt à terminer l'initialisation", + "分类名称": "Nom de la catégorie", + "分组": "Groupe", + "分组与模型定价设置": "Paramètres de groupe et de tarification du modèle", + "分组价格": "Prix de groupe", + "分组倍率": "Ratio", + "分组倍率设置": "Paramètres de ratio de groupe", + "分组倍率设置,可以在此处新增分组或修改现有分组的倍率,格式为 JSON 字符串,例如:{\"vip\": 0.5, \"test\": 1},表示 vip 分组的倍率为 0.5,test 分组的倍率为 1": "Paramètres de ratio de groupe, vous pouvez ajouter de nouveaux groupes ou modifier le ratio des groupes existants ici, au format de chaîne JSON, par exemple : {\"vip\": 0,5, \"test\": 1}, ce qui signifie que le ratio du groupe vip est 0,5 et celui du groupe test est 1", + "分组特殊倍率": "Ratio spécial de groupe", + "分组设置": "Paramètres de groupe", + "分组速率配置优先级高于全局速率限制。": "La priorité de configuration du taux de groupe est supérieure à la limite de taux globale.", + "分组速率限制": "Limitation du taux de groupe", + "分钟": "minutes", + "切换为Assistant角色": "Basculer vers le rôle Assistant", + "切换为System角色": "Basculer vers le rôle Système", + "切换为单密钥模式": "Passer en mode clé unique", + "切换主题": "Changer de thème", + "划转到余额": "Transférer au solde", + "划转邀请额度": "Quota d'invitation de transfert", + "划转金额最低为": "Le montant minimum du virement est de", + "划转额度": "Montant du virement", + "列设置": "Paramètres de colonne", + "创建令牌默认选择auto分组,初始令牌也将设为auto(否则留空,为用户默认分组)": "Lors de la création d'un jeton, le groupe auto est sélectionné par défaut, et le jeton initial sera également défini sur auto (sinon laisser vide, pour le groupe par défaut de l'utilisateur)", + "创建失败": "Échec de la création", + "创建成功": "Création réussie", + "创建新用户账户": "Créer un nouveau compte utilisateur", + "创建新的令牌": "Créer un nouveau jeton", + "创建新的兑换码": "Créer un nouveau code d'échange", + "创建新的模型": "Créer un nouveau modèle", + "创建新的渠道": "Créer un nouveau canal", + "创建新的预填组": "Créer un nouveau groupe pré-rempli", + "创建时间": "Heure de création", + "创建用户": "Créer un utilisateur", + "初始化失败,请重试": "Échec de l'initialisation, veuillez réessayer", + "初始化系统": "Initialiser le système", + "删除": "Supprimer", + "删除失败": "Échec de la suppression", + "删除密钥失败": "Échec de la suppression de la clé", + "删除成功": "Supprimé avec succès", + "删除所选": "Supprimer la sélection", + "删除所选令牌": "Supprimer le jeton sélectionné", + "删除所选通道": "Supprimer les canaux sélectionnés", + "删除禁用密钥失败": "Échec de la suppression des clés désactivées", + "删除禁用通道": "Supprimer les canaux désactivés", + "删除自动禁用密钥": "Supprimer les clés désactivées automatiquement", + "删除账户": "Supprimer le compte", + "删除账户确认": "Confirmation de la suppression du compte", + "刷新": "Actualiser", + "刷新失败": "Échec de l'actualisation", + "前缀": "Préfixe", + "剩余备用码:": "Codes de sauvegarde restants : ", + "剩余额度": "Quota restant", + "剩余额度/总额度": "Restant/Total", + "剩余额度$": "Quota restant $", + "功能特性": "Fonctionnalités", + "加入预填组": "Rejoindre un groupe pré-rempli", + "加载中...": "Chargement...", + "加载供应商信息失败": "Échec du chargement des informations du fournisseur", + "加载关于内容失败...": "Échec du chargement du contenu À propos...", + "加载分组失败": "Échec du chargement du groupe", + "加载失败": "Échec du chargement", + "加载模型信息失败": "Échec du chargement des informations du modèle", + "加载模型失败": "Échec du chargement du modèle", + "加载用户协议内容失败...": "Échec du chargement du contenu de l'accord utilisateur...", + "加载账单失败": "Échec du chargement des factures", + "加载隐私政策内容失败...": "Échec du chargement du contenu de la politique de confidentialité...", + "包含": "Contient", + "包含来自未知或未标明供应商的AI模型,这些模型可能来自小型供应商或开源项目。": "Comprend des modèles d'IA de fournisseurs inconnus ou non marqués, qui peuvent provenir de petits fournisseurs ou de projets open-source.", + "包括失败请求的次数,0代表不限制": "Y compris les tentatives de requête échouées, 0 signifie aucune limite", + "匹配类型": "Type de correspondance", + "区域": "Région", + "历史消耗": "Consommation historique", + "原价": "Prix original", + "原因:": "Raison :", + "原密码": "Mot de passe original", + "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Doublons supprimés : {{before}} clés avant, {{after}} clés après", + "参与官方同步": "Participer à la synchronisation officielle", + "参数": "paramètre", + "参数值": "Valeur du paramètre", + "参数覆盖": "Remplacement des paramètres", + "参照生视频": "Générer une vidéo par référence", + "友情链接": "Liens amicaux", + "发布日期": "Date de publication", + "发布时间": "Heure de publication", + "取消": "Annuler", + "取消全选": "Annuler la sélection", + "变换": "Variation", + "变焦": "Zoom", + "只包括请求成功的次数": "N'inclure que les tentatives de requête réussies", + "只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求": "Seul HTTPS est pris en charge, le système enverra des notifications via POST, veuillez vous assurer que l'adresse peut recevoir des requêtes POST", + "只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录": "Ce n'est que lorsque l'utilisateur définit l'enregistrement IP que l'enregistrement IP des journaux de type requête et erreur sera effectué", + "可信": "Fiable", + "可在设置页面设置关于内容,支持 HTML & Markdown": "Le contenu \"À propos\" peut être défini sur la page des paramètres, prenant en charge HTML & Markdown", + "可用令牌分组": "Groupes de jetons disponibles", + "可用分组": "Groupes disponibles", + "可用模型": "Modèles disponibles", + "可用端点类型": "Types de points de terminaison pris en charge", + "可用邀请额度": "Quota d'invitation disponible", + "可视化": "Visualisation", + "可视化倍率设置": "Paramètres de ratio de modèle visuel", + "可视化编辑": "Édition visuelle", + "可选,公告的补充说明": "Facultatif, informations supplémentaires pour l'avis", + "可选值": "Valeur facultative", + "同时重置消息": "Réinitialiser également les messages", + "同步": "Synchroniser", + "同步向导": "Assistant de synchronisation", + "同步失败": "Échec de la synchronisation", + "同步成功": "Synchronisation réussie", + "同步接口": "Interface de synchronisation", + "名称": "Nom", + "名称+密钥": "Nom + clé", + "名称不能为空": "Le nom ne peut pas être vide", + "名称匹配类型": "Type de correspondance de nom", + "后端请求失败": "Échec de la requête du backend", + "后缀": "Suffixe", + "否": "Non", + "启动时间": "Heure de démarrage", + "启用": "Activer", + "启用 Prompt 检查": "Activer la vérification de l'invite", + "启用2FA失败": "Échec de l'activation de 2FA", + "启用Claude思考适配(-thinking后缀)": "Activer l'adaptation de la pensée Claude (suffixe -thinking)", + "启用Gemini思考后缀适配": "Activer l'adaptation du suffixe de la pensée Gemini", + "启用Ping间隔": "Activer l'intervalle de ping", + "启用SMTP SSL": "Activer SMTP SSL", + "启用SSRF防护(推荐开启以保护服务器安全)": "Activer la protection SSRF (recommandé pour la sécurité du serveur)", + "启用全部": "Activer tout", + "启用密钥失败": "Échec de l'activation de la clé", + "启用屏蔽词过滤功能": "Activer la fonction de filtrage des mots sensibles", + "启用所有密钥失败": "Échec de l'activation de toutes les clés", + "启用数据看板(实验性)": "Activer le tableau de bord des données (expérimental)", + "启用用户模型请求速率限制(可能会影响高并发性能)": "Activer la limite de débit de requête de modèle utilisateur (peut affecter les performances à haute concurrence)", + "启用绘图功能": "Activer la fonction de dessin", + "启用请求体透传功能": "Activer la fonctionnalité de transmission du corps de la requête", + "启用请求透传": "Activer la transmission de la requête", + "启用额度消费日志记录": "Activer la journalisation de la consommation de quota", + "启用验证": "Activer l'authentification", + "周": "semaine", + "和": "et", + "响应": "Réponse", + "响应时间": "Temps de réponse", + "商品价格 ID": "ID du prix du produit", + "回答内容": "Contenu de la réponse", + "回调 URL 填": "Remplir l'URL de rappel", + "回调地址": "Adresse de rappel", + "固定价格": "Prix fixe", + "固定价格(每次)": "Prix fixe (par utilisation)", + "固定价格值": "Valeur de prix fixe", + "图像生成": "Génération d'images", + "图标": "Icône", + "图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ": "L'icône utilise la bibliothèque @lobehub/icons, telle que : OpenAI, Claude.Color, prend en charge les paramètres de chaîne : OpenAI.Avatar.type={'platform'}, OpenRouter.Avatar.shape={'square'}, interroger toutes les icônes disponibles s'il vous plaît ", + "图混合": "Mélanger", + "图片生成调用:{{symbol}}{{price}} / 1次": "Appel de génération d'image : {{symbol}}{{price}} / 1 fois", + "图片输入: {{imageRatio}}": "Entrée d'image : {{imageRatio}}", + "图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})": "Prix d'entrée d'image : {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (ratio d'image : {{imageRatio}})", + "图片输入倍率(仅部分模型支持该计费)": "Ratio d'entrée d'image (seulement certains modèles prennent en charge cette facturation)", + "图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费": "Paramètres de ratio liés à l'entrée d'image, la clé est le nom du modèle, la valeur est le ratio, seulement certains modèles prennent en charge cette facturation", + "图生文": "Décrire", + "图生视频": "Générer une vidéo à partir d'une image", + "在Gotify服务器创建应用后获得的令牌,用于发送通知": "Jeton obtenu après la création d'une application sur le serveur Gotify, utilisé pour envoyer des notifications", + "在Gotify服务器的应用管理中创建新应用": "Créer une nouvelle application dans la gestion des applications du serveur Gotify", + "在找兑换码?": "Vous cherchez un code d'échange ? ", + "在此输入 Logo 图片地址": "Saisissez l'URL de l'image du logo ici", + "在此输入新的公告内容,支持 Markdown & HTML 代码": "Saisissez le nouveau contenu de l'annonce ici, prend en charge le code Markdown & HTML", + "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面": "Saisissez le nouveau contenu \"À propos\" ici, prend en charge Markdown", + "在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码": "Saisissez le nouveau pied de page ici, laissez vide pour utiliser le pied de page par défaut, prend en charge le code HTML.", + "在此输入用户协议内容,支持 Markdown & HTML 代码": "Saisissez le contenu de l'accord utilisateur ici, prend en charge le code Markdown & HTML", + "在此输入系统名称": "Saisissez le nom du système ici", + "在此输入隐私政策内容,支持 Markdown & HTML 代码": "Saisissez le contenu de la politique de confidentialité ici, prend en charge le code Markdown & HTML", + "在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "Saisissez le contenu de la page d'accueil ici, prend en charge Markdown & HTML. Après configuration, les informations d'état de la page d'accueil ne seront plus affichées. Si un lien est saisi, il sera utilisé comme attribut src de l'iframe, ce qui vous permet de définir n'importe quelle page web comme page d'accueil", + "域名IP过滤详细说明": "⚠️ Il s'agit d'une option expérimentale. Un domaine peut se résoudre en plusieurs adresses IPv4/IPv6. Si cette option est activée, assurez-vous que la liste de filtres IP couvre ces adresses, sinon l'accès peut échouer.", + "域名白名单": "Liste blanche de domaines", + "域名黑名单": "Liste noire de domaines", + "基本信息": "Informations de base", + "填入": "Remplir", + "填入所有模型": "Remplir tous les modèles", + "填入模板": "Remplir le modèle", + "填入相关模型": "Remplir les modèles associés", + "填写Gotify服务器的完整URL地址": "Remplir l'adresse URL complète du serveur Gotify", + "填写带https的域名,逗号分隔": "Saisir les domaines avec https, séparés par des virgules", + "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "Après avoir rempli le contenu de l'accord utilisateur, les utilisateurs devront cocher avoir lu l'accord utilisateur lors de l'inscription", + "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "Après avoir rempli le contenu de la politique de confidentialité, les utilisateurs devront cocher avoir lu la politique de confidentialité lors de l'inscription", + "备份支持": "Prise en charge de la sauvegarde", + "备份状态": "État de la sauvegarde", + "备注": "Remarque", + "备用恢复代码": "Codes de récupération de sauvegarde", + "备用码已复制到剪贴板": "Codes de sauvegarde copiés dans le presse-papiers", + "备用码重新生成成功": "Codes de sauvegarde régénérés avec succès", + "复制": "Copier", + "复制代码": "Copier le code", + "复制令牌": "Copier le Jeton", + "复制全部": "Tout copier", + "复制名称": "Copier le nom", + "复制失败": "Échec de la copie", + "复制失败,请手动复制": "Échec de la copie, veuillez copier manuellement", + "复制已选": "Copier la sélection", + "复制应用的令牌(Token)并填写到上方的应用令牌字段": "Copier le jeton de l'application et le remplir dans le champ de jeton d'application ci-dessus", + "复制成功": "Copié avec succès", + "复制所有代码": "Copier tous les codes", + "复制所有模型": "Copier tous les modèles", + "复制所选令牌": "Copier le jeton sélectionné", + "复制所选兑换码到剪贴板": "Copier les codes d'échange sélectionnés dans le presse-papiers", + "复制渠道的所有信息": "Copier toutes les informations d'un canal", + "外接设备": "Périphériques externes", + "多密钥渠道操作项目组": "Groupe d'opérations de canal multi-clés", + "多密钥管理": "Gestion multi-clés", + "多种充值方式,安全便捷": "Plusieurs méthodes de recharge, sûres et pratiques", + "天": "Jour", + "天前": "il y a des jours", + "失败": "Échec", + "失败原因": "Raison de l'échec", + "失败时自动禁用通道": "Désactiver automatiquement le canal en cas d'échec", + "失败重试次数": "Nombre de tentatives en cas d'échec", + "奖励说明": "Description de la récompense", + "如:大带宽批量分析图片推荐": "par exemple, Recommandations d'analyse d'images par lots à large bande passante", + "如:香港线路": "par exemple, Ligne de Hong Kong", + "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "Si vous vous connectez à des projets de redirection One API ou New API en amont, veuillez utiliser le type OpenAI. N'utilisez pas ce type, sauf si vous savez ce que vous faites.", + "如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "Si la requête de l'utilisateur contient un prompt système, utilisez ce paramètre pour le concaténer avant le prompt système de l'utilisateur", + "始终使用浅色主题": "Toujours utiliser le thème clair", + "始终使用深色主题": "Toujours utiliser le thème sombre", + "字段透传控制": "Contrôle du passage des champs", + "存在重复的键名:": "Il existe des noms de clés en double :", + "安全提醒": "Rappel de sécurité", + "安全设置": "Paramètres de sécurité", + "安全验证": "Vérification de sécurité", + "安全验证级别": "Niveau de vérification de la sécurité", + "安装指南": "Guide d'installation", + "完成": "Terminé", + "完成初始化": "Terminer l'initialisation", + "完成设置并启用两步验证": "Terminer la configuration et activer l'authentification à deux facteurs", + "完整的 Base URL,支持变量{model}": "URL de base complète, prend en charge la variable {model}", + "官方": "Officiel", + "官方文档": "Documentation officielle", + "官方模型同步": "Synchronisation des modèles officiels", + "定价模式": "Mode de tarification", + "定时测试所有通道": "Tester périodiquement tous les canaux", + "定期更改密码可以提高账户安全性": "Changer régulièrement votre mot de passe peut améliorer la sécurité de votre compte", + "实付": "Paiement réel", + "实付金额": "Montant du paiement réel", + "实付金额:": "Montant du paiement réel : ", + "实际模型": "Modèle réel", + "实际请求体": "Corps de requête réel", + "密码": "Mot de passe", + "密码修改成功!": "Mot de passe changé avec succès !", + "密码已复制到剪贴板:": "Le mot de passe a été copié dans le presse-papiers : ", + "密码已重置并已复制到剪贴板:": "Le mot de passe a été réinitialisé et copié dans le presse-papiers : ", + "密码管理": "Gestion des mots de passe", + "密码重置": "Réinitialisation du mot de passe", + "密码重置完成": "Réinitialisation du mot de passe terminée", + "密码重置确认": "Confirmation de la réinitialisation du mot de passe", + "密码长度至少为8个字符": "Le mot de passe doit comporter au moins 8 caractères", + "密钥": "Clé API", + "密钥(编辑模式下,保存的密钥不会显示)": "Clé (en mode édition, les clés enregistrées ne sont pas affichées)", + "密钥去重": "Suppression des doublons de clés", + "密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性": "La clé sera ajoutée à l'en-tête de la requête en tant que Bearer pour vérifier la légitimité de la requête webhook", + "密钥已删除": "La clé a été supprimée", + "密钥已启用": "La clé a été activée", + "密钥已复制到剪贴板": "Clé copiée dans le presse-papiers", + "密钥已禁用": "La clé a été désactivée", + "密钥文件 (.json)": "Fichier de clé (.json)", + "密钥更新模式": "Mode de mise à jour de la clé", + "密钥格式": "Format de la clé", + "密钥格式无效,请输入有效的 JSON 格式密钥": "Format de clé invalide, veuillez saisir une clé au format JSON valide", + "密钥聚合模式": "Mode d'agrégation de clés", + "密钥获取成功": "Acquisition de la clé réussie", + "密钥输入方式": "Méthode de saisie de la clé", + "密钥预览": "Aperçu de la clé", + "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "Pour les canaux officiels, le new-api a une adresse intégrée. Sauf s'il s'agit d'un site proxy tiers ou d'une adresse d'accès Azure spéciale, il n'est pas nécessaire de la remplir", + "对域名启用 IP 过滤(实验性)": "Activer le filtrage IP pour les domaines (expérimental)", + "对外运营模式": "Mode par défaut", + "导入": "Importer", + "导入的配置将覆盖当前设置,是否继续?": "La configuration importée remplacera les paramètres actuels. Continuer ?", + "导入配置": "Importer la configuration", + "导入配置失败: ": "Échec de l'importation de la configuration : ", + "导出": "Exporter", + "导出配置": "Exporter la configuration", + "导出配置失败: ": "Échec de l'exportation de la configuration : ", + "将 reasoning_content 转换为 标签拼接到内容中": "Convertir reasoning_content en balises et les ajouter au contenu", + "将为选中的 ": "Définira pour la sélection ", + "将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "Seul le premier fichier de clé sera conservé, et les fichiers restants seront supprimés. Continuer ?", + "将删除": "Supprimera", + "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "Cela supprimera tous les codes d'échange utilisés, désactivés et expirés, cette opération ne peut pas être annulée.", + "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "Effacera toutes les configurations enregistrées et rétablira les paramètres par défaut. Cette opération ne peut pas être annulée. Continuer ?", + "将清除选定时间之前的所有日志": "Effacera tous les journaux avant l'heure sélectionnée", + "小时": "Heure", + "尚未使用": "Pas encore utilisé", + "局部重绘-提交": "Varier la région", + "屏蔽词列表": "Liste des mots sensibles", + "屏蔽词过滤设置": "Paramètres de filtrage des mots sensibles", + "展开": "Développer", + "展开更多": "Développer plus", + "左侧边栏个人设置": "Paramètres personnels de la barre latérale gauche", + "已为 {{count}} 个模型设置{{type}}_one": "{{type}} défini pour {{count}} modèle", + "已为 {{count}} 个模型设置{{type}}_many": "{{type}} défini pour {{count}} modèles", + "已为 {{count}} 个模型设置{{type}}_other": "{{type}} défini pour {{count}} modèles", + "已为 ${count} 个渠道设置标签!": "Étiquettes définies pour ${count} canaux !", + "已修复 ${success} 个通道,失败 ${fails} 个通道。": "${success} canaux réparés, ${fails} canaux en échec.", + "已停止批量测试": "Test par lots arrêté", + "已关闭后续提醒": "Rappels suivants désactivés", + "已切换为Assistant角色": "Basculé vers le rôle Assistant", + "已切换为System角色": "Basculé vers le rôle Système", + "已切换至最优倍率视图,每个模型使用其最低倍率分组": "Passé à la vue de ratio optimal, chaque modèle utilise son groupe de ratio le plus bas", + "已初始化": "Initialisé", + "已删除 {{count}} 个令牌!": "Supprimé {{count}} jetons !", + "已删除 {{count}} 条失效兑换码_one": "{{count}} code d'échange invalide supprimé", + "已删除 {{count}} 条失效兑换码_many": "{{count}} codes d'échange invalides supprimés", + "已删除 {{count}} 条失效兑换码_other": "{{count}} codes d'échange invalides supprimés", + "已删除 ${data} 个通道!": "${data} canaux supprimés !", + "已删除所有禁用渠道,共计 ${data} 个": "Tous les canaux désactivés ont été supprimés, au total ${data}", + "已删除消息及其回复": "Message et ses réponses supprimés", + "已发送到 Fluent": "Envoyé à Fluent", + "已取消 Passkey 注册": "Enregistrement du Passkey annulé", + "已启用": "Activé", + "已启用 Passkey,无需密码即可登录": "Passkey activé. Connexion sans mot de passe disponible.", + "已启用所有密钥": "Toutes les clés ont été activées", + "已备份": "Sauvegardé", + "已复制": "Copié", + "已复制 ${count} 个模型": "${count} modèles copiés", + "已复制:": "Copié :", + "已复制:{{name}}": "Copié : {{name}}", + "已复制到剪切板": "Copié dans le presse-papiers", + "已复制到剪贴板": "Copié dans le presse-papiers", + "已复制到剪贴板!": "Copié dans le presse-papiers !", + "已复制模型名称": "Nom du modèle copié", + "已成功开始测试所有已启用通道,请刷新页面查看结果。": "Le test de tous les canaux activés a démarré avec succès. Veuillez actualiser la page pour voir les résultats.", + "已提交": "Soumis", + "已新增 {{count}} 个模型:{{list}}_one": "{{count}} nouveau modèle ajouté : {{list}}", + "已新增 {{count}} 个模型:{{list}}_many": "{{count}} nouveaux modèles ajoutés : {{list}}", + "已新增 {{count}} 个模型:{{list}}_other": "{{count}} nouveaux modèles ajoutés : {{list}}", + "已更新完毕所有已启用通道余额!": "Le quota de tous les canaux activés a été mis à jour !", + "已有保存的配置": "Configuration enregistrée existante", + "已有的模型": "Modèles existants", + "已有账户?": "Vous avez déjà un compte ?", + "已注销": "Déconnecté", + "已添加到白名单": "Ajouté à la liste blanche", + "已清空测试结果": "Résultats de test effacés", + "已用/剩余": "Utilisé/Restant", + "已用额度": "Quota utilisé", + "已禁用": "Désactivé", + "已禁用所有密钥": "Toutes les clés ont été désactivées", + "已绑定": "Lié", + "已绑定渠道": "Canaux liés", + "已耗尽": "Épuisé", + "已过期": "Expiré", + "已选择 {{count}} 个模型_one": "{{count}} modèle sélectionné", + "已选择 {{count}} 个模型_many": "{{count}} modèles sélectionnés", + "已选择 {{count}} 个模型_other": "{{count}} modèles sélectionnés", + "已选择 {{selected}} / {{total}}": "{{selected}} / {{total}} sélectionnés", + "已选择 ${count} 个渠道": "${count} canaux sélectionnés", + "已重置为默认配置": "Réinitialisé à la configuration par défaut", + "常见问答": "FAQ", + "常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)": "Gestion de la FAQ, fournissant des réponses aux questions courantes des utilisateurs (maximum 50, afficher les 20 dernières sur le front-end)", + "平台": "plateforme", + "平均RPM": "RPM moyen", + "平均TPM": "TPM moyen", + "平移": "Panoramique", + "应用同步": "Appliquer la synchronisation", + "应用更改": "Appliquer les modifications", + "应用覆盖": "Appliquer le remplacement", + "建立连接时发生错误": "Erreur lors de l'établissement de la connexion", + "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "Il est recommandé d'utiliser les bases de données MySQL ou PostgreSQL dans les environnements de production, ou de s'assurer que le fichier de base de données SQLite est mappé sur le stockage persistant de la machine hôte.", + "开": "Ouvert", + "开启之后会清除用户提示词中的": "Après l'activation, l'invite de l'utilisateur sera effacée", + "开启之后将上游地址替换为服务器地址": "Après l'activation, l'adresse en amont sera remplacée par l'adresse du serveur", + "开启后,仅\"消费\"和\"错误\"日志将记录您的客户端IP地址": "Après l'activation, seuls les journaux \"consommation\" et \"erreur\" enregistreront votre adresse IP client", + "开启后,将定期发送ping数据保持连接活跃": "Après activation, des données ping seront envoyées périodiquement pour maintenir la connexion active", + "开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启": "Après activation, toutes les requêtes seront directement transmises en amont sans aucun traitement (la redirection et l'adaptation de canal seront également désactivées), veuillez activer avec prudence", + "开启后不限制:必须设置模型倍率": "Après l'activation, aucune limite : le ratio de modèle doit être défini", + "开启后未登录用户无法访问模型广场": "Lorsqu'il est activé, les utilisateurs non authentifiés ne peuvent pas accéder à la place du marché des modèles", + "开启批量操作": "Activer la sélection par lots", + "开始同步": "Démarrer la synchronisation", + "开始批量测试 ${count} 个模型,已清空上次结果...": "Démarrage du test par lots de ${count} modèles, résultats précédents effacés...", + "开始时间": "heure de début", + "弱变换": "Faible variation", + "强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)": "Forcer le formatage des réponses au format standard OpenAI (uniquement pour les types de canaux OpenAI)", + "强制格式化": "Forcer le format", + "强制要求": "Exigence obligatoire", + "强变换": "Forte variation", + "当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道": "Lorsque le canal en amont renvoie une erreur contenant ces mots-clés (insensible à la casse), désactivez automatiquement le canal", + "当前余额": "Solde actuel", + "当前值": "Valeur actuelle", + "当前分组为 auto,会自动选择最优分组,当一个组不可用时自动降级到下一个组(熔断机制)": "Le groupe actuel est auto, il sélectionnera automatiquement le groupe optimal et passera automatiquement au groupe suivant lorsqu'un groupe n'est pas disponible (mécanisme de disjoncteur)", + "当前时间": "Heure actuelle", + "当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。": "Le rappel Midjourney actuel n'est pas activé, certains projets peuvent ne pas être en mesure d'obtenir des résultats de dessin, qui peuvent être activés dans les paramètres de fonctionnement.", + "当前查看的分组为:{{group}},倍率为:{{ratio}}": "Groupe actuel : {{group}}, ratio : {{ratio}}", + "当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。": "La liste de modèles actuelle est la plus longue liste de modèles de canal sous cette étiquette, pas l'union de tous les canaux. Veuillez noter que cela peut entraîner la perte de certains modèles de canal.", + "当前版本": "Version actuelle", + "当前计费": "Facturation actuelle", + "当前设备不支持 Passkey": "Passkey n'est pas pris en charge sur cet appareil", + "当前设置类型: ": "Type de paramètre actuel : ", + "当前跟随系统": "Suit actuellement le système", + "当剩余额度低于此数值时,系统将通过选择的方式发送通知": "Lorsque le quota restant est inférieur à cette valeur, le système enverra une notification via la méthode sélectionnée", + "当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用": "Acceptez les appels même si le modèle n'a pas de prix défini, utilisez uniquement lorsque vous faites confiance au site Web, ce qui peut entraîner des coûts élevés", + "当运行通道全部测试时,超过此时间将自动禁用通道": "Lors de l'exécution de tous les tests de canaux, le canal sera automatiquement désactivé lorsque ce temps sera dépassé", + "待使用收益": "Produits à utiliser", + "微信": "WeChat", + "微信公众号二维码图片链接": "Lien de l'image du code QR du compte public WeChat", + "微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "Scannez le code QR WeChat pour suivre le compte officiel, entrez \"code de vérification\" pour obtenir le code (valide 3 minutes)", + "微信扫码登录": "Scanner le code WeChat pour vous connecter", + "微信账户绑定成功!": "Compte WeChat lié avec succès !", + "必须是有效的 JSON 字符串数组,例如:[\"g1\",\"g2\"]": "Doit être un tableau de chaînes JSON valide, par exemple : [\"g1\",\"g2\"]", + "忘记密码?": "Mot de passe oublié ?", + "快速开始": "Démarrage rapide", + "思考中...": "Réflexion en cours...", + "思考内容转换": "Conversion du contenu de la pensée", + "思考过程": "Processus de réflexion", + "思考适配 BudgetTokens 百分比": "Adaptation de la pensée BudgetTokens pourcentage", + "思考预算占比": "Ratio du budget de la pensée", + "性能指标": "Indicateurs de performance", + "总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}": "Prix total : prix du texte {{textPrice}} + prix de l'audio {{audioPrice}} = {{symbol}}{{total}}", + "总密钥数": "Nombre total de clés", + "总收益": "revenu total", + "总计": "Total", + "总额度": "Quota total", + "您可以个性化设置侧边栏的要显示功能": "Vous pouvez personnaliser les fonctions de la barre latérale à afficher", + "您无权访问此页面,请联系管理员": "Vous n'êtes pas autorisé à accéder à cette page. Veuillez contacter l'administrateur.", + "您正在使用 MySQL 数据库。MySQL 是一个可靠的关系型数据库管理系统,适合生产环境使用。": "Vous utilisez la base de données MySQL. MySQL est un système de gestion de base de données relationnelle fiable, adapté aux environnements de production.", + "您正在使用 PostgreSQL 数据库。PostgreSQL 是一个功能强大的开源关系型数据库系统,提供了出色的可靠性和数据完整性,适合生产环境使用。": "Vous utilisez la base de données PostgreSQL. PostgreSQL est un système de base de données relationnelle open-source puissant qui offre une excellente fiabilité et intégrité des données, adapté aux environnements de production.", + "您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!": "Vous utilisez la base de données SQLite. Si vous exécutez dans un environnement de conteneur, veuillez vous assurer que le mappage de persistance du fichier de base de données est correctement défini, sinon toutes les données seront perdues après le redémarrage du conteneur !", + "您正在删除自己的帐户,将清空所有数据且不可恢复": "Vous êtes sur le point de supprimer votre compte. Toutes les données seront effacées et ne pourront pas être récupérées.", + "您的数据将安全地存储在本地计算机上。所有配置、用户信息和使用记录都会自动保存,关闭应用后不会丢失。": "Vos données seront stockées en toute sécurité sur votre ordinateur local. Toutes les configurations, informations utilisateur et historiques d'utilisation seront automatiquement sauvegardés et ne seront pas perdus après la fermeture de l'application.", + "您确定要取消密码登录功能吗?这可能会影响用户的登录方式。": "Êtes-vous sûr de vouloir annuler la fonction de connexion par mot de passe ? Cela pourrait affecter les méthodes de connexion des utilisateurs.", + "您需要先启用两步验证或 Passkey 才能执行此操作": "Vous devez d'abord activer l'authentification à deux facteurs ou Passkey pour effectuer cette opération", + "您需要先启用两步验证或 Passkey 才能查看敏感信息。": "Vous devez d'abord activer l'authentification à deux facteurs ou Passkey pour afficher les informations sensibles.", + "想起来了?": "Vous vous souvenez ?", + "成功": "Succès", + "成功兑换额度:": "Montant de l'échange réussi :", + "成功时自动启用通道": "Activer le canal en cas de succès", + "我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "J'ai compris que la désactivation de l'authentification à deux facteurs supprimera définitivement tous les paramètres et codes de sauvegarde associés, cette opération ne peut pas être annulée", + "我已阅读并同意": "J'ai lu et j'accepte", + "或": "Ou", + "或其兼容new-api-worker格式的其他版本": "ou d'autres versions compatibles avec le format new-api-worker", + "或手动输入密钥:": "Ou saisissez manuellement le secret :", + "所有上游数据均可信": "Toutes les données en amont sont fiables", + "所有密钥已复制到剪贴板": "Toutes les clés ont été copiées dans le presse-papiers", + "所有编辑均为覆盖操作,留空则不更改": "Toutes les modifications sont des opérations de remplacement, laisser vide ne changera rien", + "手动禁用": "Désactivé manuellement", + "手动编辑": "Modification manuelle", + "手动输入": "Saisie manuelle", + "打开侧边栏": "Ouvrir la barre latérale", + "执行中": "En cours", + "扫描二维码": "Scanner le code QR", + "批量创建": "Création par lots", + "批量创建时会在名称后自动添加随机后缀": "Lors de la création par lots, un suffixe aléatoire sera automatiquement ajouté au nom", + "批量创建模式下仅支持文件上传,不支持手动输入": "En mode création par lots, seul le téléchargement de fichiers est pris en charge, la saisie manuelle n'est pas prise en charge", + "批量删除": "Supprimer par lots", + "批量删除令牌": "Supprimer le jeton par lots", + "批量删除失败": "Échec de la suppression par lots", + "批量删除模型": "Supprimer les modèles par lots", + "批量操作": "Opérations par lots", + "批量测试${count}个模型": "Tester par lots ${count} modèles", + "批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}": "Test par lots terminé ! Succès : ${success}, Échec : ${fail}, Total : ${total}", + "批量测试已停止": "Le test par lots a été arrêté", + "批量测试过程中发生错误: ": "Une erreur s'est produite pendant le test par lots : ", + "批量设置": "Paramétrage par lots", + "批量设置成功": "Paramétrage par lots réussi", + "批量设置标签": "Définir l'étiquette par lots", + "批量设置模型参数": "Paramètres de modèle par lots", + "折": "% de réduction", + "按K显示单位": "Afficher en K", + "按价格设置": "Définir par prix", + "按倍率类型筛选": "Filtrer par type de ratio", + "按倍率设置": "Définir par ratio", + "按次计费": "Paiement à la séance", + "按量计费": "Paiement à l'utilisation", + "按顺序替换content中的变量占位符": "Remplacer les espaces réservés de variable dans le contenu dans l'ordre", + "换脸": "Remplacement de visage", + "授权,需在遵守": " et doit être utilisé conformément au ", + "授权失败": "Échec de l'autorisation", + "排队中": "En file d'attente", + "接受未设置价格模型": "Accepter les modèles sans prix défini", + "接口凭证": "Informations d'identification de l'interface", + "控制台": "Console", + "控制台区域": "Zone de la console", + "控制顶栏模块显示状态,全局生效": "Contrôler l'état d'affichage du module d'en-tête, effet global", + "推荐:用户可以选择是否使用指纹等验证": "Recommandé : les utilisateurs peuvent choisir d'utiliser ou non la vérification par empreinte digitale", + "推荐使用(用户可选)": "Recommandé (optionnel pour l'utilisateur)", + "描述": "Description", + "提交": "Soumettre", + "提交时间": "Heure de soumission", + "提交结果": "Résultats", + "提升": "Promouvoir", + "提示": "Invite", + "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Invite {{input}} tokens / 1M tokens * {{symbol}}{{price}} + Complétion {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Invite {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + Cache {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + Création de cache {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + Complétion {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "提示:如需备份数据,只需复制上述目录即可": "Astuce : pour sauvegarder les données, il suffit de copier le répertoire ci-dessus", + "提示:链接中的{key}将被替换为API密钥,{address}将被替换为服务器地址": "Astuce : {key} dans le lien sera remplacé par la clé API, {address} sera remplacé par l'adresse du serveur", + "提示价格:{{symbol}}{{price}} / 1M tokens": "Prix d'invite : {{symbol}}{{price}} / 1M tokens", + "提示缓存倍率": "Ratio de cache d'invite", + "搜索供应商": "Rechercher un fournisseur", + "搜索关键字": "Rechercher des mots-clés", + "搜索无结果": "Aucun résultat trouvé", + "搜索条件": "Conditions de recherche", + "搜索模型": "Rechercher des modèles", + "搜索模型...": "Rechercher des modèles...", + "搜索模型名称": "Rechercher un nom de modèle", + "搜索模型失败": "Échec de la recherche de modèles", + "搜索渠道名称或地址": "Rechercher un nom ou une adresse de canal", + "搜索聊天应用名称": "Rechercher le nom de l'application de chat", + "操作": "Actions", + "操作失败": "Opération échouée", + "操作失败,请重试": "L'opération a échoué, veuillez réessayer", + "操作成功完成!": "Opération terminée avec succès !", + "操作暂时被禁用": "Opération temporairement désactivée", + "操练场": "Terrain de jeu", + "操练场和聊天功能": "Terrain de jeu et fonctions de discussion", + "支付地址": "Adresse de paiement", + "支付宝": "Alipay", + "支付方式": "Mode de paiement", + "支付设置": "Paramètres de paiement", + "支付请求失败": "Échec de la demande de paiement", + "支付金额": "Montant payé", + "支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。": "Prend en charge le code de vérification TOTP à 6 chiffres ou le code de sauvegarde à 8 chiffres, peut être configuré ou consulté dans `Paramètres personnels - Paramètres de sécurité - Paramètres d'authentification à deux facteurs`.", + "支持CIDR格式,如:8.8.8.8, 192.168.1.0/24": "Prend en charge le format CIDR, par exemple : 8.8.8.8, 192.168.1.0/24", + "支持HTTP和HTTPS,填写Gotify服务器的完整URL地址": "Prend en charge HTTP et HTTPS, saisissez l'URL complète du serveur Gotify", + "支持HTTP和HTTPS,模板变量: {{title}} (通知标题), {{content}} (通知内容)": "Prend en charge HTTP et HTTPS, variables de modèle : {{title}} (titre de la notification), {{content}} (contenu de la notification)", + "支持众多的大模型供应商": "Prise en charge de divers fournisseurs de LLM", + "支持单个端口和端口范围,如:80, 443, 8000-8999": "Prend en charge les ports uniques et les plages de ports, par exemple : 80, 443, 8000-8999", + "支持变量:": "Variables prises en charge :", + "支持备份": "Pris en charge", + "支持搜索用户的 ID、用户名、显示名称和邮箱地址": "Prise en charge de la recherche par ID utilisateur, nom d'utilisateur, nom d'affichage et adresse e-mail", + "支持的图像模型": "Modèles d'image pris en charge", + "支持通配符格式,如:example.com, *.api.example.com": "Prend en charge le format générique, par exemple : example.com, *.api.example.com", + "收益": "Gains", + "收益统计": "Statistiques sur les revenus", + "收起": "Réduire", + "收起侧边栏": "Réduire la barre latérale", + "收起内容": "Réduire le contenu", + "放大": "Upscalers", + "放大编辑": "Développer l'éditeur", + "敏感信息不会发送到前端显示": "Les informations sensibles ne seront pas affichées dans le frontend", + "数据存储位置:": "Emplacement de stockage des données :", + "数据库信息": "Informations sur la base de données", + "数据库检查": "Vérification de la base de données", + "数据库类型": "Type de base de données", + "数据库警告": "Avertissement de la base de données", + "数据格式错误": "Erreur de format de données", + "数据看板": "Tableau de bord", + "数据看板更新间隔": "Intervalle de mise à jour du tableau de bord des données", + "数据看板设置": "Paramètres du tableau de bord des données", + "数据看板默认时间粒度": "Granularité temporelle par défaut du tableau de bord des données", + "数据管理和日志查看": "Gestion des données et affichage des journaux", + "文件上传": "Téléchargement de fichier", + "文件搜索价格:{{symbol}}{{price}} / 1K 次": "Prix de recherche de fichier : {{symbol}}{{price}} / 1K fois", + "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "Invite texte {{input}} tokens / 1M tokens * {{symbol}}{{price}} + Complétion texte {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", + "文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "Invite texte {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + Cache {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + Complétion texte {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", + "文字输入": "Saisie de texte", + "文字输出": "Sortie de texte", + "文心一言": "ERNIE Bot", + "文档": "Documentation", + "文档地址": "Lien du document", + "文生视频": "Texte vers vidéo", + "新增供应商": "Ajouter un fournisseur", + "新密码": "Nouveau mot de passe", + "新密码需要和原密码不一致!": "Le nouveau mot de passe doit être différent de l'ancien mot de passe !", + "新建": "Créer", + "新建数量": "Nouvelle quantité", + "新建组": "Nouveau groupe", + "新格式(支持条件判断与json自定义):": "Nouveau format (prend en charge les conditions et la personnalisation JSON) :", + "新格式模板": "Modèle de nouveau format", + "新版本": "Nouvelle version", + "新用户使用邀请码奖励额度": "Quota de bonus de code d'invitation pour nouvel utilisateur", + "新用户初始额度": "Quota initial pour les nouveaux utilisateurs", + "新的备用恢复代码": "Nouveau code de récupération de sauvegarde", + "新的备用码已生成": "Un nouveau code de sauvegarde a été généré", + "新获取的模型": "Nouveaux modèles", + "新额度:": "Nouveau quota : ", + "无": "Aucun", + "无冲突项": "Aucun élément en conflit", + "无效的重置链接,请重新发起密码重置请求": "Lien de réinitialisation non valide, veuillez lancer une nouvelle demande de réinitialisation de mot de passe", + "无法发起 Passkey 注册": "Impossible de lancer l'inscription Passkey", + "无法复制到剪贴板,请手动复制": "Impossible de copier dans le presse-papiers, veuillez copier manuellement", + "无邀请人": "Pas d'invitant", + "无限制": "Illimité", + "无限额度": "Quota illimité", + "日志清理失败:": "Échec du nettoyage des journaux :", + "日志类型": "Type de journal", + "日志设置": "Paramètres du journal", + "日志详情": "Détails du journal", + "旧格式(直接覆盖):": "Ancien format (remplacement direct) :", + "旧格式模板": "Modèle d'ancien format", + "旧的备用码已失效,请保存新的备用码": "Les anciens codes de sauvegarde ont été invalidés, veuillez enregistrer les nouveaux codes de sauvegarde", + "早上好": "Bonjour", + "时间": "Heure", + "时间粒度": "Granularité temporelle", + "易支付商户ID": "ID marchand Epay", + "易支付商户密钥": "Clé marchand Epay", + "是": "Oui", + "是否为企业账户": "Est-ce un compte d'entreprise ?", + "是否同时重置对话消息?选择\"是\"将清空所有对话记录并恢复默认示例;选择\"否\"将保留当前对话记录。": "Voulez-vous également réinitialiser les messages de conversation ? Choisir \"Oui\" effacera tous les enregistrements de conversation et restaurera les exemples par défaut ; choisir \"Non\" conservera les enregistrements de conversation actuels.", + "是否将该订单标记为成功并为用户入账?": "Marquer cette commande comme réussie et créditer l'utilisateur ?", + "是否自动禁用": "Désactiver automatiquement", + "是否要求指纹/面容等生物识别": "Exiger une reconnaissance biométrique par empreinte digitale/faciale", + "显示倍率": "Afficher le ratio", + "显示最新20条": "Afficher les 20 dernières", + "显示名称": "Nom d'affichage", + "显示完整内容": "Afficher le contenu complet", + "显示操作项": "Afficher les actions", + "显示更多": "Afficher plus", + "显示第": "Affichage de", + "显示设置": "Paramètres d'affichage", + "显示调试": "Afficher le débogage", + "晚上好": "Bonsoir", + "普通用户": "Utilisateur normal", + "智能体ID": "ID de l'agent intelligent", + "智能熔断": "Fallback intelligent", + "智谱": "Zhipu AI", + "暂无API信息": "Aucune information sur l'API", + "暂无保存的配置": "Aucune configuration enregistrée", + "暂无充值记录": "Aucune recharge", + "暂无公告": "Pas d'avis", + "暂无匹配模型": "Aucun modèle correspondant", + "暂无可用的支付方式,请联系管理员配置": "Aucune méthode de paiement disponible, veuillez contacter l'administrateur pour la configuration", + "暂无响应数据": "Aucune donnée de réponse", + "暂无密钥数据": "Aucune donnée de clé", + "暂无差异化倍率显示": "Aucun affichage de ratio différentiel", + "暂无常见问答": "Pas de FAQ", + "暂无成功模型": "Aucun modèle réussi", + "暂无数据": "Aucune donnée", + "暂无数据,点击下方按钮添加键值对": "Aucune donnée, cliquez sur le bouton ci-dessous pour ajouter des paires clé-valeur", + "暂无模型描述": "Aucune description de modèle", + "暂无监控数据": "Pas de données de surveillance", + "暂无系统公告": "Pas d'avis système", + "暂无缺失模型": "Aucun modèle manquant", + "暂无请求数据": "Aucune donnée de requête", + "暂无项目": "Aucun projet", + "暂无预填组": "Aucun groupe pré-rempli", + "暴露倍率接口": "Exposer l'API de ratio", + "更多": "Développer plus", + "更多信息请参考": "Pour plus d'informations, veuillez vous référer à", + "更多参数请参考": "Pour plus de paramètres, veuillez vous référer à", + "更好的价格,更好的稳定性,只需要将模型基址替换为:": "Meilleur prix, meilleure stabilité, aucun abonnement requis, il suffit de remplacer l'URL de BASE du modèle par : ", + "更新": "Mettre à jour", + "更新 Stripe 设置": "Mettre à jour les paramètres Stripe", + "更新SSRF防护设置": "Mettre à jour les paramètres de protection SSRF", + "更新Worker设置": "Mettre à jour les paramètres du worker", + "更新令牌信息": "Mettre à jour les informations du jeton", + "更新兑换码信息": "Mettre à jour les informations du code d'échange", + "更新失败": "Échec de la mise à jour", + "更新成功": "Mise à jour réussie", + "更新所有已启用通道余额": "Mettre à jour le solde de tous les canaux activés", + "更新支付设置": "Mettre à jour les paramètres de paiement", + "更新时间": "Heure de mise à jour", + "更新服务器地址": "Mettre à jour l'adresse du serveur", + "更新模型信息": "Mettre à jour les informations du modèle", + "更新渠道信息": "Mettre à jour les informations du canal", + "更新预填组": "Mettre à jour le groupe pré-rempli", + "服务可用性": "État du service", + "服务器地址": "Adresse du serveur", + "服务显示名称": "Nom d'affichage du service", + "未发现新增模型": "Aucun nouveau modèle n'a été ajouté", + "未发现重复密钥": "Aucune clé en double trouvée", + "未启动": "Pas de démarrage", + "未启用": "Non activé", + "未命名": "Sans nom", + "未备份": "Non sauvegardé", + "未开始": "Non démarré", + "未找到匹配的模型": "Aucun modèle correspondant trouvé", + "未找到差异化倍率,无需同步": "Aucun ratio différentiel trouvé, aucune synchronisation n'est requise", + "未提交": "Non soumis", + "未检测到 Fluent 容器": "Conteneur Fluent non détecté", + "未检测到 FluentRead(流畅阅读),请确认扩展已启用": "FluentRead non détecté, veuillez confirmer que l'extension est activée", + "未测试": "Non testé", + "未登录或登录已过期,请重新登录": "Non connecté ou la connexion a expiré, veuillez vous reconnecter", + "未知": "Inconnu", + "未知供应商": "Inconnu", + "未知模型": "Modèle inconnu", + "未知渠道": "Canal inconnu", + "未知状态": "Statut inconnu", + "未知类型": "Type inconnu", + "未知身份": "Identité inconnue", + "未绑定": "Non lié", + "未获取到授权码": "Code d'autorisation non obtenu", + "未设置": "Non défini", + "未设置倍率模型": "Modèles sans ratio", + "未配置模型": "Aucun modèle configuré", + "未配置的模型列表": "Modèles non configurés", + "本地": "Local", + "本地数据存储": "Stockage de données locales", + "本设备:手机指纹/面容,外接:USB安全密钥": "Intégré : empreinte digitale/visage du téléphone, Externe : clé de sécurité USB", + "本设备内置": "Intégré à cet appareil", + "本项目根据": "Ce projet est sous licence ", + "权重": "Poids", + "权限设置": "Paramètres d'autorisation", + "条": "éléments", + "条 - 第": "à", + "条,共": "sur", + "条日志已清理!": "les journaux ont été effacés !", + "查看": "Voir", + "查看图片": "Voir les images", + "查看密钥": "Afficher la clé", + "查看当前可用的所有模型": "Voir tous les modèles actuellement disponibles", + "查看所有可用的AI模型供应商,包括众多知名供应商的模型。": "Affichez tous les fournisseurs de modèles d'IA disponibles, y compris les modèles de nombreux fournisseurs bien connus.", + "查看渠道密钥": "Afficher la clé du canal", + "查询": "Requête", + "标签": "Étiquette", + "标签不能为空!": "L'étiquette ne peut pas être vide !", + "标签信息": "Informations sur l'étiquette", + "标签名称": "Nom de l'étiquette", + "标签的基本配置": "Configuration de base de l'étiquette", + "标签组": "Groupe d'étiquettes", + "标签聚合": "Agrégation d'étiquettes", + "标签聚合模式": "Activer le mode étiquette", + "标识颜色": "Couleur de l'identifiant", + "根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含": "Rechercher les métadonnées du modèle en fonction du nom du modèle et des règles de correspondance, priorité : exact > préfixe > suffixe > contient", + "格式示例:": "Exemple de format :", + "检查更新": "Vérifier les mises à jour", + "检测到 FluentRead(流畅阅读)": "FluentRead détecté", + "检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。": "Plusieurs clés détectées, vous pouvez copier chaque clé individuellement ou cliquer sur Tout copier pour obtenir le contenu complet.", + "检测到该消息后有AI回复,是否删除后续回复并重新生成?": "Une réponse IA a été détectée après ce message, voulez-vous supprimer les réponses suivantes et régénérer ?", + "检测必须等待绘图成功才能进行放大等操作": "La détection doit attendre que le dessin réussisse avant d'effectuer un zoom et d'autres opérations", + "模型": "Modèle", + "模型: {{ratio}}": "Modèle : {{ratio}}", + "模型专用区域": "Zone dédiée au modèle", + "模型价格": "Prix du modèle", + "模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}": "Prix du modèle {{symbol}}{{price}}, {{ratioType}} {{ratio}}", + "模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "Prix du modèle : {{symbol}}{{price}} * {{ratioType}} : {{ratio}} = {{symbol}}{{total}}", + "模型倍率": "Ratio", + "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}": "Ratio du modèle {{modelRatio}}, ratio de cache {{cacheRatio}}, ratio de complétion {{completionRatio}}, {{ratioType}} {{ratio}}", + "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次": "Ratio du modèle {{modelRatio}}, ratio de cache {{cacheRatio}}, ratio de complétion {{completionRatio}}, {{ratioType}} {{ratio}}, appels de recherche Web {{webSearchCallCount}} fois", + "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}": "Ratio du modèle {{modelRatio}}, ratio de cache {{cacheRatio}}, ratio de complétion {{completionRatio}}, ratio d'entrée image {{imageRatio}}, {{ratioType}} {{ratio}}", + "模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}": "Ratio du modèle {{modelRatio}}, ratio de complétion {{completionRatio}}, ratio de cache {{cacheRatio}}, ratio de création de cache {{cacheCreationRatio}}, {{ratioType}} {{ratio}}", + "模型倍率值": "Valeur du ratio de modèle", + "模型倍率和补全倍率": "Ratio de modèle et ratio de complétion", + "模型倍率和补全倍率同时设置": "Le ratio de modèle et le ratio de complétion sont définis simultanément", + "模型倍率设置": "Paramètres de ratio de modèle", + "模型关键字": "mot-clé du modèle", + "模型列表已复制到剪贴板": "Liste des modèles copiée dans le presse-papiers", + "模型列表已更新": "La liste des modèles a été mise à jour", + "模型创建成功!": "Modèle créé avec succès !", + "模型名称": "Nom du modèle", + "模型名称已存在": "Le nom du modèle existe déjà", + "模型固定价格": "Prix du modèle par appel", + "模型图标": "Icône du modèle", + "模型定价,需要登录访问": "Tarification du modèle, nécessite une connexion pour y accéder", + "模型广场": "Place du marché des modèles", + "模型支持的接口端点信息": "Informations sur les points de terminaison de l'API pris en charge par le modèle", + "模型数据分析": "Analyse des données du modèle", + "模型映射必须是合法的 JSON 格式!": "Le mappage de modèles doit être au format JSON valide !", + "模型更新成功!": "Modèle mis à jour avec succès !", + "模型消耗分布": "Distribution de la consommation des modèles", + "模型消耗趋势": "Tendance de la consommation des modèles", + "模型版本": "Version du modèle", + "模型的详细描述和基本特性": "Description détaillée et caractéristiques de base du modèle", + "模型相关设置": "Paramètres liés au modèle", + "模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:": "La communauté des modèles a besoin de la contribution de tous. Si vous trouvez des données incorrectes ou si vous souhaitez contribuer à de nouvelles données de modèle, veuillez visiter :", + "模型管理": "Gestion des modèles", + "模型组": "Groupe de modèles", + "模型补全倍率(仅对自定义模型有效)": "Ratio d'achèvement de modèle (uniquement efficace pour les modèles personnalisés)", + "模型请求速率限制": "Limite de débit de requête de modèle", + "模型调用次数占比": "Ratio d'appels de modèles", + "模型调用次数排行": "Classement des appels de modèles", + "模型选择和映射设置": "Sélection de modèle et paramètres de mappage", + "模型配置": "Configuration du modèle", + "模型重定向": "Redirection de modèle", + "模型限制列表": "Liste des restrictions de modèle", + "模板示例": "Exemple de modèle", + "模糊搜索模型名称": "Recherche floue de nom de modèle", + "次": "Fois", + "欢迎使用,请完成以下设置以开始使用系统": "Bienvenue, veuillez compléter les paramètres suivants pour commencer à utiliser le système", + "正在处理大内容...": "Traitement de contenu volumineux...", + "正在提交": "Envoi en cours", + "正在构造请求体预览...": "Construction de l'aperçu du corps de la requête...", + "正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)": "Test des modèles ${current} - ${end} sur ${total} au total", + "正在跳转...": "Redirection...", + "此代理仅用于图片请求转发,Webhook通知发送等,AI API请求仍然由服务器直接发出,可在渠道设置中单独配置代理": "Ce proxy est utilisé uniquement pour le transfert des requêtes d'images, l'envoi de notifications Webhook, etc. Les requêtes d'API IA sont toujours émises directement par le serveur, le proxy peut être configuré séparément dans les paramètres du canal", + "此修改将不可逆": "Cette modification sera irréversible", + "此操作不可恢复,请仔细确认时间后再操作!": "Cette opération est irréversible, veuillez confirmer attentivement l'heure avant d'opérer !", + "此操作不可撤销,将永久删除已自动禁用的密钥": "Cette opération ne peut pas être annulée et toutes les clés désactivées automatiquement seront définitivement supprimées.", + "此操作不可撤销,将永久删除该密钥": "Cette opération ne peut être annulée et la clé sera définitivement supprimée.", + "此操作不可逆,所有数据将被永久删除": "Cette opération est irréversible, toutes les données seront définitivement supprimées", + "此操作将启用用户账户": "Cette opération activera le compte utilisateur", + "此操作将提升用户的权限级别": "Cette opération augmentera le niveau de permission de l'utilisateur", + "此操作将禁用用户账户": "Cette opération désactivera le compte utilisateur", + "此操作将禁用该用户当前的两步验证配置,下次登录将不再强制输入验证码,直到用户重新启用。": "Cela désactivera la configuration actuelle de l'authentification à deux facteurs de l'utilisateur. Aucun code de vérification ne sera requis jusqu'à ce qu'il la réactive.", + "此操作将解绑用户当前的 Passkey,下次登录需要重新注册。": "Cela détachera le Passkey actuel de l'utilisateur. Il devra se réenregistrer lors de sa prochaine connexion.", + "此操作将降低用户的权限级别": "Cette opération abaissera le niveau de permission de l'utilisateur", + "此支付方式最低充值金额为": "Le montant minimum de recharge pour ce mode de paiement est de", + "此设置用于系统内部计算,默认值500000是为了精确到6位小数点设计,不推荐修改。": "Ce paramètre est utilisé pour les calculs internes du système, la valeur par défaut 500000 est conçue pour une précision de 6 décimales, la modification n'est pas recommandée.", + "此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除": "Cette page n'affiche que les modèles sans prix ni ratio. Après le paramétrage, ils seront automatiquement supprimés de la liste", + "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "Lecture seule, paramètres personnels de l'utilisateur, et ne peut pas être modifié directement", + "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:": "Ceci est facultatif, utilisé pour modifier le nom du modèle dans le corps de la requête, c'est une chaîne JSON, la clé est le nom du modèle dans la requête, et la valeur est le nom du modèle à remplacer, par exemple :", + "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改": "Ceci est facultatif, utilisé pour modifier le nom du modèle dans le corps de la requête, sous forme de chaîne JSON, la clé est le nom du modèle dans la requête, la valeur est le nom du modèle à remplacer, laisser vide ne changera rien", + "此项可选,用于复写返回的状态码,仅影响本地判断,不修改返回到上游的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:": "Facultatif, utilisé pour remplacer les codes d'état renvoyés, affecte uniquement le jugement local, ne modifie pas le code d'état renvoyé en amont, par exemple, réécrire l'erreur 400 du canal Claude en 500 (pour une nouvelle tentative). N'abusez pas de cette fonctionnalité. Exemple :", + "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数": "Ceci est facultatif, utilisé pour remplacer les paramètres de requête. Ne prend pas en charge le remplacement du paramètre stream", + "此项可选,用于覆盖请求头参数": "Ceci est facultatif, utilisé pour remplacer les paramètres d'en-tête de requête", + "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "Facultatif pour les appels d'API via une adresse d'API personnalisée, n'ajoutez pas /v1 et / à la fin", + "每隔多少分钟测试一次所有通道": "Tous les combien de minutes tester tous les canaux", + "永不过期": "N'expire jamais", + "永久删除您的两步验证设置": "Supprimer définitivement vos paramètres d'authentification à deux facteurs", + "永久删除所有备用码(包括未使用的)": "Supprimer définitivement tous les codes de sauvegarde (y compris ceux non utilisés)", + "没有可用令牌用于填充": "Aucun jeton disponible pour le remplissage", + "没有可用模型": "Aucun modèle disponible", + "没有找到匹配的模型": "Aucun modèle correspondant trouvé", + "没有未设置的模型": "Aucun modèle non configuré", + "没有模型可以复制": "Aucun modèle à copier", + "没有账户?": "Pas de compte ? ", + "注 册": "S'inscrire", + "注册": "S'inscrire", + "注册 Passkey": "Enregistrer un Passkey", + "注意": "Remarque", + "注意:JSON中重复的键只会保留最后一个同名键的值": "Remarque : Dans JSON, pour les clés dupliquées, seule la valeur de la dernière clé du même nom sera conservée", + "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "Remarque : Pour les API non-Chat, assurez-vous de saisir l'adresse API correcte, sinon elle pourrait ne pas fonctionner", + "注销": "Se déconnecter", + "注销成功!": "Déconnexion réussie !", + "流": "Flux", + "浅色": "Clair", + "浅色模式": "Mode clair", + "测试": "Tester", + "测试中": "Test en cours", + "测试中...": "Test en cours...", + "测试单个渠道操作项目组": "Tester un seul groupe de projet d'opération de canal", + "测试失败": "Échec du test", + "测试所有渠道的最长响应时间": "Temps de réponse maximal pour tester tous les canaux", + "测试所有通道": "Tester tous les canaux", + "测速": "Test de vitesse", + "消息优先级": "Priorité du message", + "消息优先级,范围0-10,默认为5": "Priorité du message, plage 0-10, par défaut 5", + "消息已删除": "Message supprimé", + "消息已复制到剪贴板": "Message copié dans le presse-papiers", + "消息已更新": "Message mis à jour", + "消息已编辑": "Message édité", + "消耗分布": "Distribution de la consommation", + "消耗趋势": "Tendance de la consommation", + "消耗额度": "Quota utilisé", + "消费": "Consommer", + "深色": "Sombre", + "深色模式": "Mode sombre", + "添加": "Ajouter", + "添加API": "Ajouter une API", + "添加令牌": "Créer un jeton", + "添加兑换码": "Ajouter un code d'échange", + "添加公告": "Ajouter un avis", + "添加分类": "Ajouter une catégorie", + "添加成功": "Ajouté avec succès", + "添加模型": "Ajouter un modèle", + "添加模型区域": "Ajouter une zone de modèle", + "添加渠道": "Ajouter un canal", + "添加用户": "Ajouter un utilisateur", + "添加聊天配置": "Ajouter une configuration de chat", + "添加键值对": "Ajouter une paire clé-valeur", + "添加问答": "Ajouter une FAQ", + "添加额度": "Ajouter un quota", + "清空重定向": "Effacer la redirection", + "清除历史日志": "Effacer les journaux historiques", + "清除失效兑换码": "Effacer les codes d'échange non valides", + "清除所有模型": "Effacer tous les modèles", + "渠道": "Canal", + "渠道 ID": "ID du Canal", + "渠道ID,名称,密钥,API地址": "ID du canal, nom, clé, URL de base", + "渠道优先级": "Priorité du canal", + "渠道信息": "Informations sur le canal", + "渠道创建成功!": "Canal créé avec succès !", + "渠道复制失败": "Échec de la copie du canal", + "渠道复制失败: ": "Échec de la copie du canal :", + "渠道复制成功": "Copie de canal réussie", + "渠道密钥": "Clé de canal", + "渠道密钥信息": "Informations sur la clé du canal", + "渠道密钥列表": "Liste des clés de canal", + "渠道更新成功!": "Canal mis à jour avec succès !", + "渠道权重": "Poids du canal", + "渠道标签": "Étiquette du canal", + "渠道模型信息不完整": "Informations du modèle de canal incomplètes", + "渠道的基本配置信息": "Informations de configuration de base du canal", + "渠道的模型测试": "Test de modèle de canal", + "渠道的高级配置选项": "Options de configuration avancées du canal", + "渠道管理": "Gestion des canaux", + "渠道额外设置": "Paramètres supplémentaires du canal", + "源地址": "Adresse source", + "演示站点": "Site de démonstration", + "演示站点模式": "Mode site de démonstration", + "点击上传文件或拖拽文件到这里": "Cliquez pour télécharger un fichier ou faites glisser et déposez un fichier ici", + "点击下方按钮通过 Telegram 完成绑定": "Cliquez sur le bouton ci-dessous pour terminer la liaison via Telegram", + "点击复制模型名称": "Cliquez pour copier le nom du modèle", + "点击查看差异": "Cliquez pour voir les différences", + "点击此处": "cliquez ici", + "点击预览视频": "Cliquez pour prévisualiser la vidéo", + "点击验证按钮,使用您的生物特征或安全密钥": "Cliquez sur le bouton de vérification pour utiliser vos caractéristiques biométriques ou votre clé de sécurité", + "版权所有": "Tous droits réservés", + "状态": "Statut", + "状态码复写": "Remplacement du code d'état", + "状态筛选": "Filtre d'état", + "状态页面Slug": "Slug de la page d'état", + "生成令牌": "Générer un jeton", + "生成数量": "Générer la quantité", + "生成数量必须大于0": "La quantité de génération doit être supérieure à 0", + "生成新的备用码": "Générer de nouveaux codes de sauvegarde", + "生成歌词": "Générer des paroles", + "生成音乐": "générer de la musique", + "用于API调用的身份验证令牌,请妥善保管": "Jeton d'authentification pour les appels d'API, veuillez le conserver en lieu sûr", + "用于配置网络代理,支持 socks5 协议": "Utilisé pour configurer le proxy réseau, prend en charge le protocole socks5", + "用以支持基于 WebAuthn 的无密码登录注册": "Prise en charge de la connexion et de l'enregistrement sans mot de passe basés sur WebAuthn", + "用以支持用户校验": "Pour prendre en charge la vérification des utilisateurs", + "用以支持系统的邮件发送": "Pour prendre en charge l'envoi d'e-mails système", + "用以支持通过 GitHub 进行登录注册": "Pour prendre en charge la connexion & l'inscription via GitHub", + "用以支持通过 Linux DO 进行登录注册": "Pour prendre en charge la connexion & l'inscription via Linux DO", + "用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP": "Pour prendre en charge la connexion via OIDC, par exemple Okta, Auth0 et autres IdP compatibles avec le protocole OIDC", + "用以支持通过 Telegram 进行登录注册": "Pour prendre en charge la connexion & l'inscription via Telegram", + "用以支持通过微信进行登录注册": "Pour prendre en charge la connexion & l'inscription via WeChat", + "用以防止恶意用户利用临时邮箱批量注册": "Pour empêcher les utilisateurs malveillants d'utiliser des e-mails temporaires pour s'inscrire en masse", + "用户": "Utilisateurs", + "用户个人功能": "Fonctions personnelles de l'utilisateur", + "用户主页,展示系统信息": "Page d'accueil de l'utilisateur, affichant les informations système", + "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "Priorité de l'utilisateur : si l'utilisateur spécifie une invite système dans la requête, le paramètre de l'utilisateur sera utilisé en premier", + "用户信息": "Informations utilisateur", + "用户信息更新成功!": "Informations utilisateur mises à jour avec succès !", + "用户分组": "Votre groupe par défaut", + "用户分组和额度管理": "Gestion des groupes d'utilisateurs et des quotas", + "用户分组配置": "Configuration du groupe d'utilisateurs", + "用户协议": "Accord utilisateur", + "用户协议已更新": "L'accord utilisateur a été mis à jour", + "用户协议更新失败": "Échec de la mise à jour de l'accord utilisateur", + "用户可选分组": "Groupes sélectionnables par l'utilisateur", + "用户名": "Nom d'utilisateur", + "用户名或邮箱": "Nom d'utilisateur ou e-mail", + "用户名称": "Nom d'utilisateur", + "用户控制面板,管理账户": "Panneau de configuration de l'utilisateur pour la gestion du compte", + "用户新建令牌时可选的分组,格式为 JSON 字符串,例如:{\"vip\": \"VIP 用户\", \"test\": \"测试\"},表示用户可以选择 vip 分组和 test 分组": "Groupes sélectionnables par l'utilisateur lors de la création d'un jeton, format de chaîne JSON, par exemple : {\"vip\": \"Utilisateur VIP\", \"test\": \"Test\"}, indiquant que l'utilisateur peut sélectionner le groupe vip et le groupe test", + "用户每周期最多请求完成次数": "Nombre maximal de requêtes utilisateur réussies par période", + "用户每周期最多请求次数": "Nombre maximal de requêtes utilisateur par période", + "用户注册时看到的网站名称,比如'我的网站'": "Nom du site Web que les utilisateurs voient lors de l'inscription, par exemple 'Mon site Web'", + "用户的基本账户信息": "Informations de base du compte utilisateur", + "用户管理": "Gestion des utilisateurs", + "用户组": "Groupe d'utilisateurs", + "用户账户创建成功!": "Compte utilisateur créé avec succès !", + "用户账户管理": "Gestion des comptes utilisateurs", + "用时/首字": "Temps/premier mot", + "留空则使用账号绑定的邮箱": "Si ce champ est laissé vide, l'adresse e-mail liée au compte sera utilisée", + "留空则使用默认端点;支持 {path, method}": "Laissez vide pour utiliser le point de terminaison par défaut ; prend en charge {path, method}", + "留空则默认使用服务器地址,注意不能携带http://或者https://": "Laissez vide pour utiliser l'adresse du serveur par défaut, notez que vous ne pouvez pas inclure http:// ou https://", + "登 录": "Se connecter", + "登录": "Se connecter", + "登录成功!": "Connexion réussie !", + "登录过期,请重新登录!": "Session expirée, veuillez vous reconnecter !", + "白名单": "Liste blanche", + "的前提下使用。": "doit être utilisé conformément aux conditions.", + "监控设置": "Paramètres de surveillance", + "目标用户:{{username}}": "Utilisateur cible : {{username}}", + "相关项目": "Projets connexes", + "相当于删除用户,此修改将不可逆": "Équivalent à supprimer l'utilisateur, cette modification sera irréversible", + "矛盾": "Conflit", + "知识库 ID": "ID de la base de connaissances", + "确定": "OK", + "确定?": "Sûr ?", + "确定删除此组?": "Confirmer la suppression de ce groupe ?", + "确定导入": "Confirmer l'importation", + "确定是否要修复数据库一致性?": "Êtes-vous sûr de vouloir réparer la cohérence de la base de données ?", + "确定是否要删除所选通道?": "Êtes-vous sûr de vouloir supprimer les canaux sélectionnés ?", + "确定是否要删除此令牌?": "Êtes-vous sûr de vouloir supprimer ce jeton ?", + "确定是否要删除此兑换码?": "Êtes-vous sûr de vouloir supprimer ce code d'échange ?", + "确定是否要删除此模型?": "Êtes-vous sûr de vouloir supprimer ce modèle ?", + "确定是否要删除此渠道?": "Êtes-vous sûr de vouloir supprimer ce canal ?", + "确定是否要删除禁用通道?": "Êtes-vous sûr de vouloir supprimer le canal désactivé ?", + "确定是否要复制此渠道?": "Êtes-vous sûr de vouloir copier ce canal ?", + "确定是否要注销此用户?": "Êtes-vous sûr de vouloir déconnecter cet utilisateur ?", + "确定清除所有失效兑换码?": "Êtes-vous sûr de vouloir effacer tous les codes d'échange non valides ?", + "确定要修改所有子渠道优先级为 ": "Confirmer la modification de toutes les priorités des sous-canaux en ", + "确定要修改所有子渠道权重为 ": "Confirmer la modification de tous les poids des sous-canaux en ", + "确定要删除供应商 \"{{name}}\" 吗?此操作不可撤销。": "Êtes-vous sûr de vouloir supprimer le fournisseur \"{{name}}\" ? Cette opération est irréversible.", + "确定要删除所有已自动禁用的密钥吗?": "Êtes-vous sûr de vouloir supprimer toutes les clés désactivées automatiquement ?", + "确定要删除所选的 {{count}} 个令牌吗?_one": "Êtes-vous sûr de vouloir supprimer le jeton sélectionné ?", + "确定要删除所选的 {{count}} 个令牌吗?_many": "Êtes-vous sûr de vouloir supprimer les {{count}} jetons sélectionnés ?", + "确定要删除所选的 {{count}} 个令牌吗?_other": "Êtes-vous sûr de vouloir supprimer les {{count}} jetons sélectionnés ?", + "确定要删除所选的 {{count}} 个模型吗?_one": "Êtes-vous sûr de vouloir supprimer le modèle sélectionné ?", + "确定要删除所选的 {{count}} 个模型吗?_many": "Êtes-vous sûr de vouloir supprimer les {{count}} modèles sélectionnés ?", + "确定要删除所选的 {{count}} 个模型吗?_other": "Êtes-vous sûr de vouloir supprimer les {{count}} modèles sélectionnés ?", + "确定要删除此API信息吗?": "Êtes-vous sûr de vouloir supprimer ces informations d'API ?", + "确定要删除此公告吗?": "Êtes-vous sûr de vouloir supprimer cet avis ?", + "确定要删除此分类吗?": "Êtes-vous sûr de vouloir supprimer cette catégorie ?", + "确定要删除此密钥吗?": "Êtes-vous sûr de vouloir supprimer cette clé ?", + "确定要删除此问答吗?": "Êtes-vous sûr de vouloir supprimer cette FAQ ?", + "确定要删除这条消息吗?": "Êtes-vous sûr de vouloir supprimer ce message ?", + "确定要启用所有密钥吗?": "Êtes-vous sûr de vouloir activer toutes les clés ?", + "确定要启用此用户吗?": "Êtes-vous sûr de vouloir activer cet utilisateur ?", + "确定要提升此用户吗?": "Êtes-vous sûr de vouloir promouvoir cet utilisateur ?", + "确定要更新所有已启用通道余额吗?": "Êtes-vous sûr de vouloir mettre à jour le solde de tous les canaux activés ?", + "确定要测试所有通道吗?": "Êtes-vous sûr de vouloir tester tous les canaux ?", + "确定要禁用所有的密钥吗?": "Êtes-vous sûr de vouloir désactiver toutes les clés ?", + "确定要禁用此用户吗?": "Êtes-vous sûr de vouloir désactiver cet utilisateur ?", + "确定要降级此用户吗?": "Êtes-vous sûr de vouloir rétrograder cet utilisateur ?", + "确定重置": "Confirmer la réinitialisation", + "确定重置模型倍率吗?": "Confirmer la réinitialisation du ratio de modèle ?", + "确认": "Confirmer", + "确认冲突项修改": "Confirmer la modification de l'élément de conflit", + "确认删除": "Confirmer la suppression", + "确认取消密码登录": "Confirmer l'annulation de la connexion par mot de passe", + "确认密码": "Confirmer le mot de passe", + "确认导入配置": "Confirmer l'importation de la configuration", + "确认新密码": "Confirmer le nouveau mot de passe", + "确认清除历史日志": "Confirmer l'effacement des journaux historiques", + "确认禁用": "Confirmer la désactivation", + "确认补单": "Confirmer la complétion", + "确认解绑": "Confirmer la dissociation", + "确认解绑 Passkey": "Confirmer la dissociation du Passkey", + "确认设置并完成初始化": "Confirmer les paramètres et terminer l'initialisation", + "确认重置 Passkey": "Confirmer la réinitialisation du Passkey", + "确认重置两步验证": "Confirmer la réinitialisation de l'authentification à deux facteurs", + "确认重置密码": "Confirmer la réinitialisation du mot de passe", + "示例": "Exemple", + "示例:{\"default\": [200, 100], \"vip\": [0, 1000]}。": "Exemple : {\"default\": [200, 100], \"vip\": [0, 1000]}.", + "视频": "Vidéo", + "禁用": "Désactiver", + "禁用 store 透传": "Désactiver le passage de store", + "禁用2FA失败": "Échec de la désactivation de 2FA", + "禁用两步验证": "Désactiver l'authentification à deux facteurs", + "禁用全部": "Désactiver tout", + "禁用原因": "Raison de la désactivation", + "禁用后的影响:": "Impact après la désactivation :", + "禁用密钥失败": "Échec de la désactivation de la clé", + "禁用所有密钥失败": "Échec de la désactivation de toutes les clés", + "禁用时间": "Heure de désactivation", + "私有IP访问详细说明": "⚠️ Avertissement de sécurité : l'activation de cette option autorise l'accès aux ressources du réseau interne (localhost, réseaux privés). N'activez cette option que si vous devez accéder à des services internes et que vous comprenez les implications en matière de sécurité.", + "私有部署地址": "Adresse de déploiement privée", + "秒": "Seconde", + "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "La suppression de la marque de copyright de One API doit d'abord être autorisée. La maintenance du projet demande beaucoup d'efforts. Si ce projet a du sens pour vous, veuillez le soutenir activement.", + "窗口处理": "gestion des fenêtres", + "窗口等待": "attente de la fenêtre", + "站点额度展示类型及汇率": "Type d'affichage du quota du site et taux de change", + "端口配置详细说明": "Limitez les requêtes externes à des ports spécifiques. Utilisez des ports uniques (80, 443) ou des plages (8000-8999). Une liste vide autorise tous les ports. La valeur par défaut inclut les ports Web courants.", + "端点": "Point de terminaison", + "端点映射": "Mappage de points de terminaison", + "端点类型": "Type de point de terminaison", + "端点组": "Groupe de points de terminaison", + "第三方账户绑定状态(只读)": "État de la liaison du compte tiers (lecture seule)", + "等价金额:": "Montant équivalent : ", + "等待中": "En attente", + "等待获取邮箱信息...": "En attente d'obtenir des informations par e-mail...", + "筛选": "Filtre", + "管理": "Gérer", + "管理你的 LinuxDO OAuth App": "Gérer votre application OAuth LinuxDO", + "管理员": "Admin", + "管理员区域": "Zone administrateur", + "管理员暂时未设置任何关于内容": "L'administrateur n'a encore défini aucun contenu personnalisé \"À propos\".", + "管理员未开启Stripe充值!": "L'administrateur n'a pas activé la recharge Stripe !", + "管理员未开启在线充值!": "L'administrateur n'a pas activé la recharge en ligne !", + "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "L'administrateur n'a pas activé la fonction de recharge en ligne, veuillez contacter l'administrateur pour l'activer ou recharger avec un code d'échange.", + "管理员未设置用户可选分组": "L'administrateur n'a pas défini de groupes sélectionnables par l'utilisateur", + "管理员设置了外部链接,点击下方按钮访问": "L'administrateur a défini un lien externe, cliquez sur le bouton ci-dessous pour y accéder", + "管理员账号": "Compte administrateur", + "管理员账号已经初始化过,请继续设置其他参数": "Le compte administrateur a déjà été initialisé, veuillez continuer à définir d'autres paramètres", + "管理模型、标签、端点等预填组": "Gérer les groupes pré-remplis de modèles, d'étiquettes, de points de terminaison, etc.", + "类型": "Type", + "精确": "Exact", + "系统": "Système", + "系统令牌已复制到剪切板": "Le jeton système a été copié dans le presse-papiers", + "系统任务记录": "Enregistrements de tâches système", + "系统信息": "Informations système", + "系统公告": "Avis système", + "系统公告管理,可以发布系统通知和重要消息(最多100个,前端显示最新20条)": "Gestion des avis système, vous pouvez publier des avis système et des messages importants (maximum 100, afficher les 20 derniers sur le front-end)", + "系统初始化": "Initialisation du système", + "系统初始化失败,请重试": "L'initialisation du système a échoué, veuillez réessayer", + "系统初始化成功,正在跳转...": "Initialisation du système réussie, redirection en cours...", + "系统参数配置": "Configuration des paramètres système", + "系统名称": "Nom du système", + "系统名称已更新": "Nom du système mis à jour", + "系统名称更新失败": "Échec de la mise à jour du nom du système", + "系统提示覆盖": "Remplacement de l'invite système", + "系统提示词": "Invite système", + "系统提示词拼接": "Concaténation des invites système", + "系统数据统计": "Statistiques des données système", + "系统文档和帮助信息": "Documentation système et informations d'aide", + "系统消息": "Messages système", + "系统管理功能": "Fonctions de gestion du système", + "系统设置": "Paramètres système", + "系统访问令牌": "Jeton d'accès au système", + "约": "Environ", + "索引": "Index", + "紧凑列表": "Liste compacte", + "线路描述": "Description de l'itinéraire", + "组列表": "Liste des groupes", + "组名": "Nom du groupe", + "组织": "Organisation", + "组织,不填则为默认组织": "Organisation, par défaut si vide", + "绑定": "Lier", + "绑定 Telegram": "Lier Telegram", + "绑定信息": "Informations de liaison", + "绑定微信账户": "Lier le compte WeChat", + "绑定成功!": "Liaison réussie !", + "绑定邮箱地址": "Lier l'adresse e-mail", + "结束时间": "Heure de fin", + "结果图片": "Résultat", + "绘图": "Dessin", + "绘图任务记录": "Enregistrements de tâches de dessin", + "绘图日志": "Journaux de dessin", + "绘图设置": "Paramètres de dessin", + "统计Tokens": "Jetons statistiques", + "统计次数": "Nombre de statistiques", + "统计额度": "Quota statistique", + "继续": "Continuer", + "缓存 Tokens": "Jetons de cache", + "缓存: {{cacheRatio}}": "Cache : {{cacheRatio}}", + "缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Prix du cache : {{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (taux de cache : {{cacheRatio}})", + "缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Prix du cache : {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (taux de cache : {{cacheRatio}})", + "缓存倍率": "Ratio de cache", + "缓存创建 Tokens": "Jetons de création de cache", + "缓存创建: {{cacheCreationRatio}}": "Création de cache : {{cacheCreationRatio}}", + "缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "Prix de création du cache : {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (taux de création de cache : {{cacheCreationRatio}})", + "编辑": "Modifier", + "编辑API": "Modifier l'API", + "编辑供应商": "Modifier le fournisseur", + "编辑公告": "Modifier l'avis", + "编辑公告内容": "Modifier le contenu de l'annonce", + "编辑分类": "Modifier la catégorie", + "编辑成功": "Modification réussie", + "编辑标签": "Modifier l'étiquette", + "编辑模型": "Modifier le modèle", + "编辑模式": "Mode d'édition", + "编辑用户": "Modifier l'utilisateur", + "编辑聊天配置": "Modifier la configuration de discussion", + "编辑问答": "Modifier la FAQ", + "缩词": "Raccourcir", + "缺省 MaxTokens": "MaxTokens par défaut", + "网站地址": "Adresse du site web", + "网站域名标识": "ID de domaine du site Web", + "网络错误": "Erreur réseau", + "置信度": "Confiance", + "聊天": "Discuter", + "聊天会话管理": "Gestion des sessions de discussion", + "聊天区域": "Zone de discussion", + "聊天应用名称": "Nom de l'application de discussion", + "聊天应用名称已存在,请使用其他名称": "Le nom de l'application de discussion existe déjà, veuillez utiliser un autre nom", + "聊天设置": "Paramètres de discussion", + "聊天配置": "Configuration de la discussion", + "聊天链接配置错误,请联系管理员": "Erreur de configuration du lien de discussion, veuillez contacter l'administrateur", + "联系我们": "Contactez-nous", + "腾讯混元": "Hunyuan", + "自动分组auto,从第一个开始选择": "Regroupement automatique auto, sélection à partir du premier", + "自动检测": "Détection automatique", + "自动模式": "Mode automatique", + "自动测试所有通道间隔时间": "Intervalle de test automatique pour tous les canaux", + "自动禁用": "Désactivé automatiquement", + "自动禁用关键词": "Mots-clés de désactivation automatique", + "自动选择": "Sélection automatique", + "自定义充值数量选项": "Options de montant de recharge personnalisées", + "自定义充值数量选项不是合法的 JSON 数组": "Les options de montant de recharge personnalisées ne sont pas un tableau JSON valide", + "自定义变焦-提交": "Zoom personnalisé-Soumettre", + "自定义模型名称": "Nom de modèle personnalisé", + "自定义货币": "Devise personnalisée", + "自定义货币符号": "Symbole de devise personnalisé", + "自用模式": "Mode auto-utilisation", + "自适应列表": "Liste adaptative", + "节省": "Économiser", + "花费": "Dépenser", + "花费时间": "passer du temps", + "若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置": "Si votre fournisseur OIDC prend en charge le Discovery Endpoint, vous pouvez simplement remplir l'URL OIDC Well-Known, le système obtiendra automatiquement la configuration OIDC", + "获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确": "Échec de l'obtention de la configuration OIDC, veuillez vérifier l'état du réseau et si l'URL Well-Known est correcte", + "获取 OIDC 配置成功!": "Configuration OIDC obtenue avec succès !", + "获取2FA状态失败": "Échec de l'obtention de l'état 2FA", + "获取初始化状态失败": "Échec de l'obtention de l'état d'initialisation", + "获取启用模型失败": "Échec de l'obtention des modèles activés", + "获取启用模型失败:": "Échec de l'obtention des modèles activés :", + "获取密钥": "Obtenir la clé", + "获取密钥失败": "Échec de l'obtention de la clé", + "获取密钥状态失败": "Échec de l'obtention de l'état de la clé", + "获取未配置模型失败": "Échec de l'obtention des modèles non configurés", + "获取模型列表": "Obtenir la liste des modèles", + "获取模型列表失败": "Échec de la récupération de la liste des modèles", + "获取渠道失败:": "Échec de l'obtention des canaux : ", + "获取组列表失败": "Échec de l'obtention de la liste des groupes", + "获取金额失败": "Échec de l'obtention du montant", + "获取验证码": "Obtenir le code de vérification", + "补全": "Achèvement", + "补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "Prix de complétion : {{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (taux de complétion : {{completionRatio}})", + "补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens": "Prix de complétion : {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens", + "补全倍率": "Ratio de complétion", + "补全倍率值": "Valeur du ratio de complétion", + "补单": "Compléter la commande", + "补单失败": "Échec de la complétion de la commande", + "补单成功": "Commande complétée avec succès", + "表单引用错误,请刷新页面重试": "Erreur de référence de formulaire, veuillez actualiser la page et réessayer", + "表格视图": "Vue tableau", + "覆盖模式:将完全替换现有的所有密钥": "Mode de remplacement : remplacera complètement toutes les clés existantes", + "覆盖现有密钥": "Remplacer les clés existantes", + "角色": "Rôle", + "解析响应数据时发生错误": "Erreur lors de l'analyse des données de réponse", + "解析密钥文件失败: {{msg}}": "Échec de l'analyse du fichier de clés : {{msg}}", + "解绑 Passkey": "Supprimer le Passkey", + "解绑后将无法使用 Passkey 登录,确定要继续吗?": "Après la dissociation, vous ne pourrez plus vous connecter avec Passkey. Êtes-vous sûr de vouloir continuer ?", + "计费类型": "Type de facturation", + "计费过程": "Processus de mise en lots", + "订单号": "N° de commande", + "讯飞星火": "Spark Desk", + "记录请求与错误日志IP": "Enregistrer l'adresse IP du journal des requêtes et des erreurs", + "设备类型偏好": "Préférence de type d'appareil", + "设置 Logo": "Définir un logo", + "设置2FA失败": "Échec de la configuration de 2FA", + "设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}": "Définir les remises correspondant aux différents montants de recharge, la clé est le montant de recharge, la valeur est le taux de remise, par exemple : {\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}", + "设置两步验证": "Configurer l'authentification à deux facteurs", + "设置令牌可用额度和数量": "Définir le quota et la quantité disponibles du jeton", + "设置令牌的基本信息": "Définir les informations de base du jeton", + "设置令牌的访问限制": "Définir les restrictions d'accès au jeton", + "设置保存失败": "Échec de l'enregistrement des paramètres", + "设置保存成功": "Paramètres enregistrés avec succès", + "设置兑换码的基本信息": "Définir les informations de base du code d'échange", + "设置兑换码的额度和数量": "Définir le quota et la quantité du code d'échange", + "设置公告": "Définir un avis", + "设置关于": "Définir \"À propos\"", + "设置已保存": "Paramètres enregistrés", + "设置模型的基本信息": "Définir les informations de base du modèle", + "设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱": "Définissez l'adresse e-mail pour recevoir les notifications d'avertissement de quota, si elle n'est pas définie, l'adresse e-mail liée au compte sera utilisée", + "设置用户协议": "Définir l'accord utilisateur", + "设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]": "Définir les options de montant de recharge sélectionnables par l'utilisateur, par exemple : [10, 20, 50, 100, 200, 500]", + "设置管理员登录信息": "Définir les informations de connexion de l'administrateur", + "设置类型": "Type de paramètre", + "设置系统名称": "Définir le nom du système", + "设置过短会影响数据库性能": "Un réglage trop court affectera les performances de la base de données", + "设置隐私政策": "Définir la politique de confidentialité", + "设置页脚": "Définir le pied de page", + "设置预填组的基本信息": "Définir les informations de base du groupe pré-rempli", + "设置首页内容": "Définir le contenu de la page d'accueil", + "设置默认地区和特定模型的专用地区": "Définir la région par défaut et les régions dédiées pour des modèles spécifiques", + "设计与开发由": "Conçu et développé avec amour par", + "访问限制": "Restrictions d'accès", + "该供应商提供多种AI模型,适用于不同的应用场景。": "Ce fournisseur propose plusieurs modèles d'IA, adaptés à différents scénarios d'application.", + "该分类下没有可用模型": "Aucun modèle disponible dans cette catégorie", + "该域名已存在于白名单中": "Ce nom de domaine existe déjà dans la liste blanche", + "该数据可能不可信,请谨慎使用": "Ces données peuvent ne pas être fiables, veuillez les utiliser avec prudence", + "该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置": "Cette adresse de serveur affectera l'adresse de rappel de paiement et l'adresse affichée sur la page d'accueil par défaut, veuillez vous assurer d'une configuration correcte", + "该模型存在固定价格与倍率计费方式冲突,请确认选择": "Le modèle a un conflit de méthode de facturation à prix fixe et à ratio, veuillez confirmer la sélection", + "详情": "Détails", + "语音输入": "Entrée vocale", + "语音输出": "Sortie vocale", + "说明": "Description", + "说明:": "Description :", + "说明信息": "Description", + "请上传密钥文件": "Veuillez télécharger le fichier de clé", + "请上传密钥文件!": "Veuillez télécharger le fichier de clé !", + "请为渠道命名": "Veuillez nommer le canal", + "请先填写服务器地址": "Veuillez d'abord remplir l'adresse du serveur", + "请先输入密钥": "Veuillez d'abord saisir la clé", + "请先选择同步渠道": "Veuillez d'abord sélectionner le canal de synchronisation", + "请先选择模型!": "Veuillez d'abord sélectionner un modèle !", + "请先选择要删除的令牌!": "Veuillez sélectionner le jeton à supprimer !", + "请先选择要删除的通道!": "Veuillez d'abord sélectionner le canal que vous souhaitez supprimer !", + "请先选择要设置标签的渠道!": "Veuillez d'abord sélectionner le canal pour lequel définir les étiquettes !", + "请先选择需要批量设置的模型": "Veuillez d'abord sélectionner les modèles pour le paramétrage par lots", + "请先阅读并同意用户协议和隐私政策": "Veuillez d'abord lire et accepter l'accord utilisateur et la politique de confidentialité", + "请再次输入新密码": "Veuillez saisir à nouveau le nouveau mot de passe", + "请前往个人设置 → 安全设置进行配置。": "Veuillez aller dans Paramètres personnels → Paramètres de sécurité pour configurer.", + "请勿过度信任此功能,IP可能被伪造": "Ne faites pas trop confiance à cette fonctionnalité, l'IP peut être usurpée", + "请在系统设置页面编辑分组倍率以添加新的分组:": "Veuillez modifier les ratios de groupe dans les paramètres système pour ajouter de nouveaux groupes :", + "请填写完整的管理员账号信息": "Veuillez remplir les informations complètes du compte administrateur", + "请填写密钥": "Veuillez saisir la clé", + "请填写渠道名称和渠道密钥!": "Veuillez saisir le nom et la clé du canal !", + "请填写部署地区": "Veuillez remplir la région de déploiement", + "请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。": "Conservez les informations de clé en lieu sûr, ne les divulguez pas à d'autres. En cas de problèmes de sécurité, veuillez changer la clé immédiatement.", + "请检查渠道配置或刷新重试": "Veuillez vérifier la configuration du canal ou actualiser et réessayer", + "请检查表单填写是否正确": "Veuillez vérifier si le formulaire est correctement rempli", + "请检查输入": "Veuillez vérifier votre saisie", + "请求发生错误": "Une erreur s'est produite lors de la demande", + "请求发生错误: ": "Une erreur s'est produite lors de la demande : ", + "请求后端接口失败:": "Échec de la requête de l'interface backend : ", + "请求失败": "Échec de la demande", + "请求头覆盖": "Remplacement des en-têtes de demande", + "请求并计费模型": "Modèle de demande et de facturation", + "请求路径": "Chemin de requête", + "请求时长: ${time}s": "Durée de la requête : ${time}s", + "请求次数": "Nombre de demandes", + "请求结束后多退少补": "Ajuster après la fin de la demande", + "请求预扣费额度": "Quota de pré-déduction pour les demandes", + "请点击我": "Veuillez cliquer sur moi", + "请确认以下设置信息,点击\"初始化系统\"开始配置": "Veuillez confirmer les informations de configuration suivantes, cliquez sur \"Initialiser le système\" pour commencer la configuration", + "请确认您已了解禁用两步验证的后果": "Veuillez confirmer que vous comprenez les conséquences de la désactivation de l'authentification à deux facteurs", + "请确认管理员密码": "Veuillez confirmer le mot de passe de l'administrateur", + "请稍后几秒重试,Turnstile 正在检查用户环境!": "Veuillez réessayer dans quelques secondes, Turnstile vérifie l'environnement utilisateur !", + "请联系管理员在系统设置中配置API信息": "Veuillez contacter l'administrateur pour configurer les informations de l'API dans les paramètres système.", + "请联系管理员在系统设置中配置Uptime": "Veuillez contacter l'administrateur pour configurer Uptime dans les paramètres système.", + "请联系管理员在系统设置中配置公告信息": "Veuillez contacter l'administrateur pour configurer les informations d'avis dans les paramètres système.", + "请联系管理员在系统设置中配置常见问答": "Veuillez contacter l'administrateur pour configurer les informations de la FAQ dans les paramètres système.", + "请联系管理员配置聊天链接": "Veuillez contacter l'administrateur pour configurer le lien de chat", + "请至少选择一个令牌!": "Veuillez sélectionner au moins un jeton !", + "请至少选择一个兑换码!": "Veuillez sélectionner au moins un code d'échange !", + "请至少选择一个模型": "Veuillez sélectionner au moins un modèle", + "请至少选择一个模型!": "Veuillez sélectionner au moins un modèle !", + "请至少选择一个渠道": "Veuillez sélectionner au moins un canal", + "请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "Veuillez saisir AZURE_OPENAI_ENDPOINT, par exemple : https://docs-test-001.openai.azure.com", + "请输入 JSON 格式的密钥内容,例如:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}": "Veuillez saisir le contenu de la clé au format JSON, par exemple :\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}", + "请输入 OIDC 的 Well-Known URL": "Veuillez saisir l'URL Well-Known de l'OIDC", + "请输入6位验证码或8位备用码": "Veuillez saisir le code de vérification à 6 chiffres ou le code de sauvegarde à 8 chiffres", + "请输入API地址": "Veuillez saisir l'adresse de l'API", + "请输入API地址!": "Veuillez saisir l'adresse de l'API !", + "请输入Bark推送URL": "Veuillez saisir l'URL de notification Bark", + "请输入Bark推送URL,例如: https://api.day.app/yourkey/{{title}}/{{content}}": "Veuillez saisir l'URL de notification Bark, par exemple : https://api.day.app/yourkey/{{title}}/{{content}}", + "请输入Gotify应用令牌": "Veuillez saisir le jeton d'application Gotify", + "请输入Gotify服务器地址": "Veuillez saisir l'adresse du serveur Gotify", + "请输入Gotify服务器地址,例如: https://gotify.example.com": "Veuillez saisir l'adresse du serveur Gotify, par exemple : https://gotify.example.com", + "请输入Uptime Kuma地址": "Veuillez saisir l'adresse Uptime Kuma", + "请输入Uptime Kuma服务地址,如:https://status.example.com": "Veuillez saisir l'adresse du service Uptime Kuma, telle que : https://status.example.com", + "请输入URL链接": "Veuillez saisir le lien URL", + "请输入Webhook地址": "Veuillez saisir l'adresse du Webhook", + "请输入Webhook地址,例如: https://example.com/webhook": "Veuillez saisir l'URL du Webhook, par exemple : https://example.com/webhook", + "请输入你的账户名以确认删除!": "Veuillez saisir votre nom de compte pour confirmer la suppression !", + "请输入供应商名称": "Veuillez saisir le nom du fournisseur", + "请输入供应商名称,如:OpenAI": "Veuillez saisir le nom du fournisseur, tel que : OpenAI", + "请输入供应商描述": "Veuillez saisir la description du fournisseur", + "请输入兑换码": "Veuillez saisir le code d'échange", + "请输入兑换码!": "Veuillez saisir le code d'échange !", + "请输入公告内容": "Veuillez saisir le contenu de l'avis", + "请输入公告内容(支持 Markdown/HTML)": "Veuillez saisir le contenu de l'avis (prend en charge Markdown/HTML)", + "请输入分类名称": "Veuillez saisir le nom de la catégorie", + "请输入分类名称,如:OpenAI、Claude等": "Veuillez saisir le nom de la catégorie, tel que : OpenAI, Claude, etc.", + "请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com": "Veuillez saisir le chemin avant /suno, généralement le domaine, par exemple : https://api.example.com", + "请输入原密码": "Veuillez saisir le mot de passe original", + "请输入原密码!": "Veuillez saisir le mot de passe original !", + "请输入名称": "Veuillez saisir un nom", + "请输入回答内容": "Veuillez saisir le contenu de la réponse", + "请输入回答内容(支持 Markdown/HTML)": "Veuillez saisir le contenu de la réponse (prend en charge Markdown/HTML)", + "请输入图标名称": "Veuillez saisir le nom de l'icône", + "请输入填充值": "Veuillez saisir une valeur", + "请输入备注(仅管理员可见)": "Veuillez saisir une remarque (visible uniquement par les administrateurs)", + "请输入完整的 JSON 格式密钥内容": "Veuillez saisir le contenu complet de la clé au format JSON", + "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Veuillez saisir l'URL complète, par exemple : https://api.openai.com/v1/chat/completions", + "请输入完整的URL链接": "Veuillez saisir le lien URL complet", + "请输入密码": "Veuillez saisir un mot de passe", + "请输入密钥": "Veuillez saisir la clé", + "请输入密钥,一行一个": "Veuillez saisir la clé, une par ligne", + "请输入密钥!": "Veuillez saisir la clé !", + "请输入您的密码": "Veuillez saisir votre mot de passe", + "请输入您的用户名以确认删除": "Veuillez saisir votre nom d'utilisateur pour confirmer la suppression", + "请输入您的用户名或邮箱地址": "Veuillez saisir votre nom d'utilisateur ou votre adresse e-mail", + "请输入您的邮箱地址": "Veuillez saisir votre adresse e-mail", + "请输入您的问题...": "Veuillez saisir votre question...", + "请输入数值": "Saisir une valeur", + "请输入数字": "Veuillez saisir un nombre", + "请输入新密码": "Veuillez saisir le nouveau mot de passe", + "请输入新密码!": "Veuillez saisir le nouveau mot de passe !", + "请输入新建数量": "Veuillez saisir la quantité à créer", + "请输入新标签,留空则解散标签": "Veuillez saisir une nouvelle étiquette, laissez vide pour dissoudre l'étiquette", + "请输入新的剩余额度": "Veuillez saisir le nouveau quota restant", + "请输入新的密码,最短 8 位": "Veuillez saisir un nouveau mot de passe, d'au moins 8 caractères", + "请输入新的显示名称": "Veuillez saisir un nouveau nom d'affichage", + "请输入新的用户名": "Veuillez saisir un nouveau nom d'utilisateur", + "请输入显示名称": "Veuillez saisir un nom d'affichage", + "请输入有效的数字": "Veuillez saisir un nombre valide", + "请输入标签名称": "Veuillez saisir le nom de l'étiquette", + "请输入模型倍率": "Saisir le ratio de modèle", + "请输入模型倍率和补全倍率": "Veuillez saisir le ratio de modèle et le ratio d'achèvement", + "请输入模型名称": "Veuillez saisir le nom du modèle", + "请输入模型名称,如:gpt-4": "Veuillez saisir le nom du modèle, tel que : gpt-4", + "请输入模型描述": "Veuillez saisir la description du modèle", + "请输入消息内容...": "Veuillez saisir le contenu du message...", + "请输入状态页面Slug": "Veuillez saisir le Slug de la page d'état", + "请输入状态页面的Slug,如:my-status": "Veuillez saisir le slug de la page d'état, tel que : my-status", + "请输入生成数量": "Veuillez saisir la quantité à générer", + "请输入用户名": "Veuillez saisir un nom d'utilisateur", + "请输入私有部署地址,格式为:https://fastgpt.run/api/openapi": "Veuillez saisir l'adresse de déploiement privée, format : https://fastgpt.run/api/openapi", + "请输入管理员密码": "Veuillez saisir le mot de passe de l'administrateur", + "请输入管理员用户名": "Veuillez saisir le nom d'utilisateur de l'administrateur", + "请输入线路描述": "Veuillez saisir la description de l'itinéraire", + "请输入组名": "Veuillez saisir le nom du groupe", + "请输入组描述": "Veuillez saisir la description du groupe", + "请输入组织org-xxx": "Veuillez saisir l'organisation org-xxx", + "请输入聊天应用名称": "Veuillez saisir le nom de l'application de chat", + "请输入补全倍率": "Saisir le ratio d'achèvement", + "请输入要设置的标签名称": "Veuillez saisir le nom de l'étiquette à définir", + "请输入认证器验证码": "Veuillez saisir le code de vérification de l'authentificateur", + "请输入认证器验证码或备用码": "Veuillez saisir le code de vérification de l'authentificateur ou le code de sauvegarde", + "请输入说明": "Veuillez saisir la description", + "请输入邮箱!": "Veuillez saisir votre e-mail !", + "请输入邮箱地址": "Veuillez saisir l'adresse e-mail", + "请输入邮箱验证码!": "Veuillez saisir le code de vérification de l'e-mail !", + "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "Veuillez saisir la région de déploiement, par exemple : us-central1\nPrend en charge l'utilisation du format de mappage de modèle\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}", + "请输入问题标题": "Veuillez saisir le titre de la question", + "请输入预警阈值": "Veuillez saisir le seuil d'alerte", + "请输入预警额度": "Veuillez saisir le quota d'alerte", + "请输入额度": "Veuillez saisir le quota", + "请输入验证码": "Veuillez saisir le code de vérification", + "请输入验证码或备用码": "Veuillez saisir le code de vérification ou le code de sauvegarde", + "请输入默认 API 版本,例如:2025-04-01-preview": "Veuillez saisir la version de l'API par défaut, par exemple : 2025-04-01-preview.", + "请选择API地址": "Veuillez sélectionner l'adresse de l'API", + "请选择你的复制方式": "Veuillez sélectionner votre méthode de copie", + "请选择使用模式": "Veuillez sélectionner le mode d'utilisation", + "请选择分组": "Veuillez sélectionner un groupe", + "请选择发布日期": "Veuillez sélectionner la date de publication", + "请选择可以使用该渠道的分组": "Veuillez sélectionner les groupes qui peuvent utiliser ce canal", + "请选择可以使用该渠道的分组,留空则不更改": "Veuillez sélectionner les groupes qui peuvent utiliser ce canal, laisser vide ne changera rien", + "请选择同步语言": "Veuillez sélectionner la langue de synchronisation", + "请选择名称匹配类型": "Veuillez sélectionner le type de correspondance de nom", + "请选择多密钥使用策略": "Veuillez sélectionner la stratégie d'utilisation de plusieurs clés", + "请选择密钥更新模式": "Veuillez sélectionner le mode de mise à jour des clés", + "请选择密钥格式": "Veuillez sélectionner le format de clé", + "请选择日志记录时间": "Veuillez sélectionner l'heure d'enregistrement du journal", + "请选择模型": "Veuillez sélectionner un modèle", + "请选择模型。": "Veuillez sélectionner un modèle.", + "请选择消息优先级": "Veuillez sélectionner la priorité du message", + "请选择渠道类型": "Veuillez sélectionner le type de canal", + "请选择组类型": "Veuillez sélectionner le type de groupe", + "请选择该令牌支持的模型,留空支持所有模型": "Sélectionnez les modèles pris en charge par le jeton, laissez vide pour prendre en charge tous les modèles", + "请选择该渠道所支持的模型": "Veuillez sélectionner le modèle pris en charge par ce canal", + "请选择该渠道所支持的模型,留空则不更改": "Veuillez sélectionner les modèles pris en charge par le canal, laisser vide ne changera rien", + "请选择过期时间": "Veuillez sélectionner une date d'expiration", + "请选择通知方式": "Veuillez sélectionner la méthode de notification", + "调用次数": "Nombre d'appels", + "调用次数分布": "Distribution des appels de modèles", + "调用次数排行": "Classement des appels de modèles", + "调试信息": "Informations de débogage", + "谨慎": "Prudent", + "警告": "Avertissement", + "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔": "Avertissement : après l'activation du keep-alive, si une erreur de canal se produit après l'écriture des données de keep-alive, le système ne peut pas réessayer. Si vous devez l'activer, il est recommandé de définir un intervalle Ping aussi grand que possible", + "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "Avertissement : la désactivation de l'authentification à deux facteurs supprimera définitivement vos paramètres de vérification et tous les codes de sauvegarde. Cette action est irréversible !", + "豆包": "Doubao", + "账单": "Factures", + "账户充值": "Recharge de compte", + "账户已删除!": "Le compte a été supprimé !", + "账户已锁定": "Compte verrouillé", + "账户数据": "Données du compte", + "账户管理": "Gestion de compte", + "账户绑定": "Liaison de compte", + "账户绑定、安全设置和身份验证": "Liaison de compte, paramètres de sécurité et vérification d'identité", + "账户统计": "Statistiques du compte", + "货币单位": "Unité monétaire", + "购买兑换码": "Acheter un code d'échange", + "资源消耗": "Consommation de ressources", + "起始时间": "Heure de début", + "超级管理员": "Super Admin", + "超级管理员未设置充值链接!": "Le super administrateur n'a pas défini le lien de recharge !", + "跟随系统主题设置": "Suivre le thème du système", + "跳转": "Sauter", + "轮询": "Sondage", + "轮询模式": "Mode de sondage", + "轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能": "Le mode de sondage doit être utilisé avec les fonctionnalités Redis et cache mémoire, sinon les performances seront considérablement réduites et la fonctionnalité de sondage ne pourra pas être réalisée", + "输入": "Entrée", + "输入 OIDC 的 Authorization Endpoint": "Saisir le point de terminaison d'autorisation OIDC", + "输入 OIDC 的 Client ID": "Saisir l'ID client OIDC", + "输入 OIDC 的 Token Endpoint": "Saisir le point de terminaison de jeton OIDC", + "输入 OIDC 的 Userinfo Endpoint": "Saisir le point de terminaison des informations utilisateur OIDC", + "输入IP地址后回车,如:8.8.8.8": "Saisissez l'adresse IP et appuyez sur Entrée, par exemple : 8.8.8.8", + "输入JSON对象": "Saisir l'objet JSON", + "输入价格": "Saisir le prix", + "输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}": "Saisir le prix : {{symbol}}{{price}} / 1M tokens{{audioPrice}}", + "输入你注册的 LinuxDO OAuth APP 的 ID": "Saisir l'ID de votre application OAuth LinuxDO enregistrée", + "输入你的账户名{{username}}以确认删除": "Saisissez votre nom de compte{{username}}pour confirmer la suppression", + "输入域名后回车": "Saisissez le domaine et appuyez sur Entrée", + "输入域名后回车,如:example.com": "Saisissez le domaine et appuyez sur Entrée, par exemple : example.com", + "输入密码,最短 8 位,最长 20 位": "Saisissez un mot de passe, d'au moins 8 caractères et jusqu'à 20 caractères", + "输入数字": "Saisir un nombre", + "输入标签或使用\",\"分隔多个标签": "Saisissez des étiquettes ou utilisez \",\" pour séparer plusieurs étiquettes", + "输入模型倍率": "Saisir le ratio de modèle", + "输入每次价格": "Saisir le prix par utilisation", + "输入端口后回车,如:80 或 8000-8999": "Saisissez le port et appuyez sur Entrée, par exemple : 80 ou 8000-8999", + "输入系统提示词,用户的系统提示词将优先于此设置": "Saisissez l'invite système, l'invite système de l'utilisateur aura la priorité sur ce paramètre", + "输入自定义模型名称": "Saisir un nom de modèle personnalisé", + "输入补全价格": "Saisir le prix d'achèvement", + "输入补全倍率": "Saisir le ratio d'achèvement", + "输入要添加的邮箱域名": "Saisir le domaine e-mail à ajouter", + "输入认证器应用显示的6位数字验证码": "Saisissez le code de vérification à 6 chiffres affiché sur l'application d'authentification", + "输入邮箱地址": "Saisir l'adresse e-mail", + "输入项目名称,按回车添加": "Saisissez le nom de l'élément, appuyez sur Entrée pour ajouter", + "输入验证码": "Saisir le code de vérification", + "输入验证码完成设置": "Saisissez le code de vérification pour terminer la configuration", + "输出": "Sortie", + "输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}": "Sortie {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}", + "输出价格": "Prix de sortie", + "输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "Prix de sortie : {{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (ratio d'achèvement : {{completionRatio}})", + "边栏设置": "Paramètres de la barre latérale", + "过期时间": "Date d'expiration", + "过期时间不能早于当前时间!": "La date d'expiration ne peut pas être antérieure à l'heure actuelle !", + "过期时间快捷设置": "Paramètres rapides de la date d'expiration", + "过期时间格式错误!": "Erreur de format de la date d'expiration !", + "运营设置": "Paramètres de fonctionnement", + "返回登录": "Retour à la connexion", + "这是重复键中的最后一个,其值将被使用": "Ceci est la dernière clé dupliquée, sa valeur sera utilisée", + "进度": "calendrier", + "进行中": "En cours", + "进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "Lors de cette opération, cela peut entraîner des erreurs d'accès au canal. Veuillez ne l'utiliser que lorsqu'il y a un problème avec la base de données.", + "连接保活设置": "Paramètres de maintien de connexion", + "连接已断开": "Connexion interrompue", + "追加到现有密钥": "Ajouter aux clés existantes", + "追加模式:将新密钥添加到现有密钥列表末尾": "Mode d'ajout : ajouter les nouvelles clés à la fin de la liste de clés existantes", + "追加模式:新密钥将添加到现有密钥列表的末尾": "Mode d'ajout : les nouvelles clés seront ajoutées à la fin de la liste de clés existantes", + "退出": "Quitter", + "适用于个人使用的场景,不需要设置模型价格": "Adapté à un usage personnel, pas besoin de définir le prix du modèle.", + "适用于为多个用户提供服务的场景": "Adapté aux scénarios où plusieurs utilisateurs sont fournis.", + "适用于展示系统功能的场景,提供基础功能演示": "Adapté aux scénarios où les fonctions du système sont affichées, fournissant des démonstrations de fonctionnalités de base.", + "适配 -thinking、-thinking-预算数字 和 -nothinking 后缀": "Adapter les suffixes -thinking, -thinking-budget et -nothinking", + "选择充值额度": "Sélectionner le montant de la recharge", + "选择分组": "Sélectionner un groupe", + "选择同步来源": "Sélectionner la source de synchronisation", + "选择同步渠道": "Sélectionner le canal de synchronisation", + "选择同步语言": "Sélectionner la langue de synchronisation", + "选择成功": "Sélection réussie", + "选择支付方式": "Sélectionner le mode de paiement", + "选择支持的认证设备类型": "Choisissez les types d'appareils d'authentification pris en charge", + "选择方式": "Sélectionner la méthode", + "选择时间": "Sélectionner l'heure", + "选择模型": "Sélectionner un modèle", + "选择模型供应商": "Sélectionner le fournisseur du modèle", + "选择模型后可一键填充当前选中令牌(或本页第一个令牌)。": "Après avoir sélectionné un modèle, vous pouvez remplir en un clic le jeton actuellement sélectionné (ou le premier jeton de cette page).", + "选择模型开始对话": "Sélectionner un modèle pour commencer la conversation", + "选择端点类型": "Sélectionner le type de point de terminaison", + "选择系统运行模式": "Sélectionner le mode de fonctionnement du système", + "选择组类型": "Sélectionner le type de groupe", + "选择要覆盖的冲突项": "Sélectionner les éléments en conflit à remplacer", + "选择语言": "Sélectionner la langue", + "选择过期时间(可选,留空为永久)": "Sélectionnez la date d'expiration (facultatif, laissez vide pour permanent)", + "透传请求体": "Corps de transmission", + "通义千问": "Qwen", + "通用设置": "Paramètres généraux", + "通知": "Avis", + "通知、价格和隐私相关设置": "Paramètres de notification, de prix et de confidentialité", + "通知内容": "Contenu de la notification", + "通知内容,支持 {{value}} 变量占位符": "Contenu de la notification, prend en charge les espaces réservés de variable {{value}}", + "通知方式": "Méthode de notification", + "通知标题": "Titre de la notification", + "通知类型 (quota_exceed: 额度预警)": "Type de notification (quota_exceed : avertissement de quota)", + "通知邮箱": "E-mail de notification", + "通知配置": "Configuration des notifications", + "通过划转功能将奖励额度转入到您的账户余额中": "Transférez le montant de la récompense sur le solde de votre compte via la fonction de virement", + "通过密码注册时需要进行邮箱验证": "La vérification par e-mail est requise lors de l'inscription via mot de passe", + "通道 ${name} 余额更新成功!": "Le quota du canal ${name} a été mis à jour avec succès !", + "通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。": "Test du canal ${name} réussi, modèle ${model} a pris ${time.toFixed(2)} secondes.", + "通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Test du canal ${name} réussi, a pris ${time.toFixed(2)} secondes.", + "速率限制设置": "Paramètres de limitation de débit", + "邀请": "Invitations", + "邀请人": "Inviteur", + "邀请人数": "Nombre de personnes invitées", + "邀请信息": "Informations sur l'invitation", + "邀请奖励": "Récompense d'invitation", + "邀请好友注册,好友充值后您可获得相应奖励": "Invitez des amis à s'inscrire et vous pourrez obtenir la récompense correspondante après que l'ami ait rechargé", + "邀请好友获得额外奖励": "Invitez des amis pour obtenir des récompenses supplémentaires", + "邀请新用户奖励额度": "Quota de bonus de parrainage", + "邀请的好友越多,获得的奖励越多": "Plus vous invitez d'amis, plus vous obtiendrez de récompenses", + "邀请码": "Code d'invitation", + "邀请获得额度": "Quota d'invitation", + "邀请链接": "Lien d'invitation", + "邀请链接已复制到剪切板": "Le lien d'invitation a été copié dans le presse-papiers", + "邮件通知": "Notification par e-mail", + "邮箱": "E-mail", + "邮箱地址": "Adresse e-mail", + "邮箱域名格式不正确,请输入有效的域名,如 gmail.com": "Le format du domaine e-mail est incorrect, veuillez saisir un domaine valide, comme gmail.com", + "邮箱域名白名单格式不正确": "Le format de la liste blanche des domaines e-mail est incorrect", + "邮箱账户绑定成功!": "Liaison du compte e-mail réussie !", + "部分保存失败": "Certains paramètres n'ont pas pu être enregistrés", + "部分保存失败,请重试": "Échec de l'enregistrement partiel, veuillez réessayer", + "部分渠道测试失败:": "Certains canaux n'ont pas réussi le test : ", + "部署地区": "Région de déploiement", + "配置": "Configurer", + "配置 GitHub OAuth App": "Configurer l'application GitHub OAuth", + "配置 Linux DO OAuth": "Configurer Linux DO OAuth", + "配置 OIDC": "Configurer OIDC", + "配置 Passkey": "Configurer Passkey", + "配置 SMTP": "Configurer SMTP", + "配置 Telegram 登录": "Configurer la connexion Telegram", + "配置 Turnstile": "Configurer Turnstile", + "配置 WeChat Server": "Configurer le serveur WeChat", + "配置和消息已全部重置": "La configuration et les messages ont été entièrement réinitialisés", + "配置导入成功": "Importation de la configuration réussie", + "配置已导出到下载文件夹": "La configuration a été exportée vers le dossier de téléchargement", + "配置已重置,对话消息已保留": "La configuration a été réinitialisée, les messages de conversation ont été conservés", + "配置文件同步": "Synchronisation du fichier de configuration", + "配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全": "Configurez la protection contre la falsification de requêtes côté serveur (SSRF) pour sécuriser les ressources du réseau interne", + "配置登录注册": "Configurer la connexion/l'inscription", + "配置说明": "Instructions de configuration", + "配置邮箱域名白名单": "Configurer la liste blanche des domaines e-mail", + "重复提交": "Soumission en double", + "重复的键名": "Nom de clé dupliqué", + "重复的键名,此值将被后面的同名键覆盖": "Nom de clé dupliqué, cette valeur sera écrasée par la clé du même nom qui suit", + "重定向 URL 填": "URL de redirection", + "重新发送": "Renvoyer", + "重新生成": "Régénérer", + "重新生成备用码": "Régénérer les codes de sauvegarde", + "重新生成备用码失败": "Échec de la régénération des codes de sauvegarde", + "重新生成备用码将使现有的备用码失效,请确保您已保存了当前的备用码。": "La régénération des codes de sauvegarde invalidera les codes de sauvegarde existants. Veuillez vous assurer que vous avez enregistré les codes de sauvegarde actuels.", + "重绘": "Varier", + "重置": "Réinitialiser", + "重置 2FA": "Réinitialiser 2FA", + "重置 Passkey": "Réinitialiser le Passkey", + "重置为默认": "Réinitialiser aux valeurs par défaut", + "重置模型倍率": "Réinitialiser le ratio de modèle", + "重置选项": "Options de réinitialisation", + "重置邮件发送成功,请检查邮箱!": "L'e-mail de réinitialisation a été envoyé avec succès, veuillez vérifier votre e-mail !", + "重置配置": "Réinitialiser la configuration", + "重试": "Réessayer", + "钱包管理": "Gestion du portefeuille", + "链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1": "Le {key} dans le lien sera automatiquement remplacé par sk-xxxx, le {address} sera automatiquement remplacé par l'adresse du serveur dans les paramètres système, et la fin n'aura pas / et /v1", + "错误": "Erreur", + "键为分组名称,值为另一个 JSON 对象,键为分组名称,值为该分组的用户的特殊分组倍率,例如:{\"vip\": {\"default\": 0.5, \"test\": 1}},表示 vip 分组的用户在使用default分组的令牌时倍率为0.5,使用test分组时倍率为1": "La clé est le nom du groupe, la valeur est un autre objet JSON, la clé est le nom du groupe, la valeur est le ratio de groupe spécial des utilisateurs de ce groupe, par exemple : {\"vip\": {\"default\": 0.5, \"test\": 1}}, ce qui signifie que les utilisateurs du groupe vip ont un ratio de 0.5 lors de l'utilisation de jetons du groupe default et un ratio de 1 lors de l'utilisation du groupe test", + "键为原状态码,值为要复写的状态码,仅影响本地判断": "La clé est le code d'état d'origine, la valeur est le code d'état à réécrire, n'affecte que le jugement local", + "键为端点类型,值为路径和方法对象": "La clé est le type de point de terminaison, la valeur est le chemin et l'objet de la méthode", + "键为请求中的模型名称,值为要替换的模型名称": "La clé est le nom du modèle dans la requête, la valeur est le nom du modèle à remplacer", + "键名": "Nom de clé", + "问题标题": "Titre de la question", + "队列中": "En file d'attente", + "降低您账户的安全性": "Réduire la sécurité de votre compte", + "降级": "Rétrograder", + "限制周期": "Période de limite", + "限制周期统一使用上方配置的“限制周期”值。": "La période de limite utilise uniformément la valeur \"période de limite\" configurée ci-dessus.", + "隐私政策": "Politique de confidentialité", + "隐私政策已更新": "La politique de confidentialité a été mise à jour", + "隐私政策更新失败": "Échec de la mise à jour de la politique de confidentialité", + "隐私设置": "Paramètres de confidentialité", + "隐藏操作项": "Masquer les actions", + "隐藏调试": "Masquer le débogage", + "随机": "Aléatoire", + "随机模式": "Mode aléatoire", + "零一万物": "Yi", + "需要安全验证": "Vérification de sécurité requise", + "需要添加的额度(支持负数)": "Besoin d'ajouter un quota (prend en charge les nombres négatifs)", + "需要登录访问": "Nécessite une connexion", + "需要重新完整设置才能再次启用": "Nécessite une nouvelle configuration pour être réactivé", + "非必要,不建议启用模型限制": "Non nécessaire, les restrictions de modèle ne sont pas recommandées", + "非流": "Non flux", + "音频倍率(仅部分模型支持该计费)": "Ratio audio (seuls certains modèles prennent en charge cette facturation)", + "音频提示 {{input}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} = {{symbol}}{{total}}": "Invite audio {{input}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + achèvement audio {{completion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} = {{symbol}}{{total}}", + "音频提示价格:{{symbol}}{{price}} * {{audioRatio}} = {{symbol}}{{total}} / 1M tokens (音频倍率: {{audioRatio}})": "Prix de l'invite audio : {{symbol}}{{price}} * {{audioRatio}} = {{symbol}}{{total}} / 1M tokens (ratio audio : {{audioRatio}})", + "音频补全价格:{{symbol}}{{price}} * {{audioRatio}} * {{audioCompRatio}} = {{symbol}}{{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})": "Prix d'achèvement audio : {{symbol}}{{price}} * {{audioRatio}} * {{audioCompRatio}} = {{symbol}}{{total}} / 1M tokens (ratio d'achèvement audio : {{audioCompRatio}})", + "音频补全倍率(仅部分模型支持该计费)": "Ratio d'achèvement audio (seuls certains modèles prennent en charge cette facturation)", + "音频输入相关的倍率设置,键为模型名称,值为倍率": "Paramètres de ratio liés à l'entrée audio, la clé est le nom du modèle, la valeur est le ratio", + "音频输出补全相关的倍率设置,键为模型名称,值为倍率": "Paramètres de ratio liés à l'achèvement de la sortie audio, la clé est le nom du modèle, la valeur est le ratio", + "页脚": "Pied de page", + "页面未找到,请检查您的浏览器地址是否正确": "Page non trouvée, veuillez vérifier si l'adresse de votre navigateur est correcte", + "顶栏管理": "Gestion de l'en-tête", + "项目": "Élément", + "项目内容": "Contenu de l'élément", + "项目操作按钮组": "Groupe de boutons d'action du projet", + "预填组管理": "Groupe pré-rempli", + "预览失败": "Échec de l'aperçu", + "预览更新": "Mise à jour de l'aperçu", + "预览请求体": "Aperçu du corps de la requête", + "预警阈值必须为正数": "Le seuil d'alerte doit être un nombre positif", + "频率限制的周期(分钟)": "Période de limitation de débit (minutes)", + "颜色": "Couleur", + "额度": "Quota", + "额度必须大于0": "Le quota doit être supérieur à 0", + "额度提醒阈值": "Seuil de rappel de quota", + "额度查询接口返回令牌额度而非用户额度": "Affiche le quota de jetons au lieu du quota utilisateur", + "额度设置": "Paramètres de quota", + "额度预警阈值": "Seuil d'avertissement de quota", + "首尾生视频": "Vidéo de début et de fin", + "首页": "Accueil", + "首页内容": "Contenu de la page d'accueil", + "验证": "Vérifier", + "验证 Passkey": "Vérifier Passkey", + "验证失败,请重试": "Échec de la vérification, veuillez réessayer", + "验证成功": "Vérification réussie", + "验证数据库连接状态": "Vérifier l'état de la connexion à la base de données", + "验证码": "Code de vérification", + "验证码发送成功,请检查邮箱!": "Le code de vérification a été envoyé avec succès, veuillez vérifier votre e-mail !", + "验证设置": "Vérifier la configuration", + "验证身份": "Vérifier l'identité", + "验证配置错误": "Erreur de configuration de vérification", + "高级设置": "Paramètres avancés", + "黑名单": "Liste noire", + "默认": "Par défaut", + "默认 API 版本": "Version de l'API par défaut", + "默认 Responses API 版本,为空则使用上方版本": "Version de l'API Responses par défaut, utilise la version ci-dessus si vide", + "默认使用系统名称": "Le nom du système est utilisé par défaut", + "默认区域": "Région par défaut", + "默认区域,如: us-central1": "Région par défaut, ex: us-central1", + "默认折叠侧边栏": "Réduire la barre latérale par défaut", + "默认测试模型": "Modèle de test par défaut", + "默认补全倍率": "Taux de complétion par défaut" + } +} diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json new file mode 100644 index 000000000..db0a47626 --- /dev/null +++ b/web/src/i18n/locales/ru.json @@ -0,0 +1,2094 @@ +{ + "translation": { + " + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_one": " + Web-поиск {{count}} раз / 1K раз * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_few": " + Web-поиск {{count}} раза / 1K раз * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_many": " + Web-поиск {{count}} раз / 1K раз * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_other": " + Web-поиск {{count}} раз / 1K раз * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + 图片生成调用 {{symbol}}{{price}} / 1次 * {{ratioType}} {{ratio}}": " + Генерация изображения {{symbol}}{{price}} / 1 вызов * {{ratioType}} {{ratio}}", + " + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_one": " + Поиск файлов {{count}} раз / 1K раз * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_few": " + Поиск файлов {{count}} раза / 1K раз * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_many": " + Поиск файлов {{count}} раз / 1K раз * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_other": " + Поиск файлов {{count}} раз / 1K раз * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " 个模型设置相同的值": " моделей с одинаковыми значениями настроек", + " 吗?": "?", + " 秒": " сек", + ",时间:": ", время: ", + ",点击更新": ", нажмите для обновления", + "(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(В настоящее время поддерживается только интерфейс YiPay, по умолчанию используется адрес сервера выше в качестве адреса обратного вызова!)", + "(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "(Ввод {{input}} токенов / 1M токенов * {{symbol}}{{price}}", + "(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(Ввод {{nonAudioInput}} токенов / 1M токенов * {{symbol}}{{price}} + аудио ввод {{audioInput}} токенов / 1M токенов * {{symbol}}{{audioPrice}}", + "(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}": "(Ввод {{nonCacheInput}} токенов / 1M токенов * {{symbol}}{{price}} + кэш {{cacheInput}} токенов / 1M токенов * {{symbol}}{{cachePrice}}", + "(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * {{symbol}}{{price}}": "(Ввод {{nonImageInput}} токенов + ввод изображения {{imageInput}} токенов * {{imageRatio}} / 1M токенов * {{symbol}}{{price}}", + "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。": "[Максимальное количество запросов] и [Максимальное количество выполненных запросов] имеют максимальное значение 2147483647.", + "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。": "[Максимальное количество запросов] должно быть больше или равно 0, [Максимальное количество выполненных запросов] должно быть больше или равно 1.", + "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}": "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}", + "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}", + "© {{currentYear}}": "© {{currentYear}}", + "| 基于": "| Основано на", + "$/1M tokens": "$/1M токенов", + "0 - 最低": "0 - Минимум", + "0.002-1之间的小数": "Десятичное число между 0.002-1", + "0.1以上的小数": "Десятичное число выше 0.1", + "10 - 最高": "10 - Максимум", + "2 - 低": "2 - Низкий", + "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "Каналы, добавленные после 10 мая 2025 года, не требуют удаления \".\" из имен моделей при развертывании", + "360智脑": "360 ZhiNao", + "5 - 正常(默认)": "5 - Нормальный (по умолчанию)", + "8 - 高": "8 - Высокий", + "AGPL v3.0协议": "Лицензия AGPL v3.0", + "AI 对话": "AI диалог", + "AI模型测试环境": "Среда тестирования AI моделей", + "AI模型配置": "Конфигурация AI моделей", + "API Key 模式下不支持批量创建": "Режим API Key не поддерживает массовое создание", + "API 地址和相关配置": "Адрес API и связанные настройки", + "API 密钥": "Ключ API", + "API 文档": "Документация API", + "API 配置": "Конфигурация API", + "API令牌管理": "Управление токенами API", + "API使用记录": "История использования API", + "API信息": "Информация об API", + "API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)": "Управление информацией API, можно настроить несколько адресов API для отображения статуса и балансировки нагрузки (максимум 50)", + "API地址": "Адрес API", + "API渠道配置": "Конфигурация каналов API", + "API端点": "Конечная точка API", + "Authorization callback URL 填": "URL обратного вызова авторизации:", + "Authorization Endpoint": "Конечная точка авторизации", + "auto分组调用链路": "Цепочка вызовов автоматической группировки", + "Bark推送URL": "URL для push-уведомлений Bark", + "Bark推送URL必须以http://或https://开头": "URL для push-уведомлений Bark должен начинаться с http:// или https://", + "Bark通知": "Уведомления Bark", + "Changing batch type to:": "Изменение типа пакета на:", + "Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Адаптация мышления Claude BudgetTokens = MaxTokens * процент BudgetTokens", + "Claude设置": "Настройки Claude", + "Claude请求头覆盖": "Переопределение заголовков запроса Claude", + "Client ID": "ID клиента", + "Client Secret": "Секрет клиента", + "common.changeLanguage": "common.changeLanguage", + "default为默认设置,可单独设置每个分类的安全等级": "default - это настройка по умолчанию, можно отдельно установить уровень безопасности для каждой категории", + "default为默认设置,可单独设置每个模型的版本": "default - это настройка по умолчанию, можно отдельно установить версию для каждой модели", + "Dify渠道只适配chatflow和agent,并且agent不支持图片!": "Канал Dify адаптирован только для chatflow и agent, и agent не поддерживает изображения!", + "false": "false", + "Gemini安全设置": "Настройки безопасности Gemini", + "Gemini思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Адаптация мышления Gemini BudgetTokens = MaxTokens * процент BudgetTokens", + "Gemini思考适配设置": "Настройки адаптации мышления Gemini", + "Gemini版本设置": "Настройки версии Gemini", + "Gemini设置": "Настройки Gemini", + "GitHub": "GitHub", + "GitHub Client ID": "ID клиента GitHub", + "GitHub Client Secret": "Секрет клиента GitHub", + "GitHub ID": "ID GitHub", + "Gotify应用令牌": "Токен приложения Gotify", + "Gotify服务器地址": "Адрес сервера Gotify", + "Gotify服务器地址必须以http://或https://开头": "Адрес сервера Gotify должен начинаться с http:// или https://", + "Gotify通知": "Уведомления Gotify", + "Homepage URL 填": "URL домашней страницы:", + "ID": "ID", + "IP": "IP", + "IP白名单": "Белый список IP", + "IP限制": "Ограничения IP", + "IP黑名单": "Черный список IP", + "JSON": "JSON", + "JSON 模式支持手动输入或上传服务账号 JSON": "Режим JSON поддерживает ручной ввод или загрузку JSON сервисного аккаунта", + "JSON格式密钥,请确保格式正确": "Ключ в формате JSON, убедитесь в правильности формата", + "JSON编辑": "Редактирование JSON", + "JSON解析错误:": "Ошибка парсинга JSON:", + "Linux DO Client ID": "ID клиента Linux DO", + "Linux DO Client Secret": "Секрет клиента Linux DO", + "LinuxDO": "LinuxDO", + "LinuxDO ID": "ID LinuxDO", + "Logo 图片地址": "Адрес изображения логотипа", + "Midjourney 任务记录": "Записи задач Midjourney", + "MIT许可证": "Лицензия MIT", + "New API项目仓库地址:": "Адрес репозитория проекта New API:", + "OIDC": "OIDC", + "OIDC ID": "ID OIDC", + "Passkey": "Passkey", + "Passkey 已解绑": "Passkey отвязан", + "Passkey 已重置": "Passkey сброшен", + "Passkey 是基于 WebAuthn 标准的无密码身份验证方法,支持指纹、面容、硬件密钥等认证方式": "Passkey - это метод аутентификации без пароля на основе стандарта WebAuthn, поддерживающий отпечатки пальцев, распознавание лиц, аппаратные ключи и другие способы аутентификации", + "Passkey 注册失败,请重试": "Регистрация Passkey не удалась, попробуйте еще раз", + "Passkey 注册成功": "Регистрация Passkey успешна", + "Passkey 登录": "Вход через Passkey", + "Ping间隔(秒)": "Интервал Ping (секунды)", + "price_xxx 的商品价格 ID,新建产品后可获得": "ID цены товара price_xxx, можно получить после создания нового продукта", + "Reasoning Effort": "Усилие рассуждения", + "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "Поле safety_identifier помогает OpenAI идентифицировать пользователей приложений, которые могут нарушать политику использования. По умолчанию отключено для защиты конфиденциальности пользователей", + "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "Поле service_tier используется для указания уровня сервиса, позволяет передавать параметры, которые могут привести к фактической оплате выше ожидаемой. По умолчанию отключено для избежания дополнительных расходов", + "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "Ключ Stripe sk_xxx или rk_xxx, конфиденциальная информация не отображается", + "SMTP 发送者邮箱": "Email отправителя SMTP", + "SMTP 服务器地址": "Адрес сервера SMTP", + "SMTP 端口": "Порт SMTP", + "SMTP 访问凭证": "Учетные данные доступа SMTP", + "SMTP 账户": "Учетная запись SMTP", + "SSRF防护开关详细说明": "Подробное описание переключателя защиты SSRF", + "SSRF防护设置": "Настройки защиты SSRF", + "SSRF防护详细说明": "Подробное описание защиты SSRF", + "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "Поле store используется для авторизации OpenAI хранить данные запросов для оценки и оптимизации продукта. По умолчанию отключено, после включения может привести к неработоспособности Codex", + "Stripe 设置": "Настройки Stripe", + "Telegram": "Telegram", + "Telegram Bot Token": "Токен бота Telegram", + "Telegram Bot 名称": "Имя бота Telegram", + "Telegram ID": "ID Telegram", + "Token Endpoint": "Конечная точка токена", + "true": "true", + "Turnstile Secret Key": "Секретный ключ Turnstile", + "Turnstile Site Key": "Ключ сайта Turnstile", + "Unix时间戳": "Временная метка Unix", + "Uptime Kuma地址": "Адрес Uptime Kuma", + "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Управление категориями мониторинга Uptime Kuma, можно настроить несколько категорий мониторинга для отображения статуса сервисов (максимум 20)", + "URL链接": "URL ссылка", + "User Info Endpoint": "Конечная точка информации о пользователе", + "Webhook 签名密钥": "Ключ подписи Webhook", + "Webhook地址": "Адрес Webhook", + "Webhook地址必须以https://开头": "Адрес Webhook должен начинаться с https://", + "Webhook请求结构说明": "Описание структуры запроса Webhook", + "Webhook通知": "Уведомления Webhook", + "Web搜索价格:{{symbol}}{{price}} / 1K 次": "Цена Web-поиска: {{symbol}}{{price}} / 1K раз", + "WeChat Server 服务器地址": "Адрес сервера WeChat Server", + "WeChat Server 访问凭证": "Учетные данные доступа WeChat Server", + "Well-Known URL": "Well-Known URL", + "Well-Known URL 必须以 http:// 或 https:// 开头": "Well-Known URL должен начинаться с http:// или https://", + "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示": "Ключ подписи Webhook whsec_xxx, конфиденциальная информация не отображается", + "Worker地址": "Адрес Worker", + "Worker密钥": "Ключ Worker", + "一个月": "Один месяц", + "一天": "Один день", + "一小时": "Один час", + "一次调用消耗多少刀,优先级大于模型倍率": "Сколько долларов потребляется за один вызов, приоритет выше чем коэффициент модели", + "一行一个,不区分大小写": "Один элемент на строку, без учета регистра", + "一行一个屏蔽词,不需要符号分割": "Одно запрещенное слово на строку, не требуют разделителей", + "一键填充到 FluentRead": "Однократное заполнение в FluentRead", + "上一个表单块": "Предыдущий блок формы", + "上一步": "Предыдущий шаг", + "上次保存: ": "Последнее сохранение: ", + "上游倍率同步": "Синхронизация множителей upstream", + "下一个表单块": "Следующий блок формы", + "下一步": "Следующий шаг", + "下午好": "Добрый день", + "不再提醒": "Больше не напоминать", + "不同用户分组的价格信息": "Информация о ценах для разных групп пользователей", + "不填则为模型列表第一个": "Если не заполнено, используется первая модель из списка", + "不建议使用": "Не рекомендуется использовать", + "不支持": "Не поддерживается", + "不是合法的 JSON 字符串": "Недопустимая JSON строка", + "不更改": "Не изменять", + "不限制": "Без ограничений", + "与本地相同": "Так же как локально", + "专属倍率": "Специальный коэффициент", + "两次输入的密码不一致": "Введенные пароли не совпадают", + "两次输入的密码不一致!": "Введенные пароли не совпадают!", + "两步验证": "Двухфакторная аутентификация", + "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "Двухфакторная аутентификация (2FA) предоставляет дополнительную защиту для вашего аккаунта. После включения, при входе потребуется вводить пароль и код подтверждения из приложения аутентификатора.", + "两步验证启用成功!": "Двухфакторная аутентификация успешно включена!", + "两步验证已禁用": "Двухфакторная аутентификация отключена", + "两步验证设置": "Настройки двухфакторной аутентификации", + "个": "шт.", + "个人中心": "Личный кабинет", + "个人中心区域": "Область личного кабинета", + "个人信息设置": "Настройки личной информации", + "个人设置": "Личные настройки", + "个性化设置": "Персонализированные настройки", + "个性化设置左侧边栏的显示内容": "Настройка отображения содержимого левой боковой панели", + "个未配置模型": " не настроенных моделей", + "个模型": " моделей", + "中午好": "Добрый день", + "为一个 JSON 对象,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}": "Является JSON объектом, например: {\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}", + "为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]": "Является JSON массивом, например: [10, 20, 50, 100, 200, 500]", + "为一个 JSON 文本": "Является JSON текстом", + "为一个 JSON 文本,例如:": "Является JSON текстом, например:", + "为一个 JSON 文本,键为分组名称,值为倍率": "Является JSON текстом, ключ - имя группы, значение - коэффициент", + "为一个 JSON 文本,键为分组名称,值为分组描述": "Является JSON текстом, ключ - имя группы, значение - описание группы", + "为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 \"gpt-4-gizmo-*\": 0.1,一次消耗0.1刀": "Является JSON текстом, ключ - имя модели, значение - сколько долларов потребляется за один вызов, например \"gpt-4-gizmo-*\": 0.1, один вызов потребляет 0.1 долларов", + "为一个 JSON 文本,键为模型名称,值为倍率": "Является JSON текстом, ключ - имя модели, значение - коэффициент", + "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-audio-preview\": 16}": "Является JSON текстом, ключ - имя модели, значение - коэффициент, например: {\"gpt-4o-audio-preview\": 16}", + "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-realtime\": 2}": "Является JSON текстом, ключ - имя модели, значение - коэффициент, например: {\"gpt-4o-realtime\": 2}", + "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-image-1\": 2}": "Является JSON текстом, ключ - имя модели, значение - коэффициент, например: {\"gpt-image-1\": 2}", + "为一个 JSON 文本,键为组名称,值为倍率": "Является JSON текстом, ключ - имя группы, значение - коэффициент", + "为了保护账户安全,请验证您的两步验证码。": "Для защиты безопасности вашего аккаунта, пожалуйста, подтвердите ваш код двухфакторной аутентификации.", + "为了保护账户安全,请验证您的身份。": "Для защиты безопасности вашего аккаунта, пожалуйста, подтвердите вашу личность.", + "为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https": "Если пусто, используется адрес сервера по умолчанию. Несколько Origin разделяются запятыми, например https://newapi.pro,https://newapi.com. Обратите внимание, что нельзя использовать [], необходимо использовать https", + "主页链接填": "Введите ссылку на главную страницу", + "之前的所有日志": "Все предыдущие журналы", + "二步验证已重置": "Двухфакторная аутентификация сброшена", + "仅会覆盖你勾选的字段,未勾选的字段保持本地不变。": "Будут перезаписаны только отмеченные поля, неотмеченные поля останутся без изменений локально.", + "仅供参考,以实际扣费为准": "Только для справки, фактическое списание может отличаться", + "仅保存": "Только сохранить", + "仅修改展示粒度,统计精确到小时": "Только изменить детализацию отображения, статистика с точностью до часа", + "仅密钥": "Только ключ", + "仅对自定义模型有效": "Действительно только для пользовательских моделей", + "仅当自动禁用开启时有效,关闭后不会自动禁用该渠道": "Действительно только при включенном автоматическом отключении, после выключения канал не будет отключаться автоматически", + "仅支持": "Поддерживается только", + "仅支持 JSON 文件": "Поддерживаются только JSON файлы", + "仅支持 JSON 文件,支持多文件": "Поддерживаются только JSON файлы, поддерживается несколько файлов", + "仅支持 OpenAI 接口格式": "Поддерживается только формат интерфейса OpenAI", + "仅显示矛盾倍率": "Отображать только противоречивые коэффициенты", + "仅用于开发环境,生产环境应使用 HTTPS": "Только для среды разработки, в производственной среде следует использовать HTTPS", + "仅重置配置": "Только сбросить конфигурацию", + "今日关闭": "Закрыть сегодня", + "从官方模型库同步": "Синхронизировать из официальной библиотеки моделей", + "从认证器应用中获取验证码,或使用备用码": "Получите код подтверждения из приложения аутентификатора или используйте резервный код", + "从配置文件同步": "Синхронизировать из файла конфигурации", + "代理地址": "Адрес прокси", + "代理设置": "Настройки прокси", + "代码已复制到剪贴板": "Код скопирован в буфер обмена", + "令牌": "Токен", + "令牌分组": "Группа токенов", + "令牌分组,默认为用户的分组": "Группа токенов, по умолчанию используется группа пользователя", + "令牌创建成功,请在列表页面点击复制获取令牌!": "Токен успешно создан, пожалуйста, нажмите копировать на странице списка для получения токена!", + "令牌名称": "Имя токена", + "令牌已重置并已复制到剪贴板": "Токен сброшен и скопирован в буфер обмена", + "令牌更新成功!": "Токен успешно обновлен!", + "令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制": "Лимит токена используется только для ограничения максимального использования самого токена, фактическое использование ограничено остаточным лимитом аккаунта", + "令牌管理": "Управление токенами", + "以下上游数据可能不可信:": "Следующие upstream данные могут быть недостоверными:", + "以下文件解析失败,已忽略:{{list}}": "Не удалось проанализировать следующие файлы, они проигнорированы: {{list}}", + "以及": "а также", + "仪表盘设置": "Настройки панели управления", + "价格": "Цена", + "价格:${{price}} * {{ratioType}}:{{ratio}}": "Цена: ${{price}} * {{ratioType}}: {{ratio}}", + "价格设置": "Настройки цен", + "价格设置方式": "Способ настройки цен", + "任务 ID": "ID задачи", + "任务ID": "ID задачи", + "任务日志": "Журнал задач", + "任务状态": "Статус задачи", + "任务记录": "Записи задач", + "企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选": "Корпоративные аккаунты имеют специальный формат возврата, требующий специальной обработки. Если это не корпоративный аккаунт, не отмечайте этот пункт", + "优先级": "Приоритет", + "优惠": "Скидка", + "低于此额度时将发送邮件提醒用户": "Когда баланс ниже этого лимита, пользователю будет отправлено email напоминание", + "余额": "Баланс", + "余额充值管理": "Управление пополнением баланса", + "你似乎并没有修改什么": "Похоже, вы ничего не изменили", + "使用 GitHub 继续": "Продолжить с GitHub", + "使用 JSON 对象格式,格式为:{\"组名\": [最多请求次数, 最多请求完成次数]}": "Используйте формат объекта JSON, формат: {\"Имя группы\": [Максимальное количество запросов, Максимальное количество выполненных запросов]}", + "使用 LinuxDO 继续": "Продолжить с LinuxDO", + "使用 OIDC 继续": "Продолжить с OIDC", + "使用 Passkey 实现免密且更安全的登录体验": "Используйте Passkey для безпарольного и более безопасного входа", + "使用 Passkey 登录": "Войти с Passkey", + "使用 Passkey 验证": "Проверить с Passkey", + "使用 微信 继续": "Продолжить с WeChat", + "使用 用户名 注册": "Зарегистрироваться с именем пользователя", + "使用 邮箱或用户名 登录": "Войти с email или именем пользователя", + "使用ID排序": "Сортировать по ID", + "使用日志": "Журнал использования", + "使用模式": "Режим использования", + "使用统计": "Статистика использования", + "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "Отсканируйте QR-код ниже с помощью приложения аутентификатора (например, Google Authenticator, Microsoft Authenticator):", + "使用认证器应用扫描二维码": "Отсканировать QR-код с помощью приложения аутентификатора", + "例如 €, £, Rp, ₩, ₹...": "Например €, £, Rp, ₩, ₹...", + "例如 https://docs.newapi.pro": "Например https://docs.newapi.pro", + "例如:": "например:", + "例如: socks5://user:pass@host:port": "например: socks5://user:pass@host:port", + "例如:0001": "например: 0001", + "例如:1000": "например: 1000", + "例如:2,就是最低充值2$": "например: 2, это минимальное пополнение 2$", + "例如:2000": "например: 2000", + "例如:7,就是7元/美金": "например: 7, это 7 юаней/доллар США", + "例如:example.com": "например: example.com", + "例如:https://yourdomain.com": "например: https://yourdomain.com", + "例如:preview": "например: preview", + "例如发卡网站的购买链接": "например ссылка на покупку на сайте карт", + "供应商": "Поставщик", + "供应商介绍": "Описание поставщика", + "供应商信息:": "Информация о поставщике:", + "供应商创建成功!": "Поставщик успешно создан!", + "供应商删除成功": "Поставщик успешно удален", + "供应商名称": "Название поставщика", + "供应商图标": "Иконка поставщика", + "供应商更新成功!": "Поставщик успешно обновлен!", + "侧边栏管理(全局控制)": "Управление боковой панелью (глобальный контроль)", + "侧边栏设置保存成功": "Настройки боковой панели успешно сохранены", + "保存": "Сохранить", + "保存 GitHub OAuth 设置": "Сохранить настройки GitHub OAuth", + "保存 Linux DO OAuth 设置": "Сохранить настройки LinuxDO OAuth", + "保存 OIDC 设置": "Сохранить настройки OIDC", + "保存 Passkey 设置": "Сохранить настройки Passkey", + "保存 SMTP 设置": "Сохранить настройки SMTP", + "保存 Telegram 登录设置": "Сохранить настройки входа через Telegram", + "保存 Turnstile 设置": "Сохранить настройки Turnstile", + "保存 WeChat Server 设置": "Сохранить настройки WeChat Server", + "保存分组倍率设置": "Сохранить настройки коэффициентов групп", + "保存备用码": "Сохранить резервные коды", + "保存备用码以备不时之需": "Сохраните резервные коды на случай необходимости", + "保存失败": "Не удалось сохранить", + "保存失败,请重试": "Не удалось сохранить, попробуйте еще раз", + "保存失败:": "Не удалось сохранить:", + "保存屏蔽词过滤设置": "Сохранить настройки фильтрации запрещенных слов", + "保存成功": "Успешно сохранено", + "保存数据看板设置": "Сохранить настройки панели данных", + "保存日志设置": "Сохранить настройки журнала", + "保存模型倍率设置": "Сохранить настройки коэффициентов моделей", + "保存模型速率限制": "Сохранить ограничения скорости моделей", + "保存监控设置": "Сохранить настройки мониторинга", + "保存绘图设置": "Сохранить настройки рисования", + "保存聊天设置": "Сохранить настройки чата", + "保存设置": "Сохранить настройки", + "保存通用设置": "Сохранить общие настройки", + "保存邮箱域名白名单设置": "Сохранить настройки белого списка доменов email", + "保存额度设置": "Сохранить настройки лимитов", + "修复数据库一致性": "Исправить согласованность базы данных", + "修改为": "Изменить на", + "修改子渠道优先级": "Изменить приоритет дочерних каналов", + "修改子渠道权重": "Изменить вес дочерних каналов", + "修改密码": "Изменить пароль", + "修改绑定": "Изменить привязку", + "倍率": "Коэффициент", + "倍率信息": "Информация о коэффициентах", + "倍率是为了方便换算不同价格的模型": "Коэффициенты предназначены для удобного пересчета моделей с разными ценами", + "倍率模式": "Режим коэффициентов", + "倍率类型": "Тип коэффициента", + "停止测试": "Остановить тест", + "允许 AccountFilter 参数": "Разрешить параметр AccountFilter", + "允许 HTTP 协议图片请求(适用于自部署代理)": "Разрешить запросы изображений по протоколу HTTP (применимо для самостоятельно развернутых прокси)", + "允许 safety_identifier 透传": "Разрешить сквозную передачу safety_identifier", + "允许 service_tier 透传": "Разрешить сквозную передачу service_tier", + "允许 Turnstile 用户校验": "Разрешить проверку пользователей Turnstile", + "允许不安全的 Origin(HTTP)": "Разрешить небезопасные Origin (HTTP)", + "允许回调(会泄露服务器 IP 地址)": "Разрешить обратные вызовы (может раскрыть IP-адрес сервера)", + "允许在 Stripe 支付中输入促销码": "Разрешить ввод промокодов при оплате через Stripe", + "允许新用户注册": "Разрешить регистрацию новых пользователей", + "允许的 Origins": "Разрешенные Origins", + "允许的IP,一行一个,不填写则不限制": "Разрешенные IP, по одному на строку, если не заполнено - без ограничений", + "允许的端口": "Разрешенные порты", + "允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)": "Разрешить доступ к частным IP-адресам (127.0.0.1, 192.168.x.x и другие внутренние адреса)", + "允许通过 GitHub 账户登录 & 注册": "Разрешить вход и регистрацию через аккаунт GitHub", + "允许通过 Linux DO 账户登录 & 注册": "Разрешить вход и регистрацию через аккаунт LinuxDO", + "允许通过 OIDC 进行登录": "Разрешить вход через OIDC", + "允许通过 Passkey 登录 & 认证": "Разрешить вход и аутентификацию через Passkey", + "允许通过 Telegram 进行登录": "Разрешить вход через Telegram", + "允许通过密码进行注册": "Разрешить регистрацию через пароль", + "允许通过密码进行登录": "Разрешить вход через пароль", + "允许通过微信登录 & 注册": "Разрешить вход и регистрацию через WeChat", + "元": "Юань", + "充值": "Пополнить", + "充值价格(x元/美金)": "Цена пополнения (x юаней/доллар США)", + "充值价格显示": "Отображение цены пополнения", + "充值分组倍率": "Коэффициенты групп пополнения", + "充值分组倍率不是合法的 JSON 字符串": "Коэффициенты групп пополнения не являются допустимой JSON строкой", + "充值数量": "Количество пополнения", + "充值数量,最低 ": "Количество пополнения, минимум ", + "充值数量不能小于": "Количество пополнения не может быть меньше", + "充值方式设置": "Настройки способов пополнения", + "充值方式设置不是合法的 JSON 字符串": "Настройки способов пополнения не являются допустимой JSON строкой", + "充值确认": "Подтверждение пополнения", + "充值账单": "Счет пополнения", + "充值金额折扣配置": "Конфигурация скидок на суммы пополнения", + "充值金额折扣配置不是合法的 JSON 对象": "Конфигурация скидок на суммы пополнения не является допустимым JSON объектом", + "充值链接": "Ссылка пополнения", + "充值额度": "Лимит пополнения", + "兑换人ID": "ID обменщика", + "兑换成功!": "Обмен успешен!", + "兑换码充值": "Пополнение кодом купона", + "兑换码创建成功": "Код купона успешно создан", + "兑换码创建成功,是否下载兑换码?": "Код купона успешно создан, скачать код купона?", + "兑换码创建成功!": "Код купона успешно создан!", + "兑换码将以文本文件的形式下载,文件名为兑换码的名称。": "Код купона будет загружен в виде текстового файла, имя файла - название кода купона.", + "兑换码更新成功!": "Код купона успешно обновлен!", + "兑换码生成管理": "Управление генерацией кодов купонов", + "兑换码管理": "Управление кодами купонов", + "兑换额度": "Обменять квоту", + "全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用": "Глобальный контроль отображения области и функций боковой панели, пользователи не могут включить функции, скрытые администратором", + "全局设置": "Глобальные настройки", + "全选": "Выбрать все", + "全部": "Все", + "全部供应商": "Все поставщики", + "全部分组": "Все группы", + "全部标签": "Все теги", + "全部模型": "Все модели", + "全部状态": "Все статусы", + "全部端点": "Все конечные точки", + "全部类型": "Все типы", + "公告": "Объявление", + "公告内容": "Содержание объявления", + "公告已更新": "Объявление обновлено", + "公告更新失败": "Не удалось обновить объявление", + "公告类型": "Тип объявления", + "共": "Всего", + "共 {{count}} 个密钥_one": "Всего {{count}} ключ", + "共 {{count}} 个密钥_few": "Всего {{count}} ключа", + "共 {{count}} 个密钥_many": "Всего {{count}} ключей", + "共 {{count}} 个密钥_other": "Всего {{count}} ключей", + "共 {{count}} 个模型": "Всего {{count}} моделей", + "共 {{total}} 项,当前显示 {{start}}-{{end}} 项": "Всего {{total}} элементов, отображаются {{start}}-{{end}}", + "关": "Выкл", + "关于": "О", + "关于我们": "О нас", + "关于系统的详细信息": "Подробная информация о системе", + "关于项目": "О проекте", + "关键字(id或者名称)": "Ключевое слово (ID или имя)", + "关闭": "Закрыть", + "关闭侧边栏": "Закрыть боковую панель", + "关闭公告": "Закрыть объявление", + "关闭后,此模型将不会被“同步官方”自动覆盖或创建": "После отключения эта модель не будет автоматически перезаписана или создана при \"синхронизации с официальной\"", + "关闭弹窗,已停止批量测试": "Окно закрыто, массовое тестирование остановлено", + "其他": "Другое", + "其他注册选项": "Другие варианты регистрации", + "其他登录选项": "Другие варианты входа", + "其他设置": "Другие настройки", + "内容": "Содержание", + "内容较大,已启用性能优化模式": "Содержание большое, включен режим оптимизации производительности", + "内容较大,部分功能可能受限": "Содержание большое, некоторые функции могут быть ограничены", + "最低": "Минимум", + "最低充值美元数量": "Минимальная сумма пополнения в долларах", + "最后使用时间": "Время последнего использования", + "最后请求": "Последний запрос", + "准备完成初始化": "Подготовка к инициализации завершена", + "分类名称": "Название категории", + "分组": "Группа", + "分组与模型定价设置": "Настройки групп и ценообразования моделей", + "分组价格": "Цена группы", + "分组倍率": "Коэффициент группы", + "分组倍率设置": "Настройки коэффициента группы", + "分组倍率设置,可以在此处新增分组或修改现有分组的倍率,格式为 JSON 字符串,例如:{\"vip\": 0.5, \"test\": 1},表示 vip 分组的倍率为 0.5,test 分组的倍率为 1": "Настройки коэффициента группы, здесь можно добавить новые группы или изменить Коэффициенты существующих групп, формат - JSON строка, например: {\"vip\": 0.5, \"test\": 1}, что означает коэффициент группы vip равен 0.5, коэффициент группы test равен 1", + "分组特殊倍率": "Специальный коэффициент группы", + "分组设置": "Настройки группы", + "分组速率配置优先级高于全局速率限制。": "Конфигурация скорости группы имеет более высокий приоритет, чем глобальные ограничения скорости.", + "分组速率限制": "Ограничение скорости группы", + "分钟": "минут", + "切换为Assistant角色": "Переключиться на роль Assistant", + "切换为System角色": "Переключиться на роль System", + "切换为单密钥模式": "Переключиться на режим одного ключа", + "切换主题": "Переключить тему", + "划转到余额": "Перевести на баланс", + "划转邀请额度": "Перевести пригласительную квоту", + "划转金额最低为": "Минимальная сумма перевода", + "划转额度": "Перевести квоту", + "列设置": "Настройки столбцов", + "创建令牌默认选择auto分组,初始令牌也将设为auto(否则留空,为用户默认分组)": "При создании токена по умолчанию выбирается группа auto, начальный токен также будет установлен в auto (иначе оставить пустым для группы пользователя по умолчанию)", + "创建失败": "Не удалось создать", + "创建成功": "Успешно создано", + "创建新用户账户": "Создать новую учетную запись пользователя", + "创建新的令牌": "Создать новый токен", + "创建新的兑换码": "Создать новый код купона", + "创建新的模型": "Создать новую модель", + "创建新的渠道": "Создать новый канал", + "创建新的预填组": "Создать новую группу предварительного заполнения", + "创建时间": "Время создания", + "创建用户": "Создать пользователя", + "初始化失败,请重试": "Инициализация не удалась, попробуйте еще раз", + "初始化系统": "Инициализация системы", + "删除": "Удалить", + "删除失败": "Не удалось удалить", + "删除密钥失败": "Не удалось удалить Токен", + "删除成功": "Токен успешно удален", + "删除所选": "Удалить выбранное", + "删除所选令牌": "Удалить выбранные токены", + "删除所选通道": "Удалить выбранные каналы", + "删除禁用密钥失败": "Не удалось удалить отключенные Токены", + "删除禁用通道": "Удалить отключенные каналы", + "删除自动禁用密钥": "Удалить автоматически отключенные Токены", + "删除账户": "Удалить аккаунт", + "删除账户确认": "Подтверждение удаления аккаунта", + "刷新": "Обновить", + "刷新失败": "Не удалось обновить", + "前缀": "Префикс", + "剩余备用码:": "Оставшиеся резервные коды:", + "剩余额度": "Оставшаяся квота", + "剩余额度/总额度": "Оставшаяся квота/Общая квота", + "剩余额度$": "Оставшаяся квота$", + "功能特性": "Функциональные возможности", + "加入预填组": "Присоединиться к группе предварительного заполнения", + "加载中...": "Загрузка...", + "加载供应商信息失败": "Не удалось загрузить информацию о поставщике", + "加载关于内容失败...": "Не удалось загрузить содержимое о...", + "加载分组失败": "Не удалось загрузить группы", + "加载失败": "Не удалось загрузить", + "加载模型信息失败": "Не удалось загрузить информацию о модели", + "加载模型失败": "Не удалось загрузить модель", + "加载用户协议内容失败...": "Не удалось загрузить содержимое пользовательского соглашения...", + "加载账单失败": "Не удалось загрузить счёт", + "加载隐私政策内容失败...": "Не удалось загрузить содержимое политики конфиденциальности...", + "包含": "Включает", + "包含来自未知或未标明供应商的AI模型,这些模型可能来自小型供应商或开源项目。": "Включает модели ИИ от неизвестных или неуказанных поставщиков, эти модели могут быть от небольших поставщиков или проектов с открытым исходным кодом.", + "包括失败请求的次数,0代表不限制": "Включает количество неудачных запросов, 0 означает без ограничений", + "匹配类型": "Тип соответствия", + "区域": "Регион", + "历史消耗": "Историческое потребление", + "原价": "Первоначальная цена", + "原因:": "Причина:", + "原密码": "Старый пароль", + "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Дедупликация завершена: до дедупликации {{before}} ключей, после дедупликации {{after}} ключей", + "参与官方同步": "Участвовать в официальной синхронизации", + "参数": "Параметры", + "参数值": "Значение параметра", + "参数覆盖": "Переопределение параметров", + "参照生视频": "Ссылка на генерацию видео", + "友情链接": "Дружественные ссылки", + "发布日期": "Дата публикации", + "发布时间": "Время публикации", + "取消": "Отмена", + "取消全选": "Отменить выбор всех", + "变换": "Трансформация", + "变焦": "Масштабирование", + "只包括请求成功的次数": "Включать только успешные запросы", + "只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求": "Поддерживается только HTTPS, система будет отправлять уведомления методом POST, убедитесь, что адрес может принимать POST-запросы", + "只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录": "IP-адреса в журналах запросов и ошибок записываются только когда пользователь включил запись IP-адресов в настройках", + "可信": "Доверенный", + "可在设置页面设置关于内容,支持 HTML & Markdown": "Можно установить содержимое страницы \"О нас\" на странице настроек, поддерживается HTML и Markdown", + "可用令牌分组": "Доступные группы токенов", + "可用分组": "Доступные группы", + "可用模型": "Доступные модели", + "可用端点类型": "Доступные типы конечных точек", + "可用邀请额度": "Доступная пригласительная квота", + "可视化": "Визуализация", + "可视化倍率设置": "Визуальные настройки коэффициента", + "可视化编辑": "Визуальное редактирование", + "可选,公告的补充说明": "Необязательно, дополнительное описание объявления", + "可选值": "Дополнительные значения", + "同时重置消息": "Одновременно сбросить сообщения", + "同步": "Синхронизация", + "同步向导": "Мастер синхронизации", + "同步失败": "Синхронизация не удалась", + "同步成功": "Синхронизация успешна", + "同步接口": "Интерфейс синхронизации", + "名称": "Название", + "名称+密钥": "Название+ключ", + "名称不能为空": "Название не может быть пустым", + "名称匹配类型": "Тип соответствия названия", + "后端请求失败": "Запрос к бэкенду не удался", + "后缀": "Суффикс", + "否": "Нет", + "启动时间": "Время запуска", + "启用": "Включить", + "启用 Prompt 检查": "Включить проверку Prompt", + "启用2FA失败": "Не удалось включить 2FA", + "启用Claude思考适配(-thinking后缀)": "Включить адаптацию мышления Claude (суффикс -thinking)", + "启用Gemini思考后缀适配": "Включить адаптацию суффикса мышления Gemini", + "启用Ping间隔": "Включить интервал Ping", + "启用SMTP SSL": "Включить SMTP SSL", + "启用SSRF防护(推荐开启以保护服务器安全)": "Включить защиту SSRF (рекомендуется включить для защиты безопасности сервера)", + "启用全部": "Включить все", + "启用密钥失败": "Не удалось включить ключ", + "启用屏蔽词过滤功能": "Включить функцию фильтрации запрещённых слов", + "启用所有密钥失败": "Не удалось включить все ключи", + "启用数据看板(实验性)": "Включить панель данных (экспериментальная функция)", + "启用用户模型请求速率限制(可能会影响高并发性能)": "Включить ограничение скорости запросов моделей пользователя (может повлиять на производительность при высокой нагрузке)", + "启用绘图功能": "Включить функцию рисования", + "启用请求体透传功能": "Включить функцию прозрачной передачи тела запроса", + "启用请求透传": "Включить прозрачную передачу запросов", + "启用额度消费日志记录": "Включить журналирование потребления квоты", + "启用验证": "Включить проверку", + "周": "Неделя", + "和": "и", + "响应": "Ответ", + "响应时间": "Время ответа", + "商品价格 ID": "ID цены товара", + "回答内容": "Содержание ответа", + "回调 URL 填": "URL обратного вызова", + "回调地址": "Адрес обратного вызова", + "固定价格": "Фиксированная цена", + "固定价格(每次)": "Фиксированная цена (за каждый раз)", + "固定价格值": "Значение фиксированной цены", + "图像生成": "Генерация изображений", + "图标": "Значок", + "图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ": "Используйте библиотеку @lobehub/icons, например: OpenAI, Claude.Color, поддерживаются цепочечные параметры: OpenAI.Avatar.type={'platform'}, OpenRouter.Avatar.shape={'square'}, для просмотра всех доступных иконок, пожалуйста, ", + "图混合": "Смешивание изображений", + "图片生成调用:{{symbol}}{{price}} / 1次": "Вызов генерации изображения: {{symbol}}{{price}} / 1 раз", + "图片输入: {{imageRatio}}": "Ввод изображения: {{imageRatio}}", + "图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})": "Цена ввода изображения: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M токенов (коэффициент изображения: {{imageRatio}})", + "图片输入倍率(仅部分模型支持该计费)": "Коэффициент ввода изображения (только некоторые модели поддерживают эту тарификацию)", + "图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费": "Настройки коэффициента, связанные с вводом изображения, ключ - название модели, значение - коэффициент, только некоторые модели поддерживают эту тарификацию", + "图生文": "Изображение в текст", + "图生视频": "Изображение в видео", + "在Gotify服务器创建应用后获得的令牌,用于发送通知": "Токен, полученный после создания приложения на сервере Gotify, используется для отправки уведомлений", + "在Gotify服务器的应用管理中创建新应用": "Создать новое приложение в управлении приложениями на сервере Gotify", + "在找兑换码?": "Ищете код купона?", + "在此输入 Logo 图片地址": "Введите здесь адрес изображения Logo", + "在此输入新的公告内容,支持 Markdown & HTML 代码": "Введите здесь новое содержание объявления, поддерживается код Markdown и HTML", + "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面": "Введите здесь новое содержание о проекте, поддерживается код Markdown и HTML. Если введена ссылка, она будет использована как атрибут src для iframe, что позволяет установить любую веб-страницу как страницу о проекте", + "在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码": "Введите здесь новый нижний колонтитул, оставьте пустым для использования нижнего колонтитула по умолчанию, поддерживается HTML код", + "在此输入用户协议内容,支持 Markdown & HTML 代码": "Введите здесь содержимое пользовательского соглашения, поддерживается Markdown & HTML код", + "在此输入系统名称": "Введите здесь название системы", + "在此输入隐私政策内容,支持 Markdown & HTML 代码": "Введите здесь содержимое политики конфиденциальности, поддерживается Markdown & HTML код", + "在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "Введите здесь содержание главной страницы, поддерживается код Markdown и HTML. После настройки информация о состоянии на главной странице больше не будет отображаться. Если введена ссылка, она будет использована как атрибут src для iframe, что позволяет установить любую веб-страницу как главную страницу", + "域名IP过滤详细说明": "⚠️ Эта функция является экспериментальной опцией, доменное имя может быть разрешено в несколько адресов IPv4/IPv6, если включено, убедитесь, что список фильтрации IP покрывает эти адреса, иначе это может привести к сбою доступа.", + "域名白名单": "Белый список доменов", + "域名黑名单": "Чёрный список доменов", + "基本信息": "Основная информация", + "填入": "Заполнить", + "填入所有模型": "Заполнить все модели", + "填入模板": "Заполнить шаблон", + "填入相关模型": "Заполнить связанные модели", + "填写Gotify服务器的完整URL地址": "Введите полный URL-адрес сервера Gotify", + "填写带https的域名,逗号分隔": "Введите домены с https, разделённые запятыми", + "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "После заполнения содержимого пользовательского соглашения, пользователям потребуется отметить, что они прочитали пользовательское соглашение при регистрации", + "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "После заполнения содержимого политики конфиденциальности, пользователям потребуется отметить, что они прочитали политику конфиденциальности при регистрации", + "备份支持": "Поддержка резервного копирования", + "备份状态": "Состояние резервного копирования", + "备注": "Примечания", + "备用恢复代码": "Резервный код восстановления", + "备用码已复制到剪贴板": "Резервный код скопирован в буфер обмена", + "备用码重新生成成功": "Резервный код успешно сгенерирован заново", + "复制": "Копировать", + "复制代码": "Копировать код", + "复制令牌": "Копировать токен", + "复制全部": "Копировать всё", + "复制名称": "Копировать название", + "复制失败": "Не удалось скопировать", + "复制失败,请手动复制": "Не удалось скопировать, пожалуйста, скопируйте вручную", + "复制已选": "Копировать выбранное", + "复制应用的令牌(Token)并填写到上方的应用令牌字段": "Скопируйте токен приложения и вставьте в поле токена приложения выше", + "复制成功": "Скопировано успешно", + "复制所有代码": "Копировать весь код", + "复制所有模型": "Копировать все модели", + "复制所选令牌": "Копировать выбранные токены", + "复制所选兑换码到剪贴板": "Копировать выбранные коды обмена в буфер обмена", + "复制渠道的所有信息": "Копировать всю информацию о канале", + "外接设备": "Внешнее устройство", + "多密钥渠道操作项目组": "Группа операций с многоключевыми каналами", + "多密钥管理": "Управление множественными ключами", + "多种充值方式,安全便捷": "Множество способов пополнения, безопасно и удобно", + "天": "день", + "天前": "дней назад", + "失败": "Неудача", + "失败原因": "Причина неудачи", + "失败时自动禁用通道": "Автоматически отключать канал при неудаче", + "失败重试次数": "Количество повторных попыток при неудаче", + "奖励说明": "Описание награды", + "如:大带宽批量分析图片推荐": "Например: рекомендуется для пакетного анализа изображений с большой пропускной способностью", + "如:香港线路": "Например: Гонконгская линия", + "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "Если вы интегрируетесь с восходящими проектами пересылки, такими как One API или New API, используйте тип OpenAI, не используйте этот тип, если вы не знаете, что делаете.", + "如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "Если запрос пользователя содержит системный промпт, используйте эту настройку для добавления перед системным промптом пользователя", + "始终使用浅色主题": "Всегда использовать светлую тему", + "始终使用深色主题": "Всегда использовать темную тему", + "字段透传控制": "Управление прозрачной передачей полей", + "存在重复的键名:": "Обнаружены повторяющиеся имена ключей:", + "安全提醒": "Напоминание о безопасности", + "安全设置": "Настройки безопасности", + "安全验证": "Проверка безопасности", + "安全验证级别": "Уровень проверки безопасности", + "安装指南": "Руководство по установке", + "完成": "Завершить", + "完成初始化": "Завершить инициализацию", + "完成设置并启用两步验证": "Завершить настройки и включить двухфакторную аутентификацию", + "完整的 Base URL,支持变量{model}": "Полный Base URL, поддерживает переменную {model}", + "官方": "Официальный", + "官方文档": "Официальная документация", + "官方模型同步": "Синхронизация официальных моделей", + "定价模式": "Режим ценообразования", + "定时测试所有通道": "Периодическое тестирование всех каналов", + "定期更改密码可以提高账户安全性": "Регулярная смена пароля может повысить безопасность аккаунта", + "实付": "Фактически оплачено", + "实付金额": "Фактически оплаченная сумма", + "实付金额:": "Фактически оплаченная сумма:", + "实际模型": "Фактическая модель", + "实际请求体": "Фактическое тело запроса", + "密码": "Пароль", + "密码修改成功!": "Пароль успешно изменен!", + "密码已复制到剪贴板:": "Пароль скопирован в буфер обмена:", + "密码已重置并已复制到剪贴板:": "Пароль сброшен и скопирован в буфер обмена:", + "密码管理": "Управление паролями", + "密码重置": "Сброс пароля", + "密码重置完成": "Сброс пароля завершен", + "密码重置确认": "Подтверждение сброса пароля", + "密码长度至少为8个字符": "Длина пароля должна быть не менее 8 символов", + "密钥": "Ключ", + "密钥(编辑模式下,保存的密钥不会显示)": "Токен (в режиме редактирования сохраненные токены не отображаются)", + "密钥去重": "Удаление дубликатов ключей", + "密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性": "Ключ будет добавлен в заголовок запроса методом Bearer для проверки легитимности webhook-запросов", + "密钥已删除": "Ключ удален", + "密钥已启用": "Токен включен", + "密钥已复制到剪贴板": "Ключ скопирован в буфер обмена", + "密钥已禁用": "Токен отключен", + "密钥文件 (.json)": "Файл ключей (.json)", + "密钥更新模式": "Режим обновления ключей", + "密钥格式": "Формат ключа", + "密钥格式无效,请输入有效的 JSON 格式密钥": "Недопустимый формат ключа, введите действительный ключ в формате JSON", + "密钥聚合模式": "Режим агрегации ключей", + "密钥获取成功": "Ключ успешно получен", + "密钥输入方式": "Способ ввода ключа", + "密钥预览": "Предпросмотр ключа", + "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "Для официальных каналов new-api уже имеет встроенные адреса, если это не сторонние прокси-сайты или специальные адреса доступа Azure, заполнять не нужно", + "对域名启用 IP 过滤(实验性)": "Включить IP-фильтрацию для доменов (экспериментально)", + "对外运营模式": "Режим внешней эксплуатации", + "导入": "Импорт", + "导入的配置将覆盖当前设置,是否继续?": "Импортируемая конфигурация перезапишет текущие настройки, продолжить?", + "导入配置": "Импорт конфигурации", + "导入配置失败: ": "Ошибка импорта конфигурации: ", + "导出": "Экспорт", + "导出配置": "Экспорт конфигурации", + "导出配置失败: ": "Ошибка экспорта конфигурации: ", + "将 reasoning_content 转换为 标签拼接到内容中": "Преобразовать reasoning_content в теги и добавить к содержимому", + "将为选中的 ": "Будет выбрано ", + "将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "Будет сохранен только первый файл ключей, остальные файлы будут удалены, продолжить?", + "将删除": "Будет удалено", + "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "Будут удалены использованные, отключенные и просроченные коды обмена, эта операция необратима.", + "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "Будут очищены все сохраненные конфигурации и восстановлены настройки по умолчанию, эта операция необратима. Продолжить?", + "将清除选定时间之前的所有日志": "Будут очищены все логи до выбранного времени", + "小时": "час", + "尚未使用": "Еще не использовано", + "局部重绘-提交": "Локальная перерисовка - отправить", + "屏蔽词列表": "Список заблокированных слов", + "屏蔽词过滤设置": "Настройки фильтрации заблокированных слов", + "展开": "Развернуть", + "展开更多": "Развернуть больше", + "左侧边栏个人设置": "Персональные настройки левой боковой панели", + "已为 {{count}} 个模型设置{{type}}_one": "Установлено {{type}} для {{count}} модели", + "已为 {{count}} 个模型设置{{type}}_few": "Установлено {{type}} для {{count}} моделей", + "已为 {{count}} 个模型设置{{type}}_many": "Установлено {{type}} для {{count}} моделей", + "已为 {{count}} 个模型设置{{type}}_other": "Установлено {{type}} для {{count}} моделей", + "已为 ${count} 个渠道设置标签!": "Установлены метки для ${count} каналов!", + "已修复 ${success} 个通道,失败 ${fails} 个通道。": "Исправлено ${success} каналов, не удалось исправить ${fails} каналов.", + "已停止批量测试": "Пакетное тестирование остановлено", + "已关闭后续提醒": "Последующие уведомления отключены", + "已切换为Assistant角色": "Переключено на роль Assistant", + "已切换为System角色": "Переключено на роль System", + "已切换至最优倍率视图,每个模型使用其最低倍率分组": "Переключено на оптимальный вид множителей, каждая модель использует свою группу с минимальным множителем", + "已初始化": "Инициализировано", + "已删除 {{count}} 个令牌!": "Удалено {{count}} токенов!", + "已删除 {{count}} 条失效兑换码_one": "Удален {{count}} недействительный код купона", + "已删除 {{count}} 条失效兑换码_few": "Удалено {{count}} недействительных кода купона", + "已删除 {{count}} 条失效兑换码_many": "Удалено {{count}} недействительных кодов купонов", + "已删除 {{count}} 条失效兑换码_other": "Удалено {{count}} недействительных кодов купонов", + "已删除 ${data} 个通道!": "Удалено ${data} каналов!", + "已删除所有禁用渠道,共计 ${data} 个": "Удалены все отключенные каналы, всего ${data}", + "已删除消息及其回复": "Сообщение и его ответы удалены", + "已发送到 Fluent": "Отправлено в Fluent", + "已取消 Passkey 注册": "Регистрация Passkey отменена", + "已启用": "Включено", + "已启用 Passkey,无需密码即可登录": "Passkey включен, вход без пароля", + "已启用所有密钥": "Все ключи включены", + "已备份": "Резервная копия создана", + "已复制": "Скопировано", + "已复制 ${count} 个模型": "Скопировано ${count} моделей", + "已复制:": "Скопировано: ", + "已复制:{{name}}": "Скопировано: {{name}}", + "已复制到剪切板": "Скопировано в буфер обмена", + "已复制到剪贴板": "Скопировано в буфер обмена", + "已复制到剪贴板!": "Скопировано в буфер обмена!", + "已复制模型名称": "Название модели скопировано", + "已成功开始测试所有已启用通道,请刷新页面查看结果。": "Успешно начато тестирование всех включенных каналов, обновите страницу для просмотра результатов.", + "已提交": "Отправлено", + "已新增 {{count}} 个模型:{{list}}_one": "Добавлена {{count}} модель: {{list}}", + "已新增 {{count}} 个模型:{{list}}_few": "Добавлено {{count}} модели: {{list}}", + "已新增 {{count}} 个模型:{{list}}_many": "Добавлено {{count}} моделей: {{list}}", + "已新增 {{count}} 个模型:{{list}}_other": "Добавлено {{count}} моделей: {{list}}", + "已更新完毕所有已启用通道余额!": "Балансы всех включенных каналов обновлены!", + "已有保存的配置": "Сохраненные конфигурации уже существуют", + "已有的模型": "Существующие модели", + "已有账户?": "Уже есть аккаунт?", + "已注销": "Выход выполнен", + "已添加到白名单": "Добавлено в белый список", + "已清空测试结果": "Результаты тестов очищены", + "已用/剩余": "Использовано/Осталось", + "已用额度": "Использованная квота", + "已禁用": "Отключено", + "已禁用所有密钥": "Все ключи отключены", + "已绑定": "Привязано", + "已绑定渠道": "Каналы привязаны", + "已耗尽": "Исчерпано", + "已过期": "Просрочено", + "已选择 {{count}} 个模型_one": "Выбрана {{count}} модель", + "已选择 {{count}} 个模型_few": "Выбрано {{count}} модели", + "已选择 {{count}} 个模型_many": "Выбрано {{count}} моделей", + "已选择 {{count}} 个模型_other": "Выбрано {{count}} моделей", + "已选择 {{selected}} / {{total}}": "Выбрано {{selected}} / {{total}}", + "已选择 ${count} 个渠道": "Выбрано ${count} каналов", + "已重置为默认配置": "Сброшено на конфигурацию по умолчанию", + "常见问答": "Часто задаваемые вопросы", + "常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)": "Управление часто задаваемыми вопросами, предоставление ответов на распространенные вопросы пользователям (максимум 50, на интерфейсе отображаются последние 20)", + "平台": "Платформа", + "平均RPM": "Среднее RPM", + "平均TPM": "Среднее TPM", + "平移": "Панорамирование", + "应用同步": "Синхронизация приложения", + "应用更改": "Применить изменения", + "应用覆盖": "Перезапись приложения", + "建立连接时发生错误": "Ошибка при установке соединения", + "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "Рекомендуется использовать базы данных MySQL или PostgreSQL в производственной среде, или убедиться, что файл базы данных SQLite сопоставлен с постоянным хранилищем хоста.", + "开": "Вкл", + "开启之后会清除用户提示词中的": "После включения будет очищено в промптах пользователя:", + "开启之后将上游地址替换为服务器地址": "После включения адреса восходящих каналов будут заменены на адрес сервера", + "开启后,仅\"消费\"和\"错误\"日志将记录您的客户端IP地址": "После включения, только логи \"потребление\" и \"ошибки\" будут записывать IP-адрес вашего клиента", + "开启后,将定期发送ping数据保持连接活跃": "После включения будет периодически отправляться ping-данные для поддержания активности соединения", + "开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启": "После включения все запросы будут напрямую передаваться upstream без какой-либо обработки (перенаправление и адаптация каналов также будут отключены), включайте с осторожностью", + "开启后不限制:必须设置模型倍率": "После включения без ограничений: необходимо установить множители моделей", + "开启后未登录用户无法访问模型广场": "После включения незарегистрированные пользователи не смогут получить доступ к площади моделей", + "开启批量操作": "Включить пакетные операции", + "开始同步": "Начать синхронизацию", + "开始批量测试 ${count} 个模型,已清空上次结果...": "Начало пакетного тестирования ${count} моделей, предыдущие результаты очищены...", + "开始时间": "Время начала", + "弱变换": "Слабое преобразование", + "强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)": "Принудительно форматировать ответ в стандартный формат OpenAI (только для типов каналов OpenAI)", + "强制格式化": "Принудительное форматирование", + "强制要求": "Обязательное требование", + "强变换": "Сильное преобразование", + "当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道": "Автоматически отключать канал, когда в ошибке от восходящего канала содержатся эти ключевые слова (без учета регистра)", + "当前余额": "Текущий баланс", + "当前值": "Текущее значение", + "当前分组为 auto,会自动选择最优分组,当一个组不可用时自动降级到下一个组(熔断机制)": "Текущая группа - auto, автоматически выбирается оптимальная группа, когда одна группа недоступна, автоматически переключается на следующую (механизм предохранителя)", + "当前时间": "Текущее время", + "当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。": "В настоящее время обратный вызов Midjourney отключен, некоторые проекты могут не получить результаты рисования, можно включить в настройках эксплуатации.", + "当前查看的分组为:{{group}},倍率为:{{ratio}}": "Текущая просматриваемая группа: {{group}}, коэффициент: {{ratio}}", + "当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。": "Текущий список моделей является самым длинным списком моделей всех каналов под этой меткой, а не объединением всех каналов, обратите внимание, что это может привести к потере моделей некоторых каналов.", + "当前版本": "Текущая версия", + "当前计费": "Текущая тарификация", + "当前设备不支持 Passkey": "Текущее устройство не поддерживает Passkey", + "当前设置类型: ": "Текущий тип настроек: ", + "当前跟随系统": "Следовать системе", + "当剩余额度低于此数值时,系统将通过选择的方式发送通知": "Когда оставшаяся квота ниже этого значения, система отправит уведомление выбранным способом", + "当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用": "Принимать вызовы моделей без установленной цены, использовать только если вы доверяете сайту, могут возникнуть высокие расходы", + "当运行通道全部测试时,超过此时间将自动禁用通道": "При тестировании всех работающих каналов, превышение этого времени автоматически отключит канал", + "待使用收益": "Ожидаемый доход", + "微信": "WeChat", + "微信公众号二维码图片链接": "Ссылка на изображение QR-кода официальной учетной записи WeChat", + "微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "Отсканируйте QR-код в WeChat, чтобы подписаться на официальную учетную запись, введите «код подтверждения», чтобы получить код подтверждения (действителен в течение трех минут)", + "微信扫码登录": "Вход через сканирование QR-кода в WeChat", + "微信账户绑定成功!": "Привязка учетной записи WeChat успешна!", + "必须是有效的 JSON 字符串数组,例如:[\"g1\",\"g2\"]": "Должен быть действительный массив строк JSON, например: [\"g1\",\"g2\"]", + "忘记密码?": "Забыли пароль?", + "快速开始": "Быстрый старт", + "思考中...": "Размышляю...", + "思考内容转换": "Преобразование содержимого размышлений", + "思考过程": "Процесс размышлений", + "思考适配 BudgetTokens 百分比": "Адаптация размышлений к проценту BudgetTokens", + "思考预算占比": "Доля бюджета на размышления", + "性能指标": "Показатели производительности", + "总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}": "Общая цена: цена текста {{textPrice}} + цена аудио {{audioPrice}} = {{symbol}}{{total}}", + "总密钥数": "Общее количество ключей", + "总收益": "Общий доход", + "总计": "Итого", + "总额度": "Общая квота", + "您可以个性化设置侧边栏的要显示功能": "Вы можете персонализировать отображаемые функции боковой панели", + "您无权访问此页面,请联系管理员": "У вас нет прав доступа к этой странице, свяжитесь с администратором", + "您正在使用 MySQL 数据库。MySQL 是一个可靠的关系型数据库管理系统,适合生产环境使用。": "Вы используете базу данных MySQL. MySQL - это надежная система управления реляционными базами данных, подходящая для использования в производственной среде.", + "您正在使用 PostgreSQL 数据库。PostgreSQL 是一个功能强大的开源关系型数据库系统,提供了出色的可靠性和数据完整性,适合生产环境使用。": "Вы используете базу данных PostgreSQL. PostgreSQL - это мощная система управления реляционными базами данных с открытым исходным кодом, обеспечивающая превосходную надежность и целостность данных, подходящая для использования в производственной среде.", + "您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!": "Вы используете базу данных SQLite. Если вы работаете в контейнерной среде, убедитесь, что правильно настроено постоянное сопоставление файлов базы данных, иначе все данные будут потеряны после перезапуска контейнера!", + "您正在删除自己的帐户,将清空所有数据且不可恢复": "Вы удаляете свою учетную запись, все данные будут очищены и не могут быть восстановлены", + "您的数据将安全地存储在本地计算机上。所有配置、用户信息和使用记录都会自动保存,关闭应用后不会丢失。": "Ваши данные будут безопасно храниться на локальном компьютере. Все конфигурации, информация о пользователях и записи об использовании будут автоматически сохранены и не потеряются после закрытия приложения.", + "您确定要取消密码登录功能吗?这可能会影响用户的登录方式。": "Вы уверены, что хотите отменить функцию входа по паролю? Это может повлиять на способ входа пользователей.", + "您需要先启用两步验证或 Passkey 才能执行此操作": "Вам необходимо сначала включить двухфакторную аутентификацию или Passkey для выполнения этой операции", + "您需要先启用两步验证或 Passkey 才能查看敏感信息。": "Вам необходимо сначала включить двухфакторную аутентификацию или Passkey для просмотра конфиденциальной информации.", + "想起来了?": "Вспомнили?", + "成功": "Успешно", + "成功兑换额度:": "Успешно обменяно квота: ", + "成功时自动启用通道": "Автоматически включать канал при успехе", + "我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "Я понимаю, что отключение двухфакторной аутентификации приведет к постоянному удалению всех связанных настроек и резервных кодов, и эта операция не может быть отменена", + "我已阅读并同意": "Я прочитал(а) и согласен(на)", + "或": "или", + "或其兼容new-api-worker格式的其他版本": "или другие версии, совместимые с форматом new-api-worker", + "或手动输入密钥:": "или введите ключ вручную: ", + "所有上游数据均可信": "Все восходящие данные доверенные", + "所有密钥已复制到剪贴板": "Все ключи скопированы в буфер обмена", + "所有编辑均为覆盖操作,留空则不更改": "Все редактирования являются операциями перезаписи, если оставить поле пустым, изменения не будут применены", + "手动禁用": "Отключить вручную", + "手动编辑": "Редактировать вручную", + "手动输入": "Ввести вручную", + "打开侧边栏": "Открыть боковую панель", + "执行中": "Выполняется", + "扫描二维码": "Сканировать QR-код", + "批量创建": "Пакетное создание", + "批量创建时会在名称后自动添加随机后缀": "При пакетном создании к имени автоматически добавляется случайный суффикс", + "批量创建模式下仅支持文件上传,不支持手动输入": "В режиме пакетного создания поддерживается только загрузка файлов, ручной ввод не поддерживается", + "批量删除": "Пакетное удаление", + "批量删除令牌": "Пакетное удаление токенов", + "批量删除失败": "Пакетное удаление не удалось", + "批量删除模型": "Пакетное удаление моделей", + "批量操作": "Пакетные операции", + "批量测试${count}个模型": "Пакетное тестирование ${count} моделей", + "批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}": "Пакетное тестирование завершено! Успешно: ${success}, Неудачно: ${fail}, Всего: ${total}", + "批量测试已停止": "Пакетное тестирование остановлено", + "批量测试过程中发生错误: ": "Произошла ошибка в процессе пакетного тестирования: ", + "批量设置": "Пакетные настройки", + "批量设置成功": "Пакетные настройки успешны", + "批量设置标签": "Пакетная установка меток", + "批量设置模型参数": "Пакетная установка параметров модели", + "折": "скидка", + "按K显示单位": "Отображать единицы в K", + "按价格设置": "Настроить по цене", + "按倍率类型筛选": "Фильтровать по типу коэффициента", + "按倍率设置": "Настроить по множителю", + "按次计费": "Оплата за использование", + "按量计费": "Оплата по объему", + "按顺序替换content中的变量占位符": "Последовательно заменять переменные-заполнители в content", + "换脸": "Замена лица", + "授权,需在遵守": "Авторизация, необходимо соблюдать", + "授权失败": "Авторизация не удалась", + "排队中": "В очереди", + "接受未设置价格模型": "Принимать модели без установленной цены", + "接口凭证": "Учетные данные интерфейса", + "控制台": "Консоль", + "控制台区域": "Область консоли", + "控制顶栏模块显示状态,全局生效": "Управление состоянием отображения модулей верхней панели, действует глобально", + "推荐:用户可以选择是否使用指纹等验证": "Рекомендуется: пользователи могут выбирать, использовать ли проверку по отпечатку пальца и другие методы", + "推荐使用(用户可选)": "Рекомендуется использовать (по выбору пользователя)", + "描述": "Описание", + "提交": "Отправить", + "提交时间": "Время отправки", + "提交结果": "Результат отправки", + "提升": "Повысить", + "提示": "Промпт", + "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Ввод {{input}} токенов / 1M токенов * {{symbol}}{{price}} + Вывод {{completion}} токенов / 1M токенов * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Ввод {{nonCacheInput}} токенов / 1M токенов * {{symbol}}{{price}} + Кэш {{cacheInput}} токенов / 1M токенов * {{symbol}}{{cachePrice}} + Создание кэша {{cacheCreationInput}} токенов / 1M токенов * {{symbol}}{{cacheCreationPrice}} + Вывод {{completion}} токенов / 1M токенов * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "提示:如需备份数据,只需复制上述目录即可": "Промпт: для резервного копирования данных просто скопируйте указанный выше каталог", + "提示:链接中的{key}将被替换为API密钥,{address}将被替换为服务器地址": "Промпт: {key} в ссылке будет заменен на API-ключ, {address} будет заменен на адрес сервера", + "提示价格:{{symbol}}{{price}} / 1M tokens": "Цена промпта: {{symbol}}{{price}} / 1M токенов", + "提示缓存倍率": "Коэффициент кэша промптов", + "搜索供应商": "Поиск поставщиков", + "搜索关键字": "Поиск по ключевым словам", + "搜索无结果": "Поиск не дал результатов", + "搜索条件": "Условия поиска", + "搜索模型": "Поиск моделей", + "搜索模型...": "Поиск моделей...", + "搜索模型名称": "Поиск по названию модели", + "搜索模型失败": "Поиск моделей не удался", + "搜索渠道名称或地址": "Поиск по названию или адресу канала", + "搜索聊天应用名称": "Поиск по названию чат-приложения", + "操作": "Операции", + "操作失败": "Операция не удалась", + "操作失败,请重试": "Операция не удалась, попробуйте еще раз", + "操作成功完成!": "Операция успешно завершена!", + "操作暂时被禁用": "Операция временно отключена", + "操练场": "Тренировочная площадка", + "操练场和聊天功能": "Тренировочная площадка и чат-функции", + "支付地址": "Адрес оплаты", + "支付宝": "Alipay", + "支付方式": "Способ оплаты", + "支付设置": "Настройки оплаты", + "支付请求失败": "Запрос на оплату не удался", + "支付金额": "Сумма оплаты", + "支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。": "Поддерживает 6-значные TOTP коды подтверждения или 8-значные резервные коды, можно настроить или просмотреть в `Личные настройки-Настройки безопасности-Настройки двухфакторной аутентификации`.", + "支持CIDR格式,如:8.8.8.8, 192.168.1.0/24": "Поддерживает формат CIDR, например: 8.8.8.8, 192.168.1.0/24", + "支持HTTP和HTTPS,填写Gotify服务器的完整URL地址": "Поддерживает HTTP и HTTPS, укажите полный URL-адрес сервера Gotify", + "支持HTTP和HTTPS,模板变量: {{title}} (通知标题), {{content}} (通知内容)": "Поддерживает HTTP и HTTPS, переменные шаблона: {{title}} (заголовок уведомления), {{content}} (содержимое уведомления)", + "支持众多的大模型供应商": "Поддерживает множество поставщиков больших моделей", + "支持单个端口和端口范围,如:80, 443, 8000-8999": "Поддерживает отдельные порты и диапазоны портов, например: 80, 443, 8000-8999", + "支持变量:": "Поддерживаемые переменные: ", + "支持备份": "Поддерживает резервное копирование", + "支持搜索用户的 ID、用户名、显示名称和邮箱地址": "Поддерживает поиск по ID пользователя, имени пользователя, отображаемому имени и адресу электронной почты", + "支持的图像模型": "Поддерживаемые модели изображений", + "支持通配符格式,如:example.com, *.api.example.com": "Поддерживает формат с подстановочными знаками, например: example.com, *.api.example.com", + "收益": "Доход", + "收益统计": "Статистика доходов", + "收起": "Свернуть", + "收起侧边栏": "Свернуть боковую панель", + "收起内容": "Свернуть содержимое", + "放大": "Увеличить", + "放大编辑": "Увеличить и редактировать", + "敏感信息不会发送到前端显示": "Конфиденциальная информация не будет отправляться для отображения на frontend", + "数据存储位置:": "Место хранения данных: ", + "数据库信息": "Информация о базе данных", + "数据库检查": "Проверка базы данных", + "数据库类型": "Тип базы данных", + "数据库警告": "Предупреждение базы данных", + "数据格式错误": "Ошибка формата данных", + "数据看板": "Панель данных", + "数据看板更新间隔": "Интервал обновления панели данных", + "数据看板设置": "Настройки панели данных", + "数据看板默认时间粒度": "Временная гранулярность панели данных по умолчанию", + "数据管理和日志查看": "Управление данными и просмотр журналов", + "文件上传": "Загрузка файла", + "文件搜索价格:{{symbol}}{{price}} / 1K 次": "Цена поиска файлов: {{symbol}}{{price}} / 1K запросов", + "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "Текстовый ввод {{input}} токенов / 1M токенов * {{symbol}}{{price}} + Текстовый вывод {{completion}} токенов / 1M токенов * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", + "文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "Текстовый ввод {{nonCacheInput}} токенов / 1M токенов * {{symbol}}{{price}} + Кэш {{cacheInput}} токенов / 1M токенов * {{symbol}}{{cachePrice}} + Текстовый вывод {{completion}} токенов / 1M токенов * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", + "文字输入": "Текстовый ввод", + "文字输出": "Текстовый вывод", + "文心一言": "Wenxin Yiyan", + "文档": "Документация", + "文档地址": "Адрес документации", + "文生视频": "Текст в видео", + "新增供应商": "Добавить поставщика", + "新密码": "Новый пароль", + "新密码需要和原密码不一致!": "Новый пароль должен отличаться от старого!", + "新建": "Создать", + "新建数量": "Количество для создания", + "新建组": "Создать группу", + "新格式(支持条件判断与json自定义):": "Новый формат (поддерживает условные суждения и пользовательскую настройку json):", + "新格式模板": "Шаблон нового формата", + "新版本": "Новая версия", + "新用户使用邀请码奖励额度": "Квота вознаграждения для новых пользователей, использующих приглашение", + "新用户初始额度": "Начальная квота для новых пользователей", + "新的备用恢复代码": "Новый резервный код восстановления", + "新的备用码已生成": "Новые резервные коды сгенерированы", + "新获取的模型": "Новые полученные модели", + "新额度:": "Новая квота: ", + "无": "Нет", + "无冲突项": "Нет конфликтующих элементов", + "无效的重置链接,请重新发起密码重置请求": "Недействительная ссылка для сброса, пожалуйста, отправьте запрос на сброс пароля повторно", + "无法发起 Passkey 注册": "Не удалось инициировать регистрацию Passkey", + "无法复制到剪贴板,请手动复制": "Не удалось скопировать в буфер обмена, пожалуйста, скопируйте вручную", + "无邀请人": "Нет приглашающего", + "无限制": "Без ограничений", + "无限额度": "Безлимитная квота", + "日志清理失败:": "Очистка журнала не удалась: ", + "日志类型": "Тип журнала", + "日志设置": "Настройки журнала", + "日志详情": "Детали журнала", + "旧格式(直接覆盖):": "Старый формат (прямая перезапись):", + "旧格式模板": "Шаблон старого формата", + "旧的备用码已失效,请保存新的备用码": "Старые резервные коды больше не действительны, пожалуйста, сохраните новые резервные коды", + "早上好": "Доброе утро", + "时间": "Время", + "时间粒度": "Временная гранулярность", + "易支付商户ID": "ID торговца EasyPay", + "易支付商户密钥": "Ключ торговца EasyPay", + "是": "Да", + "是否为企业账户": "Является ли корпоративным аккаунтом", + "是否同时重置对话消息?选择\"是\"将清空所有对话记录并恢复默认示例;选择\"否\"将保留当前对话记录。": "Одновременно сбросить сообщения диалога? Выбор \"Да\" очистит все записи диалогов и восстановит примеры по умолчанию; выбор \"Нет\" сохранит текущие записи диалогов.", + "是否将该订单标记为成功并为用户入账?": "Отметить этот заказ как успешный и зачислить средства пользователю?", + "是否自动禁用": "Автоматически отключать", + "是否要求指纹/面容等生物识别": "Требовать биометрическую аутентификацию (отпечаток пальца/лицо и т.д.)", + "显示倍率": "Отображать коэффициент", + "显示最新20条": "Отображать последние 20 записей", + "显示名称": "Отображаемое имя", + "显示完整内容": "Отображать полное содержимое", + "显示操作项": "Отображать элементы операций", + "显示更多": "Отображать больше", + "显示第": "Отображать", + "显示设置": "Настройки отображения", + "显示调试": "Отображать отладку", + "晚上好": "Добрый вечер", + "普通用户": "Обычный пользователь", + "智能体ID": "ID интеллектуального агента", + "智能熔断": "Интеллектуальный предохранитель", + "智谱": "Zhipu", + "暂无API信息": "Временно нет информации об API", + "暂无保存的配置": "Нет сохраненных конфигураций", + "暂无充值记录": "Нет записей о пополнении", + "暂无公告": "Нет объявлений", + "暂无匹配模型": "Нет соответствующих моделей", + "暂无可用的支付方式,请联系管理员配置": "Нет доступных способов оплаты, свяжитесь с администратором для настройки", + "暂无响应数据": "Нет данных ответа", + "暂无密钥数据": "Нет данных ключей", + "暂无差异化倍率显示": "Нет отображения дифференцированных множителей", + "暂无常见问答": "Нет часто задаваемых вопросов", + "暂无成功模型": "Нет успешных моделей", + "暂无数据": "Нет данных", + "暂无数据,点击下方按钮添加键值对": "Нет данных, нажмите кнопку ниже, чтобы добавить пару ключ-значение", + "暂无模型描述": "Нет описания модели", + "暂无监控数据": "Нет данных мониторинга", + "暂无系统公告": "Нет системных объявлений", + "暂无缺失模型": "Нет отсутствующих моделей", + "暂无请求数据": "Нет данных запросов", + "暂无项目": "Нет проектов", + "暂无预填组": "Нет предварительно заполненных групп", + "暴露倍率接口": "Интерфейс экспонирования коэффициента", + "更多": "Больше", + "更多信息请参考": "Для получения дополнительной информации см.", + "更多参数请参考": "Для получения дополнительных параметров см.", + "更好的价格,更好的稳定性,只需要将模型基址替换为:": "Лучшая цена, лучшая стабильность, просто замените базовый адрес модели на:", + "更新": "Обновить", + "更新 Stripe 设置": "Обновить настройки Stripe", + "更新SSRF防护设置": "Обновить настройки защиты SSRF", + "更新Worker设置": "Обновить настройки Worker", + "更新令牌信息": "Обновить информацию о токене", + "更新兑换码信息": "Обновить информацию о коде обмена", + "更新失败": "Обновление не удалось", + "更新成功": "Обновление успешно", + "更新所有已启用通道余额": "Обновить баланс всех включенных каналов", + "更新支付设置": "Обновить настройки оплаты", + "更新时间": "Время обновления", + "更新服务器地址": "Обновить адрес сервера", + "更新模型信息": "Обновить информацию о модели", + "更新渠道信息": "Обновить информацию о канале", + "更新预填组": "Обновить предварительно заполненную группу", + "服务可用性": "Доступность сервиса", + "服务器地址": "Адрес сервера", + "服务显示名称": "Отображаемое имя сервиса", + "未发现新增模型": "Новые модели не обнаружены", + "未发现重复密钥": "Дублирующиеся ключи не обнаружены", + "未启动": "Не запущено", + "未启用": "Не включено", + "未命名": "Без имени", + "未备份": "Не резервировано", + "未开始": "Не начато", + "未找到匹配的模型": "Соответствующие модели не найдены", + "未找到差异化倍率,无需同步": "Дифференцированные множители не найдены, синхронизация не требуется", + "未提交": "Не отправлено", + "未检测到 Fluent 容器": "Контейнер Fluent не обнаружен", + "未检测到 FluentRead(流畅阅读),请确认扩展已启用": "FluentRead (плавное чтение) не обнаружен, убедитесь, что расширение включено", + "未测试": "Не протестировано", + "未登录或登录已过期,请重新登录": "Вы не вошли в систему или срок входа истек, войдите снова", + "未知": "Неизвестно", + "未知供应商": "Неизвестный поставщик", + "未知模型": "Неизвестная модель", + "未知渠道": "Неизвестный канал", + "未知状态": "Неизвестное состояние", + "未知类型": "Неизвестный тип", + "未知身份": "Неизвестная личность", + "未绑定": "Не привязано", + "未获取到授权码": "Код авторизации не получен", + "未设置": "Не настроено", + "未设置倍率模型": "Модели с неустановленным множителем", + "未配置模型": "Ненастроенные модели", + "未配置的模型列表": "Список ненастроенных моделей", + "本地": "Локальный", + "本地数据存储": "Локальное хранение данных", + "本设备:手机指纹/面容,外接:USB安全密钥": "Это устройство: отпечаток пальца/лицо телефона, внешнее: USB-ключ безопасности", + "本设备内置": "Встроенное в это устройство", + "本项目根据": "Этот проект основан на", + "权重": "Вес", + "权限设置": "Настройки прав доступа", + "条": "запись", + "条 - 第": "запись -", + "条,共": "записей, всего", + "条日志已清理!": "записей журнала очищено!", + "查看": "Просмотр", + "查看图片": "Просмотр изображения", + "查看密钥": "Просмотр ключа", + "查看当前可用的所有模型": "Просмотреть все доступные в настоящее время модели", + "查看所有可用的AI模型供应商,包括众多知名供应商的模型。": "Просмотреть всех доступных поставщиков моделей ИИ, включая модели от многих известных поставщиков.", + "查看渠道密钥": "Просмотр ключа канала", + "查询": "Запрос", + "标签": "Метка", + "标签不能为空!": "Метка не может быть пустой!", + "标签信息": "Информация о метке", + "标签名称": "Название метки", + "标签的基本配置": "Базовая конфигурация метки", + "标签组": "Группа меток", + "标签聚合": "Агрегация меток", + "标签聚合模式": "Режим агрегации меток", + "标识颜色": "Цвет идентификатора", + "根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含": "Поиск метаданных модели по имени и правилам соответствия, приоритет: точный > префикс > суффикс > содержит", + "格式示例:": "Пример формата: ", + "检查更新": "Проверить обновления", + "检测到 FluentRead(流畅阅读)": "Обнаружен FluentRead (плавное чтение)", + "检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。": "Обнаружено несколько ключей, вы можете скопировать каждый ключ отдельно или нажать \"Копировать все\" для получения полного содержимого.", + "检测到该消息后有AI回复,是否删除后续回复并重新生成?": "Обнаружен ответ ИИ после этого сообщения, удалить ли последующие ответы и сгенерировать заново?", + "检测必须等待绘图成功才能进行放大等操作": "Обнаружение должно ждать успешного вывода рисования для выполнения операций увеличения и т.д.", + "模型": "Модель", + "模型: {{ratio}}": "Модель: {{ratio}}", + "模型专用区域": "Специальная область моделей", + "模型价格": "Цена модели", + "模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}": "Цена модели {{symbol}}{{price}}, {{ratioType}} {{ratio}}", + "模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "Цена модели: {{symbol}}{{price}} * {{ratioType}}: {{ratio}} = {{symbol}}{{total}}", + "模型倍率": "Коэффициент модели", + "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}": "Коэффициент модели {{modelRatio}}, коэффициент кэша {{cacheRatio}}, коэффициент вывода {{completionRatio}}, {{ratioType}} {{ratio}}", + "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次": "Коэффициент модели {{modelRatio}}, коэффициент кэша {{cacheRatio}}, коэффициент вывода {{completionRatio}}, {{ratioType}} {{ratio}}, вызовы веб-поиска {{webSearchCallCount}} раз", + "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}": "Коэффициент модели {{modelRatio}}, коэффициент кэша {{cacheRatio}}, коэффициент вывода {{completionRatio}}, коэффициент ввода изображений {{imageRatio}}, {{ratioType}} {{ratio}}", + "模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}": "Коэффициент модели {{modelRatio}}, коэффициент вывода {{completionRatio}}, коэффициент кэша {{cacheRatio}}, коэффициент создания кэша {{cacheCreationRatio}}, {{ratioType}} {{ratio}}", + "模型倍率值": "Значение коэффициента модели", + "模型倍率和补全倍率": "Коэффициент модели и коэффициент вывода", + "模型倍率和补全倍率同时设置": "Одновременная настройка коэффициента модели и коэффициента вывода", + "模型倍率设置": "Настройка коэффициента модели", + "模型关键字": "Ключевые слова модели", + "模型列表已复制到剪贴板": "Список моделей скопирован в буфер обмена", + "模型列表已更新": "Список моделей обновлен", + "模型创建成功!": "Модель успешно создана!", + "模型名称": "Название модели", + "模型名称已存在": "Название модели уже существует", + "模型固定价格": "Фиксированная цена модели", + "模型图标": "Иконка модели", + "模型定价,需要登录访问": "Ценообразование моделей, требуется вход для доступа", + "模型广场": "Площадка моделей", + "模型支持的接口端点信息": "Информация о конечных точках интерфейса, поддерживаемых моделью", + "模型数据分析": "Анализ данных моделей", + "模型映射必须是合法的 JSON 格式!": "Сопоставление моделей должно быть в допустимом формате JSON!", + "模型更新成功!": "Модель успешно обновлена!", + "模型消耗分布": "Распределение потребления моделей", + "模型消耗趋势": "Тенденции потребления моделей", + "模型版本": "Версия модели", + "模型的详细描述和基本特性": "Подробное описание и основные характеристики модели", + "模型相关设置": "Настройки, связанные с моделью", + "模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:": "Сообщество моделей требует совместного поддержания всеми. Если вы обнаружили ошибки в данных или хотите внести новые данные о моделях, посетите:", + "模型管理": "Управление моделями", + "模型组": "Группа моделей", + "模型补全倍率(仅对自定义模型有效)": "Коэффициент вывода модели (действует только для пользовательских моделей)", + "模型请求速率限制": "Ограничение скорости запросов модели", + "模型调用次数占比": "Доля вызовов модели", + "模型调用次数排行": "Рейтинг вызовов модели", + "模型选择和映射设置": "Настройки выбора и сопоставления моделей", + "模型配置": "Конфигурация модели", + "模型重定向": "Перенаправление модели", + "模型限制列表": "Список ограничений модели", + "模板示例": "Пример шаблона", + "模糊搜索模型名称": "Нечеткий поиск по названию модели", + "次": "раз", + "欢迎使用,请完成以下设置以开始使用系统": "Добро пожаловать, пожалуйста, выполните следующие настройки, чтобы начать использовать систему", + "正在处理大内容...": "Обработка большого содержимого...", + "正在提交": "Отправка...", + "正在构造请求体预览...": "Создание предварительного просмотра тела запроса...", + "正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)": "Тестирование моделей с ${current} по ${end} (всего ${total})", + "正在跳转...": "Переход...", + "此代理仅用于图片请求转发,Webhook通知发送等,AI API请求仍然由服务器直接发出,可在渠道设置中单独配置代理": "Этот прокси используется только для пересылки изображений, отправки уведомлений Webhook и т.д., AI API запросы по-прежнему отправляются напрямую сервером, прокси можно настроить отдельно в настройках канала", + "此修改将不可逆": "Это изменение будет необратимым", + "此操作不可恢复,请仔细确认时间后再操作!": "Эта операция необратима, пожалуйста, внимательно подтвердите время перед выполнением!", + "此操作不可撤销,将永久删除已自动禁用的密钥": "Эта операция необратима, навсегда удалит автоматически отключенные ключи", + "此操作不可撤销,将永久删除该密钥": "Эта операция необратима, навсегда удалит этот ключ", + "此操作不可逆,所有数据将被永久删除": "Эта операция необратима, все данные будут удалены навсегда", + "此操作将启用用户账户": "Эта операция включит учетную запись пользователя", + "此操作将提升用户的权限级别": "Эта операция повысит уровень прав пользователя", + "此操作将禁用用户账户": "Эта операция отключит учетную запись пользователя", + "此操作将禁用该用户当前的两步验证配置,下次登录将不再强制输入验证码,直到用户重新启用。": "Эта операция отключит текущую конфигурацию двухфакторной аутентификации пользователя, при следующем входе в систему больше не потребуется вводить проверочный код, пока пользователь не включит её снова.", + "此操作将解绑用户当前的 Passkey,下次登录需要重新注册。": "Эта операция отвяжет текущий Passkey пользователя, при следующем входе потребуется повторная регистрация.", + "此操作将降低用户的权限级别": "Эта операция понизит уровень прав пользователя", + "此支付方式最低充值金额为": "Минимальная сумма пополнения для этого способа оплаты составляет", + "此设置用于系统内部计算,默认值500000是为了精确到6位小数点设计,不推荐修改。": "Этот параметр используется для внутренних вычислений системы, значение по умолчанию 500000 разработано для точности до 6 знаков после запятой, не рекомендуется изменять.", + "此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除": "Эта страница отображает только модели с неустановленной ценой или коэффициентом, после настройки они автоматически удалятся из списка", + "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "Этот параметр только для чтения, пользователю необходимо выполнить привязку через соответствующие кнопки на странице личных настроек, прямое изменение невозможно", + "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:": "Этот параметр необязательный, используется для изменения имени модели в теле запроса, представляет собой JSON строку, где ключ - это имя модели в запросе, а значение - имя модели для замены, например:", + "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改": "Этот параметр необязательный, используется для изменения имени модели в теле запроса, представляет собой JSON строку, где ключ - это имя модели в запросе, а значение - имя модели для замены, если оставить пустым, изменения не применяются", + "此项可选,用于复写返回的状态码,仅影响本地判断,不修改返回到上游的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:": "Этот параметр необязательный, используется для перезаписи возвращаемого кода состояния, влияет только на локальную проверку, не изменяет код состояния, возвращаемый upstream, например, перезапись ошибки 400 канала claude на 500 (для повтора), не злоупотребляйте этой функцией, например:", + "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数": "Этот параметр необязательный, используется для переопределения параметров запроса. Не поддерживает переопределение параметра stream", + "此项可选,用于覆盖请求头参数": "Этот параметр необязательный, используется для переопределения параметров заголовка запроса", + "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "Этот параметр необязательный, используется для выполнения API вызовов через пользовательский адрес API, в конце не должно быть /v1 и /", + "每隔多少分钟测试一次所有通道": "Как часто тестировать все каналы (в минутах)", + "永不过期": "Никогда не истекает", + "永久删除您的两步验证设置": "Окончательно удалить настройки двухфакторной аутентификации", + "永久删除所有备用码(包括未使用的)": "Окончательно удалить все резервные коды (включая неиспользованные)", + "没有可用令牌用于填充": "Нет доступных токенов для заполнения", + "没有可用模型": "Нет доступных моделей", + "没有找到匹配的模型": "Не найдено соответствующих моделей", + "没有未设置的模型": "Нет неустановленных моделей", + "没有模型可以复制": "Нет моделей для копирования", + "没有账户?": "Нет аккаунта?", + "注 册": "РЕГИСТРАЦИЯ", + "注册": "Регистрация", + "注册 Passkey": "Регистрация Passkey", + "注意": "Внимание", + "注意:JSON中重复的键只会保留最后一个同名键的值": "Внимание: в JSON повторяющиеся ключи сохранят только значение последнего ключа с тем же именем", + "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "Внимание: это не Chat API, обязательно укажите правильный адрес API, иначе это может привести к невозможности использования", + "注销": "Выйти", + "注销成功!": "Выход выполнен успешно!", + "流": "Поток", + "浅色": "Светлая", + "浅色模式": "Светлый режим", + "测试": "Тест", + "测试中": "Тестирование", + "测试中...": "Тестирование...", + "测试单个渠道操作项目组": "Тестирование отдельного канала операционной группы проекта", + "测试失败": "Тест не удался", + "测试所有渠道的最长响应时间": "Максимальное время отклика для тестирования всех каналов", + "测试所有通道": "Тестировать все каналы", + "测速": "Измерение скорости", + "消息优先级": "Приоритет сообщения", + "消息优先级,范围0-10,默认为5": "Приоритет сообщения, диапазон 0-10, по умолчанию 5", + "消息已删除": "Сообщение удалено", + "消息已复制到剪贴板": "Сообщение скопировано в буфер обмена", + "消息已更新": "Сообщение обновлено", + "消息已编辑": "Сообщение отредактировано", + "消耗分布": "Распределение потребления", + "消耗趋势": "Тенденции потребления", + "消耗额度": "Лимит потребления", + "消费": "Расходы", + "深色": "Тёмная", + "深色模式": "Тёмный режим", + "添加": "Добавить", + "添加API": "Добавить API", + "添加令牌": "Добавить токен", + "添加兑换码": "Добавить код купона", + "添加公告": "Добавить объявление", + "添加分类": "Добавить категорию", + "添加成功": "Добавлено успешно", + "添加模型": "Добавить модель", + "添加模型区域": "Добавить область модели", + "添加渠道": "Добавить канал", + "添加用户": "Добавить пользователя", + "添加聊天配置": "Добавить конфигурацию чата", + "添加键值对": "Добавить пару ключ-значение", + "添加问答": "Добавить вопрос-ответ", + "添加额度": "Добавить лимит", + "清空重定向": "Очистить перенаправление", + "清除历史日志": "Очистить историю логов", + "清除失效兑换码": "Очистить недействительные коды обмена", + "清除所有模型": "Очистить все модели", + "渠道": "Канал", + "渠道 ID": "ID канала", + "渠道ID,名称,密钥,API地址": "ID Канала, имя, Токен, адрес API", + "渠道优先级": "Приоритет канала", + "渠道信息": "Информация о канале", + "渠道创建成功!": "Канал создан успешно!", + "渠道复制失败": "Ошибка копирования канала", + "渠道复制失败: ": "Ошибка копирования канала: ", + "渠道复制成功": "Канал скопирован успешно", + "渠道密钥": "Ключ канала", + "渠道密钥信息": "Информация о ключе канала", + "渠道密钥列表": "Список ключей канала", + "渠道更新成功!": "Канал обновлён успешно!", + "渠道权重": "Вес канала", + "渠道标签": "Метки Канала", + "渠道模型信息不完整": "Информация о моделях канала неполная", + "渠道的基本配置信息": "Основная информация о конфигурации канала", + "渠道的模型测试": "Тестирование моделей канала", + "渠道的高级配置选项": "Расширенные параметры конфигурации канала", + "渠道管理": "Управление каналами", + "渠道额外设置": "Дополнительные настройки канала", + "源地址": "Исходный адрес", + "演示站点": "Демонстрационный сайт", + "演示站点模式": "Режим демонстрационного сайта", + "点击上传文件或拖拽文件到这里": "Нажмите для загрузки файла или перетащите файл сюда", + "点击下方按钮通过 Telegram 完成绑定": "Нажмите кнопку ниже для вывода привязки через Telegram", + "点击复制模型名称": "Нажмите для копирования имени модели", + "点击查看差异": "Нажмите для просмотра различий", + "点击此处": "Нажмите здесь", + "点击预览视频": "Нажмите для предварительного просмотра видео", + "点击验证按钮,使用您的生物特征或安全密钥": "Нажмите кнопку проверки, используйте ваши биометрические данные или ключ безопасности", + "版权所有": "Все права защищены", + "状态": "Статус", + "状态码复写": "Перезапись кода состояния", + "状态筛选": "Фильтр по статусу", + "状态页面Slug": "Slug страницы статуса", + "生成令牌": "Сгенерировать токен", + "生成数量": "Количество для генерации", + "生成数量必须大于0": "Количество для генерации должно быть больше 0", + "生成新的备用码": "Сгенерировать новые резервные коды", + "生成歌词": "Сгенерировать текст песни", + "生成音乐": "Сгенерировать музыку", + "用于API调用的身份验证令牌,请妥善保管": "Токен аутентификации для API вызовов, пожалуйста, храните его надёжно", + "用于配置网络代理,支持 socks5 协议": "Используется для настройки сетевого прокси, поддерживает протокол socks5", + "用以支持基于 WebAuthn 的无密码登录注册": "Используется для поддержки входа и регистрации без пароля на основе WebAuthn", + "用以支持用户校验": "Используется для поддержки проверки пользователей", + "用以支持系统的邮件发送": "Используется для поддержки отправки электронной почты системой", + "用以支持通过 GitHub 进行登录注册": "Используется для поддержки входа и регистрации через GitHub", + "用以支持通过 Linux DO 进行登录注册": "Используется для поддержки входа и регистрации через Linux DO", + "用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP": "Используется для поддержки входа через OIDC, например Okta, Auth0 и другие IdP, совместимые с протоколом OIDC", + "用以支持通过 Telegram 进行登录注册": "Используется для поддержки входа и регистрации через Telegram", + "用以支持通过微信进行登录注册": "Используется для поддержки входа и регистрации через WeChat", + "用以防止恶意用户利用临时邮箱批量注册": "Используется для предотвращения массовой регистрации злоумышленниками с использованием временных почтовых ящиков", + "用户": "Пользователь", + "用户个人功能": "Персональные функции пользователя", + "用户主页,展示系统信息": "Главная страница пользователя, отображение системной информации", + "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "Приоритет пользователя: если пользователь указал системное приглашение в запросе, будут использоваться настройки пользователя", + "用户信息": "Информация о пользователе", + "用户信息更新成功!": "Информация о пользователе обновлена успешно!", + "用户分组": "Группы пользователей", + "用户分组和额度管理": "Управление группами пользователей и лимитами", + "用户分组配置": "Конфигурация групп пользователей", + "用户协议": "Пользовательское соглашение", + "用户协议已更新": "Пользовательское соглашение обновлено", + "用户协议更新失败": "Не удалось обновить пользовательское соглашение", + "用户可选分组": "Доступные для выбора группы пользователей", + "用户名": "Имя пользователя", + "用户名或邮箱": "Имя пользователя или email", + "用户名称": "Имя пользователя", + "用户控制面板,管理账户": "Панель управления пользователя, управление аккаунтом", + "用户新建令牌时可选的分组,格式为 JSON 字符串,例如:{\"vip\": \"VIP 用户\", \"test\": \"测试\"},表示用户可以选择 vip 分组和 test 分组": "Группы, доступные для выбора при создании токена пользователем, формат JSON строки, например: {\"vip\": \"VIP пользователь\", \"test\": \"тест\"}, означает, что пользователь может выбрать группу vip и группу test", + "用户每周期最多请求完成次数": "Максимальное количество выполненных запросов пользователя за период", + "用户每周期最多请求次数": "Максимальное количество запросов пользователя за период", + "用户注册时看到的网站名称,比如'我的网站'": "Название сайта, которое видят пользователи при регистрации, например 'Мой сайт'", + "用户的基本账户信息": "Основная информация об аккаунте пользователя", + "用户管理": "Управление пользователями", + "用户组": "Группа пользователей", + "用户账户创建成功!": "Аккаунт пользователя создан успешно!", + "用户账户管理": "Управление аккаунтами пользователей", + "用时/首字": "Время/первый символ", + "留空则使用账号绑定的邮箱": "Если оставить пустым, будет использован email, привязанный к аккаунту", + "留空则使用默认端点;支持 {path, method}": "Если оставить пустым, будет использоваться конечная точка по умолчанию; поддерживает {path, method}", + "留空则默认使用服务器地址,注意不能携带http://或者https://": "Если оставить пустым, по умолчанию будет использоваться адрес сервера, обратите внимание, что нельзя указывать http:// или https://", + "登 录": "ВОЙТИ", + "登录": "Войти", + "登录成功!": "Вход выполнен успешно!", + "登录过期,请重新登录!": "Сессия истекла, пожалуйста, войдите снова!", + "白名单": "Белый список", + "的前提下使用。": "использовать при условии.", + "监控设置": "Настройки мониторинга", + "目标用户:{{username}}": "Целевой пользователь: {{username}}", + "相关项目": "Связанные проекты", + "相当于删除用户,此修改将不可逆": "Эквивалентно удалению пользователя, это изменение будет необратимым", + "矛盾": "Противоречие", + "知识库 ID": "ID базы знаний", + "确定": "Подтвердить", + "确定?": "Подтвердить?", + "确定删除此组?": "Удалить эту группу?", + "确定导入": "Подтвердить импорт", + "确定是否要修复数据库一致性?": "Подтвердить, нужно ли восстановить согласованность базы данных?", + "确定是否要删除所选通道?": "Подтвердить, нужно ли удалить выбранные каналы?", + "确定是否要删除此令牌?": "Подтвердить, нужно ли удалить этот токен?", + "确定是否要删除此兑换码?": "Подтвердить, нужно ли удалить этот код купона?", + "确定是否要删除此模型?": "Подтвердить, нужно ли удалить эту модель?", + "确定是否要删除此渠道?": "Подтвердить, нужно ли удалить этот канал?", + "确定是否要删除禁用通道?": "Подтвердить, нужно ли удалить отключенные каналы?", + "确定是否要复制此渠道?": "Подтвердить, нужно ли скопировать этот канал?", + "确定是否要注销此用户?": "Подтвердить, нужно ли деактивировать этого пользователя?", + "确定清除所有失效兑换码?": "Подтвердить очистку всех недействительных кодов купонов?", + "确定要修改所有子渠道优先级为 ": "Подтвердить изменение приоритета всех дочерних каналов на ", + "确定要修改所有子渠道权重为 ": "Подтвердить изменение веса всех дочерних каналов на ", + "确定要删除供应商 \"{{name}}\" 吗?此操作不可撤销。": "Подтвердить удаление поставщика \"{{name}}\"? Это действие нельзя отменить.", + "确定要删除所有已自动禁用的密钥吗?": "Подтвердить удаление всех автоматически отключенных ключей?", + "确定要删除所选的 {{count}} 个令牌吗?_one": "Подтвердить удаление выбранного {{count}} токена?", + "确定要删除所选的 {{count}} 个令牌吗?_few": "Подтвердить удаление выбранных {{count}} токенов?", + "确定要删除所选的 {{count}} 个令牌吗?_many": "Подтвердить удаление выбранных {{count}} токенов?", + "确定要删除所选的 {{count}} 个令牌吗?_other": "Подтвердить удаление выбранных {{count}} токенов?", + "确定要删除所选的 {{count}} 个模型吗?_one": "Подтвердить удаление выбранной {{count}} модели?", + "确定要删除所选的 {{count}} 个模型吗?_few": "Подтвердить удаление выбранных {{count}} моделей?", + "确定要删除所选的 {{count}} 个模型吗?_many": "Подтвердить удаление выбранных {{count}} моделей?", + "确定要删除所选的 {{count}} 个模型吗?_other": "Подтвердить удаление выбранных {{count}} моделей?", + "确定要删除此API信息吗?": "Подтвердить удаление этой информации API?", + "确定要删除此公告吗?": "Подтвердить удаление этого объявления?", + "确定要删除此分类吗?": "Подтвердить удаление этой категории?", + "确定要删除此密钥吗?": "Подтвердить удаление этого ключа?", + "确定要删除此问答吗?": "Подтвердить удаление этого вопроса-ответа?", + "确定要删除这条消息吗?": "Подтвердить удаление этого сообщения?", + "确定要启用所有密钥吗?": "Подтвердить включение всех ключей?", + "确定要启用此用户吗?": "Подтвердить включение этого пользователя?", + "确定要提升此用户吗?": "Подтвердить повышение этого пользователя?", + "确定要更新所有已启用通道余额吗?": "Подтвердить обновление баланса всех включенных каналов?", + "确定要测试所有通道吗?": "Подтвердить тестирование всех каналов?", + "确定要禁用所有的密钥吗?": "Подтвердить отключение всех ключей?", + "确定要禁用此用户吗?": "Подтвердить отключение этого пользователя?", + "确定要降级此用户吗?": "Подтвердить понижение этого пользователя?", + "确定重置": "Подтвердить сброс", + "确定重置模型倍率吗?": "Подтвердить сброс коэффициента модели?", + "确认": "Подтверждение", + "确认冲突项修改": "Подтвердить изменение конфликтующих элементов", + "确认删除": "Подтвердить удаление", + "确认取消密码登录": "Подтвердить отмену входа по паролю", + "确认密码": "Подтвердить пароль", + "确认导入配置": "Подтвердить импорт конфигурации", + "确认新密码": "Подтвердить новый пароль", + "确认清除历史日志": "Подтвердить очистку истории логов", + "确认禁用": "Подтвердить отключение", + "确认补单": "Подтвердить дополнение заказа", + "确认解绑": "Подтвердить отвязку", + "确认解绑 Passkey": "Подтвердить отвязку Passkey", + "确认设置并完成初始化": "Подтвердить настройки и завершить инициализацию", + "确认重置 Passkey": "Подтвердить сброс Passkey", + "确认重置两步验证": "Подтвердить сброс двухфакторной аутентификации", + "确认重置密码": "Подтвердить сброс пароля", + "示例": "Пример", + "示例:{\"default\": [200, 100], \"vip\": [0, 1000]}。": "Пример: {\"default\": [200, 100], \"vip\": [0, 1000]}.", + "视频": "Видео", + "禁用": "Отключить", + "禁用 store 透传": "Отключить сквозную передачу store", + "禁用2FA失败": "Ошибка отключения 2FA", + "禁用两步验证": "Отключить двухфакторную аутентификацию", + "禁用全部": "Отключить все", + "禁用原因": "Причина отключения", + "禁用后的影响:": "Последствия отключения:", + "禁用密钥失败": "Ошибка отключения ключа", + "禁用所有密钥失败": "Ошибка отключения всех ключей", + "禁用时间": "Время отключения", + "私有IP访问详细说明": "⚠️ Предупреждение безопасности: включение этой опции позволит доступ к ресурсам внутренней сети (localhost, частные сети). Включайте только при необходимости доступа к внутренним службам и понимании рисков безопасности.", + "私有部署地址": "Адрес частного развёртывания", + "秒": "секунда", + "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "Удаление авторских знаков One API требует предварительного разрешения, поддержка проекта требует больших усилий, если этот проект важен для вас, пожалуйста, поддержите его", + "窗口处理": "Обработка окна", + "窗口等待": "Ожидание окна", + "站点额度展示类型及汇率": "Тип отображения квот сайта и обменные курсы", + "端口配置详细说明": "Ограничение внешних запросов только к указанным портам. Поддерживает отдельные порты (80, 443) или диапазоны портов (8000-8999). Пустой список разрешает все порты. По умолчанию включает распространенные веб-порты.", + "端点": "Конечная точка", + "端点映射": "Отображение конечных точек", + "端点类型": "Тип конечной точки", + "端点组": "Группа конечных точек", + "第三方账户绑定状态(只读)": "Статус привязки сторонних аккаунтов (только для чтения)", + "等价金额:": "Эквивалентная сумма:", + "等待中": "Ожидание", + "等待获取邮箱信息...": "Ожидание получения информации об email...", + "筛选": "Фильтр", + "管理": "Управление", + "管理你的 LinuxDO OAuth App": "Управление вашим LinuxDO OAuth App", + "管理员": "Администратор", + "管理员区域": "Область администратора", + "管理员暂时未设置任何关于内容": "Администратор пока не установил никакой информации о проекте", + "管理员未开启Stripe充值!": "Администратор не включил пополнение через Stripe!", + "管理员未开启在线充值!": "Администратор не включил онлайн пополнение!", + "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "Администратор не включил функцию онлайн пополнения, свяжитесь с администратором для включения или используйте коды купонов для пополнения.", + "管理员未设置用户可选分组": "Администратор не установил доступные для выбора группы пользователей", + "管理员设置了外部链接,点击下方按钮访问": "Администратор установил внешнюю ссылку, нажмите кнопку ниже для доступа", + "管理员账号": "Аккаунт администратора", + "管理员账号已经初始化过,请继续设置其他参数": "Аккаунт администратора уже инициализирован, продолжите настройку других параметров", + "管理模型、标签、端点等预填组": "Управление предзаполненными группами моделей, тегов, конечных точек и т.д.", + "类型": "Тип", + "精确": "Точный", + "系统": "Система", + "系统令牌已复制到剪切板": "Системный токен скопирован в буфер обмена", + "系统任务记录": "Записи системных задач", + "系统信息": "Системная информация", + "系统公告": "Системные объявления", + "系统公告管理,可以发布系统通知和重要消息(最多100个,前端显示最新20条)": "Управление системными объявлениями, позволяет публиковать системные уведомления и важные сообщения (максимум 100, на интерфейсе отображаются последние 20)", + "系统初始化": "Инициализация системы", + "系统初始化失败,请重试": "Инициализация системы не удалась, попробуйте снова", + "系统初始化成功,正在跳转...": "Инициализация системы прошла успешно, выполняется перенаправление...", + "系统参数配置": "Конфигурация системных параметров", + "系统名称": "Название системы", + "系统名称已更新": "Название системы обновлено", + "系统名称更新失败": "Не удалось обновить название системы", + "系统提示覆盖": "Переопределение системного приглашения", + "系统提示词": "Системное приглашение", + "系统提示词拼接": "Объединение системных приглашений", + "系统数据统计": "Статистика системных данных", + "系统文档和帮助信息": "Системная документация и справочная информация", + "系统消息": "Системные сообщения", + "系统管理功能": "Функции системного управления", + "系统设置": "Системные настройки", + "系统访问令牌": "Токен доступа к системе", + "约": "Приблизительно", + "索引": "Индекс", + "紧凑列表": "Компактный список", + "线路描述": "Описание маршрута", + "组列表": "Список групп", + "组名": "Имя группы", + "组织": "Организация", + "组织,不填则为默认组织": "Организация, если не указано - используется организация по умолчанию", + "绑定": "Привязка", + "绑定 Telegram": "Привязка Telegram", + "绑定信息": "Информация о привязке", + "绑定微信账户": "Привязка аккаунта WeChat", + "绑定成功!": "Привязка успешна!", + "绑定邮箱地址": "Привязка адреса электронной почты", + "结束时间": "Время окончания", + "结果图片": "Изображение результата", + "绘图": "Рисование", + "绘图任务记录": "Записи задач рисования", + "绘图日志": "Журнал рисования", + "绘图设置": "Настройки рисования", + "统计Tokens": "Статистика токенов", + "统计次数": "Статистика количества", + "统计额度": "Статистика лимитов", + "继续": "Продолжить", + "缓存 Tokens": "Кэширование токенов", + "缓存: {{cacheRatio}}": "Кэш: {{cacheRatio}}", + "缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Цена кэша: {{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M токенов (коэффициент кэширования: {{cacheRatio}})", + "缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Цена кэша: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M токенов (коэффициент кэширования: {{cacheRatio}})", + "缓存倍率": "Коэффициент кэширования", + "缓存创建 Tokens": "Создание кэша токенов", + "缓存创建: {{cacheCreationRatio}}": "Создание кэша: {{cacheCreationRatio}}", + "缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "Цена создания кэша: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M токенов (коэффициент создания кэша: {{cacheCreationRatio}})", + "编辑": "Редактировать", + "编辑API": "Редактировать API", + "编辑供应商": "Редактировать поставщика", + "编辑公告": "Редактировать объявление", + "编辑公告内容": "Редактировать содержимое объявления", + "编辑分类": "Редактировать категорию", + "编辑成功": "Редактирование выполнено успешно", + "编辑标签": "Редактировать тег", + "编辑模型": "Редактировать модель", + "编辑模式": "Режим редактирования", + "编辑用户": "Редактировать пользователя", + "编辑聊天配置": "Редактировать настройки чата", + "编辑问答": "Редактировать вопрос-ответ", + "缩词": "Сокращение", + "缺省 MaxTokens": "MaxTokens по умолчанию", + "网站地址": "Адрес веб-сайта", + "网站域名标识": "Идентификатор домена веб-сайта", + "网络错误": "Сетевая ошибка", + "置信度": "Уровень доверия", + "聊天": "Чат", + "聊天会话管理": "Управление сессиями чата", + "聊天区域": "Область чата", + "聊天应用名称": "Название чат-приложения", + "聊天应用名称已存在,请使用其他名称": "Название чат-приложения уже существует, используйте другое название", + "聊天设置": "Настройки чата", + "聊天配置": "Конфигурация чата", + "聊天链接配置错误,请联系管理员": "Ошибка конфигурации ссылки чата, свяжитесь с администратором", + "联系我们": "Свяжитесь с нами", + "腾讯混元": "Tencent Hunyuan", + "自动分组auto,从第一个开始选择": "Автоматическая группировка auto, выбор начинается с первого", + "自动检测": "Автоматическое обнаружение", + "自动模式": "Автоматический режим", + "自动测试所有通道间隔时间": "Интервал автоматического тестирования всех каналов", + "自动禁用": "Автоматическое отключение", + "自动禁用关键词": "Ключевые слова для автоматического отключения", + "自动选择": "Автоматический выбор", + "自定义充值数量选项": "Пользовательские опции количества пополнения", + "自定义充值数量选项不是合法的 JSON 数组": "Пользовательские опции количества пополнения не являются допустимым массивом JSON", + "自定义变焦-提交": "Пользовательское масштабирование-отправка", + "自定义模型名称": "Пользовательское название модели", + "自定义货币": "Пользовательская валюта", + "自定义货币符号": "Пользовательский символ валюты", + "自用模式": "Режим личного использования", + "自适应列表": "Адаптивный список", + "节省": "Экономия", + "花费": "Расходы", + "花费时间": "Затраченное время", + "若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置": "Если ваш OIDC Provider поддерживает Discovery Endpoint, вы можете указать только OIDC Well-Known URL, и система автоматически получит OIDC конфигурацию", + "获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确": "Не удалось получить OIDC конфигурацию, проверьте состояние сети и правильность Well-Known URL", + "获取 OIDC 配置成功!": "OIDC конфигурация успешно получена!", + "获取2FA状态失败": "Не удалось получить статус 2FA", + "获取初始化状态失败": "Не удалось получить статус инициализации", + "获取启用模型失败": "Не удалось получить включенные модели", + "获取启用模型失败:": "Не удалось получить включенные модели:", + "获取密钥": "Получить ключ", + "获取密钥失败": "Не удалось получить ключ", + "获取密钥状态失败": "Не удалось получить статус ключа", + "获取未配置模型失败": "Не удалось получить настроенные модели", + "获取模型列表": "Получить список моделей", + "获取模型列表失败": "Не удалось получить список моделей", + "获取渠道失败:": "Не удалось получить канал:", + "获取组列表失败": "Не удалось получить список групп", + "获取金额失败": "Не удалось получить сумму", + "获取验证码": "Получить код подтверждения", + "补全": "Вывод", + "补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "Цена вывода: {{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M токенов (коэффициент вывода: {{completionRatio}})", + "补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens": "Цена вывода: {{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M токенов", + "补全倍率": "Коэффициент вывода", + "补全倍率值": "Значение коэффициента вывода", + "补单": "Вывод заказа", + "补单失败": "Не удалось дополнить заказ", + "补单成功": "Заказ успешно дополнен", + "表单引用错误,请刷新页面重试": "Ошибка ссылки формы, обновите страницу и попробуйте снова", + "表格视图": "Табличное представление", + "覆盖模式:将完全替换现有的所有密钥": "Режим перезаписи: полностью заменит все существующие ключи", + "覆盖现有密钥": "Перезаписать существующие ключи", + "角色": "Роль", + "解析响应数据时发生错误": "Произошла ошибка при разборе данных ответа", + "解析密钥文件失败: {{msg}}": "Не удалось разобрать файл ключа: {{msg}}", + "解绑 Passkey": "Отвязать Passkey", + "解绑后将无法使用 Passkey 登录,确定要继续吗?": "После отвязки невозможно будет использовать Passkey для входа, продолжить?", + "计费类型": "Тип выставления счёта", + "计费过程": "Процесс выставления счёта", + "订单号": "Номер заказа", + "讯飞星火": "iFlytek Spark", + "记录请求与错误日志IP": "Записывать IP запросов и логов ошибок", + "设备类型偏好": "Предпочтения типа устройства", + "设置 Logo": "Установить Logo", + "设置2FA失败": "Ошибка настройки 2FA", + "设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}": "Установить скидки для разных сумм пополнения, ключ - сумма пополнения, значение - ставка скидки, например: {\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}", + "设置两步验证": "Настроить двухфакторную аутентификацию", + "设置令牌可用额度和数量": "Установить доступный лимит и количество токенов", + "设置令牌的基本信息": "Установить основную информацию токена", + "设置令牌的访问限制": "Установить ограничения доступа токена", + "设置保存失败": "Ошибка сохранения настроек", + "设置保存成功": "Настройки сохранены успешно", + "设置兑换码的基本信息": "Установить основную информацию кода купона", + "设置兑换码的额度和数量": "Установить лимит и количество кодов купонов", + "设置公告": "Установить объявление", + "设置关于": "Установить информацию о проекте", + "设置已保存": "Настройки сохранены", + "设置模型的基本信息": "Установить основную информацию модели", + "设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱": "Установить адрес электронной почты для получения предупреждений о лимите, если оставить пустым, будет использован привязанный к аккаунту email", + "设置用户协议": "Установить пользовательское соглашение", + "设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]": "Установить опции количества пополнения, доступные для выбора пользователем, например: [10, 20, 50, 100, 200, 500]", + "设置管理员登录信息": "Установить информацию для входа администратора", + "设置类型": "Тип настроек", + "设置系统名称": "Установить имя системы", + "设置过短会影响数据库性能": "Слишком короткие настройки могут повлиять на производительность базы данных", + "设置隐私政策": "Установить политику конфиденциальности", + "设置页脚": "Установить нижний колонтитул", + "设置预填组的基本信息": "Установить основную информацию для предзаполненной группы", + "设置首页内容": "Установить содержимое главной страницы", + "设置默认地区和特定模型的专用地区": "Установить регион по умолчанию и специальные регионы для конкретных моделей", + "设计与开发由": "Дизайн и разработка", + "访问限制": "Ограничения доступа", + "该供应商提供多种AI模型,适用于不同的应用场景。": "Этот поставщик предоставляет различные модели ИИ, подходящие для разных сценариев применения.", + "该分类下没有可用模型": "В этой категории нет доступных моделей", + "该域名已存在于白名单中": "Этот домен уже существует в белом списке", + "该数据可能不可信,请谨慎使用": "Эти данные могут быть недостоверными, используйте с осторожностью", + "该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置": "Этот адрес сервера повлияет на адрес обратного вызова оплаты и адрес отображения главной страницы по умолчанию, убедитесь в правильной конфигурации", + "该模型存在固定价格与倍率计费方式冲突,请确认选择": "Эта модель имеет конфликт между фиксированной ценой и способом выставления счёта по коэффициенту, подтвердите выбор", + "详情": "Подробности", + "语音输入": "Голосовой ввод", + "语音输出": "Голосовой вывод", + "说明": "Описание", + "说明:": "Описание:", + "说明信息": "Информация об описании", + "请上传密钥文件": "Пожалуйста, загрузите файл ключа", + "请上传密钥文件!": "Пожалуйста, загрузите файл ключа!", + "请为渠道命名": "Пожалуйста, назовите канал", + "请先填写服务器地址": "Пожалуйста, сначала заполните адрес сервера", + "请先输入密钥": "Пожалуйста, сначала введите ключ", + "请先选择同步渠道": "Пожалуйста, сначала выберите канал синхронизации", + "请先选择模型!": "Пожалуйста, сначала выберите модель!", + "请先选择要删除的令牌!": "Пожалуйста, сначала выберите токен для удаления!", + "请先选择要删除的通道!": "Пожалуйста, сначала выберите канал для удаления!", + "请先选择要设置标签的渠道!": "Пожалуйста, сначала выберите канал для установки тега!", + "请先选择需要批量设置的模型": "Пожалуйста, сначала выберите модели для пакетной настройки", + "请先阅读并同意用户协议和隐私政策": "Пожалуйста, сначала прочтите и согласитесь с пользовательским соглашением и политикой конфиденциальности", + "请再次输入新密码": "Пожалуйста, введите новый пароль ещё раз", + "请前往个人设置 → 安全设置进行配置。": "Пожалуйста, перейдите в Личные настройки → Настройки безопасности для конфигурации.", + "请勿过度信任此功能,IP可能被伪造": "Не доверяйте этой функции чрезмерно, IP может быть подделан", + "请在系统设置页面编辑分组倍率以添加新的分组:": "Пожалуйста, отредактируйте коэффициенты групп на странице системных настроек для добавления новой группы:", + "请填写完整的管理员账号信息": "Пожалуйста, заполните полную информацию об учётной записи администратора", + "请填写密钥": "Пожалуйста, заполните ключ", + "请填写渠道名称和渠道密钥!": "Пожалуйста, заполните имя канала и ключ канала!", + "请填写部署地区": "Пожалуйста, заполните регион развертывания", + "请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。": "Пожалуйста, храните информацию о ключе в безопасности, не разглашайте её другим. При наличии сомнений в безопасности, своевременно замените ключ.", + "请检查渠道配置或刷新重试": "Пожалуйста, проверьте конфигурацию канала или обновите и попробуйте снова", + "请检查表单填写是否正确": "Пожалуйста, проверьте правильность заполнения формы", + "请检查输入": "Пожалуйста, проверьте ввод", + "请求发生错误": "Произошла ошибка запроса", + "请求发生错误: ": "Произошла ошибка запроса: ", + "请求后端接口失败:": "Не удалось запросить внутренний интерфейс:", + "请求失败": "Запрос не удался", + "请求头覆盖": "Переопределение заголовков запроса", + "请求并计费模型": "Запрос и выставление счёта модели", + "请求路径": "Путь запроса", + "请求时长: ${time}s": "Время запроса: ${time}s", + "请求次数": "Количество запросов", + "请求结束后多退少补": "После вывода запроса возврат излишков и доплата недостатка", + "请求预扣费额度": "Запрос суммы предварительного удержания", + "请点击我": "Пожалуйста, нажмите на меня", + "请确认以下设置信息,点击\"初始化系统\"开始配置": "Пожалуйста, подтвердите следующую информацию о настройках, нажмите \"Инициализация системы\" для начала конфигурации", + "请确认您已了解禁用两步验证的后果": "Пожалуйста, подтвердите, что вы понимаете последствия отключения двухфакторной аутентификации", + "请确认管理员密码": "Пожалуйста, подтвердите пароль администратора", + "请稍后几秒重试,Turnstile 正在检查用户环境!": "Пожалуйста, повторите попытку через несколько секунд, Turnstile проверяет среду пользователя!", + "请联系管理员在系统设置中配置API信息": "Пожалуйста, свяжитесь с администратором для настройки информации API в системных настройках", + "请联系管理员在系统设置中配置Uptime": "Пожалуйста, свяжитесь с администратором для настройки Uptime в системных настройках", + "请联系管理员在系统设置中配置公告信息": "Пожалуйста, свяжитесь с администратором для настройки информации об объявлениях в системных настройках", + "请联系管理员在系统设置中配置常见问答": "Пожалуйста, свяжитесь с администратором для настройки часто задаваемых вопросов в системных настройках", + "请联系管理员配置聊天链接": "Пожалуйста, свяжитесь с администратором для настройки ссылки чата", + "请至少选择一个令牌!": "Пожалуйста, выберите хотя бы один токен!", + "请至少选择一个兑换码!": "Пожалуйста, выберите хотя бы один код купона!", + "请至少选择一个模型": "Пожалуйста, выберите хотя бы одну модель", + "请至少选择一个模型!": "Пожалуйста, выберите хотя бы одну модель!", + "请至少选择一个渠道": "Пожалуйста, выберите хотя бы один канал", + "请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "Пожалуйста, введите AZURE_OPENAI_ENDPOINT, например: https://docs-test-001.openai.azure.com", + "请输入 JSON 格式的密钥内容,例如:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}": "Пожалуйста, введите содержимое ключа в формате JSON, например:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}", + "请输入 OIDC 的 Well-Known URL": "Пожалуйста, введите Well-Known URL OIDC", + "请输入6位验证码或8位备用码": "Пожалуйста, введите 6-значный код подтверждения или 8-значный резервный код", + "请输入API地址": "Пожалуйста, введите адрес API", + "请输入API地址!": "Пожалуйста, введите адрес API!", + "请输入Bark推送URL": "Пожалуйста, введите URL для push-уведомлений Bark", + "请输入Bark推送URL,例如: https://api.day.app/yourkey/{{title}}/{{content}}": "Пожалуйста, введите URL для push-уведомлений Bark, например: https://api.day.app/yourkey/{{title}}/{{content}}", + "请输入Gotify应用令牌": "Пожалуйста, введите токен приложения Gotify", + "请输入Gotify服务器地址": "Пожалуйста, введите адрес сервера Gotify", + "请输入Gotify服务器地址,例如: https://gotify.example.com": "Пожалуйста, введите адрес сервера Gotify, например: https://gotify.example.com", + "请输入Uptime Kuma地址": "Пожалуйста, введите адрес Uptime Kuma", + "请输入Uptime Kuma服务地址,如:https://status.example.com": "Пожалуйста, введите адрес службы Uptime Kuma, например: https://status.example.com", + "请输入URL链接": "Пожалуйста, введите URL-ссылку", + "请输入Webhook地址": "Пожалуйста, введите адрес Webhook", + "请输入Webhook地址,例如: https://example.com/webhook": "Пожалуйста, введите адрес Webhook, например: https://example.com/webhook", + "请输入你的账户名以确认删除!": "Пожалуйста, введите имя вашей учётной записи для подтверждения удаления!", + "请输入供应商名称": "Пожалуйста, введите имя поставщика", + "请输入供应商名称,如:OpenAI": "Пожалуйста, введите имя поставщика, например: OpenAI", + "请输入供应商描述": "Пожалуйста, введите описание поставщика", + "请输入兑换码": "Пожалуйста, введите код купона", + "请输入兑换码!": "Пожалуйста, введите код купона!", + "请输入公告内容": "Пожалуйста, введите содержание объявления", + "请输入公告内容(支持 Markdown/HTML)": "Пожалуйста, введите содержание объявления (поддерживается Markdown/HTML)", + "请输入分类名称": "Пожалуйста, введите имя категории", + "请输入分类名称,如:OpenAI、Claude等": "Пожалуйста, введите имя категории, например: OpenAI, Claude и т.д.", + "请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com": "Пожалуйста, введите путь перед /suno, обычно это доменное имя, например: https://api.example.com", + "请输入原密码": "Пожалуйста, введите старый пароль", + "请输入原密码!": "Пожалуйста, введите старый пароль!", + "请输入名称": "Пожалуйста, введите имя", + "请输入回答内容": "Пожалуйста, введите содержание ответа", + "请输入回答内容(支持 Markdown/HTML)": "Пожалуйста, введите содержание ответа (поддерживается Markdown/HTML)", + "请输入图标名称": "Пожалуйста, введите имя иконки", + "请输入填充值": "Пожалуйста, введите значение заполнения", + "请输入备注(仅管理员可见)": "Пожалуйста, введите примечание (видимо только администратору)", + "请输入完整的 JSON 格式密钥内容": "Пожалуйста, введите полное содержимое ключа в формате JSON", + "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Пожалуйста, введите полный URL, например: https://api.openai.com/v1/chat/completions", + "请输入完整的URL链接": "Пожалуйста, введите полную URL-ссылку", + "请输入密码": "Пожалуйста, введите пароль", + "请输入密钥": "Пожалуйста, введите ключ", + "请输入密钥,一行一个": "Пожалуйста, введите ключи, по одному в строке", + "请输入密钥!": "Пожалуйста, введите ключ!", + "请输入您的密码": "Пожалуйста, введите ваш пароль", + "请输入您的用户名以确认删除": "Пожалуйста, введите ваше имя пользователя для подтверждения удаления", + "请输入您的用户名或邮箱地址": "Пожалуйста, введите ваше имя пользователя или адрес электронной почты", + "请输入您的邮箱地址": "Пожалуйста, введите ваш адрес электронной почты", + "请输入您的问题...": "Пожалуйста, введите ваш вопрос...", + "请输入数值": "Пожалуйста, введите числовое значение", + "请输入数字": "Пожалуйста, введите число", + "请输入新密码": "Пожалуйста, введите новый пароль", + "请输入新密码!": "Пожалуйста, введите новый пароль!", + "请输入新建数量": "Пожалуйста, введите количество для создания", + "请输入新标签,留空则解散标签": "Пожалуйста, введите новый тег, оставьте пустым для распускания тега", + "请输入新的剩余额度": "Пожалуйста, введите новый оставшийся лимит", + "请输入新的密码,最短 8 位": "Пожалуйста, введите новый пароль, минимум 8 символов", + "请输入新的显示名称": "Пожалуйста, введите новое отображаемое имя", + "请输入新的用户名": "Пожалуйста, введите новое имя пользователя", + "请输入显示名称": "Пожалуйста, введите отображаемое имя", + "请输入有效的数字": "Пожалуйста, введите действительное число", + "请输入标签名称": "Пожалуйста, введите имя тега", + "请输入模型倍率": "Пожалуйста, введите коэффициент модели", + "请输入模型倍率和补全倍率": "Пожалуйста, введите коэффициент модели и коэффициент вывода", + "请输入模型名称": "Пожалуйста, введите имя модели", + "请输入模型名称,如:gpt-4": "Пожалуйста, введите имя модели, например: gpt-4", + "请输入模型描述": "Пожалуйста, введите описание модели", + "请输入消息内容...": "Пожалуйста, введите содержание сообщения...", + "请输入状态页面Slug": "Пожалуйста, введите Slug страницы состояния", + "请输入状态页面的Slug,如:my-status": "Пожалуйста, введите Slug страницы состояния, например: my-status", + "请输入生成数量": "Пожалуйста, введите количество для генерации", + "请输入用户名": "Пожалуйста, введите имя пользователя", + "请输入私有部署地址,格式为:https://fastgpt.run/api/openapi": "Пожалуйста, введите адрес частного развертывания, формат: https://fastgpt.run/api/openapi", + "请输入管理员密码": "Пожалуйста, введите пароль администратора", + "请输入管理员用户名": "Пожалуйста, введите имя пользователя администратора", + "请输入线路描述": "Пожалуйста, введите описание линии", + "请输入组名": "Пожалуйста, введите имя группы", + "请输入组描述": "Пожалуйста, введите описание группы", + "请输入组织org-xxx": "Пожалуйста, введите организацию org-xxx", + "请输入聊天应用名称": "Пожалуйста, введите имя чат-приложения", + "请输入补全倍率": "Пожалуйста, введите коэффициент вывода", + "请输入要设置的标签名称": "Пожалуйста, введите имя тега для установки", + "请输入认证器验证码": "Пожалуйста, введите код подтверждения аутентификатора", + "请输入认证器验证码或备用码": "Пожалуйста, введите код подтверждения аутентификатора или резервный код", + "请输入说明": "Пожалуйста, введите описание", + "请输入邮箱!": "Пожалуйста, введите адрес электронной почты!", + "请输入邮箱地址": "Пожалуйста, введите адрес электронной почты", + "请输入邮箱验证码!": "Пожалуйста, введите код подтверждения электронной почты!", + "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "Пожалуйста, введите регион развертывания, например: us-central1\nПоддерживается формат сопоставления моделей\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}", + "请输入问题标题": "Пожалуйста, введите заголовок вопроса", + "请输入预警阈值": "Пожалуйста, введите порог предупреждения", + "请输入预警额度": "Пожалуйста, введите лимит предупреждения", + "请输入额度": "Пожалуйста, введите лимит", + "请输入验证码": "Пожалуйста, введите код подтверждения", + "请输入验证码或备用码": "Пожалуйста, введите код подтверждения или резервный код", + "请输入默认 API 版本,例如:2025-04-01-preview": "Пожалуйста, введите версию API по умолчанию, например: 2025-04-01-preview", + "请选择API地址": "Пожалуйста, выберите адрес API", + "请选择你的复制方式": "Пожалуйста, выберите ваш способ копирования", + "请选择使用模式": "Пожалуйста, выберите режим использования", + "请选择分组": "Пожалуйста, выберите группу", + "请选择发布日期": "Пожалуйста, выберите дату публикации", + "请选择可以使用该渠道的分组": "Пожалуйста, выберите группы, которые могут использовать этот канал", + "请选择可以使用该渠道的分组,留空则不更改": "Пожалуйста, выберите группы, которые могут использовать этот канал, оставьте пустым для без изменений", + "请选择同步语言": "Пожалуйста, выберите язык синхронизации", + "请选择名称匹配类型": "Пожалуйста, выберите тип сопоставления имён", + "请选择多密钥使用策略": "Пожалуйста, выберите стратегию использования нескольких ключей", + "请选择密钥更新模式": "Пожалуйста, выберите режим обновления ключей", + "请选择密钥格式": "Пожалуйста, выберите формат ключей", + "请选择日志记录时间": "Пожалуйста, выберите время записи журнала", + "请选择模型": "Пожалуйста, выберите модель", + "请选择模型。": "Пожалуйста, выберите модель.", + "请选择消息优先级": "Пожалуйста, выберите приоритет сообщения", + "请选择渠道类型": "Пожалуйста, выберите тип канала", + "请选择组类型": "Пожалуйста, выберите тип группы", + "请选择该令牌支持的模型,留空支持所有模型": "Пожалуйста, выберите модели, поддерживаемые этим токеном, оставьте пустым для поддержки всех моделей", + "请选择该渠道所支持的模型": "Пожалуйста, выберите модели, поддерживаемые этим каналом", + "请选择该渠道所支持的模型,留空则不更改": "Пожалуйста, выберите модели, поддерживаемые этим каналом, оставьте пустым для без изменений", + "请选择过期时间": "Пожалуйста, выберите время истечения", + "请选择通知方式": "Пожалуйста, выберите способ уведомления", + "调用次数": "Количество вызовов", + "调用次数分布": "Распределение количества вызовов", + "调用次数排行": "Рейтинг количества вызовов", + "调试信息": "Отладочная информация", + "谨慎": "Осторожно", + "警告": "Предупреждение", + "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔": "Предупреждение: после включения поддержания активности, если канал выдаёт ошибку после записи данных поддержания активности, система не может повторить попытку, если необходимо включить, рекомендуется установить максимально возможный интервал Ping", + "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "Предупреждение: отключение двухфакторной аутентификации навсегда удалит ваши настройки проверки и все резервные коды, эта операция необратима!", + "豆包": "Doubao", + "账单": "Счёт", + "账户充值": "Пополнение счёта", + "账户已删除!": "Учётная запись удалена!", + "账户已锁定": "Учётная запись заблокирована", + "账户数据": "Данные учётной записи", + "账户管理": "Управление учётными записями", + "账户绑定": "Привязка учётной записи", + "账户绑定、安全设置和身份验证": "Привязка учётной записи, настройки безопасности и аутентификация", + "账户统计": "Статистика учётной записи", + "货币单位": "Валюта", + "购买兑换码": "Покупка кодов купонов", + "资源消耗": "Потребление ресурсов", + "起始时间": "Время начала", + "超级管理员": "Суперадминистратор", + "超级管理员未设置充值链接!": "Суперадминистратор не установил ссылку пополнения!", + "跟随系统主题设置": "Следовать настройкам темы системы", + "跳转": "Перейти", + "轮询": "Опрос", + "轮询模式": "Режим опроса", + "轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能": "Режим опроса должен использоваться вместе с функциями Redis и кэширования памяти, иначе производительность значительно снизится, и функция опроса не будет реализована", + "输入": "Ввод", + "输入 OIDC 的 Authorization Endpoint": "Введите Authorization Endpoint OIDC", + "输入 OIDC 的 Client ID": "Введите Client ID OIDC", + "输入 OIDC 的 Token Endpoint": "Введите Token Endpoint OIDC", + "输入 OIDC 的 Userinfo Endpoint": "Введите Userinfo Endpoint OIDC", + "输入IP地址后回车,如:8.8.8.8": "Введите IP-адрес и нажмите Enter, например: 8.8.8.8", + "输入JSON对象": "Введите JSON-объект", + "输入价格": "Введите цену", + "输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}": "Цена ввода: {{symbol}}{{price}} / 1M tokens{{audioPrice}}", + "输入你注册的 LinuxDO OAuth APP 的 ID": "Введите ID вашего зарегистрированного LinuxDO OAuth APP", + "输入你的账户名{{username}}以确认删除": "Введите имя вашей учётной записи {{username}} для подтверждения удаления", + "输入域名后回车": "Введите доменное имя и нажмите Enter", + "输入域名后回车,如:example.com": "Введите доменное имя и нажмите Enter, например: example.com", + "输入密码,最短 8 位,最长 20 位": "Введите пароль, минимум 8 символов, максимум 20 символов", + "输入数字": "Введите число", + "输入标签或使用\",\"分隔多个标签": "Введите теги или используйте \",\" для разделения нескольких тегов", + "输入模型倍率": "Введите коэффициент модели", + "输入每次价格": "Введите цену за использование", + "输入端口后回车,如:80 或 8000-8999": "Введите порт и нажмите Enter, например: 80 или 8000-8999", + "输入系统提示词,用户的系统提示词将优先于此设置": "Введите системный промпт, системные промпты пользователя будут иметь приоритет над этой настройкой", + "输入自定义模型名称": "Введите имя пользовательской модели", + "输入补全价格": "Введите цену вывода", + "输入补全倍率": "Введите коэффициент вывода", + "输入要添加的邮箱域名": "Введите доменное имя электронной почты для добавления", + "输入认证器应用显示的6位数字验证码": "Введите 6-значный код подтверждения, отображаемый в приложении аутентификатора", + "输入邮箱地址": "Введите адрес электронной почты", + "输入项目名称,按回车添加": "Введите имя проекта, нажмите Enter для добавления", + "输入验证码": "Введите код подтверждения", + "输入验证码完成设置": "Введите код подтверждения для вывода настройки", + "输出": "Вывод", + "输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}": "Вывод {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}", + "输出价格": "Цена вывода", + "输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "Цена вывода: {{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M токенов (коэффициент вывода: {{completionRatio}})", + "边栏设置": "Настройки боковой панели", + "过期时间": "Время истечения", + "过期时间不能早于当前时间!": "Время истечения не может быть раньше текущего времени!", + "过期时间快捷设置": "Быстрая настройка времени истечения", + "过期时间格式错误!": "Ошибка формата времени истечения!", + "运营设置": "Операционные настройки", + "返回登录": "Вернуться к входу", + "这是重复键中的最后一个,其值将被使用": "Это последний ключ в повторяющихся, его значение будет использовано", + "进度": "Прогресс", + "进行中": "В процессе", + "进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "При выполнении этой операции могут возникнуть ошибки доступа к каналам, используйте только при проблемах с базой данных", + "连接保活设置": "Настройки поддержания соединения", + "连接已断开": "Соединение разорвано", + "追加到现有密钥": "Добавить к существующим ключам", + "追加模式:将新密钥添加到现有密钥列表末尾": "Режим добавления: добавление новых ключей в конец списка существующих ключей", + "追加模式:新密钥将添加到现有密钥列表的末尾": "Режим добавления: новые ключи будут добавлены в конец списка существующих ключей", + "退出": "Выход", + "适用于个人使用的场景,不需要设置模型价格": "Подходит для сценариев личного использования, не требует установки цен на модели", + "适用于为多个用户提供服务的场景": "Подходит для сценариев предоставления услуг нескольким пользователям", + "适用于展示系统功能的场景,提供基础功能演示": "Подходит для сценариев демонстрации системных функций, предоставляет демонстрацию базовых функций", + "适配 -thinking、-thinking-预算数字 和 -nothinking 后缀": "Адаптация суффиксов -thinking, -thinking-бюджетные-цифры и -nothinking", + "选择充值额度": "Выберите сумму пополнения", + "选择分组": "Выберите группу", + "选择同步来源": "Выберите источник синхронизации", + "选择同步渠道": "Выберите канал синхронизации", + "选择同步语言": "Выберите язык синхронизации", + "选择成功": "Выбрано успешно", + "选择支付方式": "Выберите способ оплаты", + "选择支持的认证设备类型": "Выберите поддерживаемые типы устройств аутентификации", + "选择方式": "Выберите способ", + "选择时间": "Выберите время", + "选择模型": "Выберите модель", + "选择模型供应商": "Выберите поставщика моделей", + "选择模型后可一键填充当前选中令牌(或本页第一个令牌)。": "После выбора модели можно одним нажатием заполнить текущий выбранный токен (или первый токен на этой странице).", + "选择模型开始对话": "Выберите модель для начала диалога", + "选择端点类型": "Выберите тип конечной точки", + "选择系统运行模式": "Выберите режим работы системы", + "选择组类型": "Выберите тип группы", + "选择要覆盖的冲突项": "Выберите конфликтующие элементы для перезаписи", + "选择语言": "Выберите язык", + "选择过期时间(可选,留空为永久)": "Выберите время истечения (необязательно, оставьте пустым для постоянного)", + "透传请求体": "Прямая передача тела запроса", + "通义千问": "Tongyi Qianwen", + "通用设置": "Общие настройки", + "通知": "Уведомления", + "通知、价格和隐私相关设置": "Настройки уведомлений, цен и конфиденциальности", + "通知内容": "Содержание уведомления", + "通知内容,支持 {{value}} 变量占位符": "Содержание уведомления, поддерживает заполнители переменных {{value}}", + "通知方式": "Способ уведомления", + "通知标题": "Заголовок уведомления", + "通知类型 (quota_exceed: 额度预警)": "Тип уведомления (quota_exceed: предупреждение о превышении квоты)", + "通知邮箱": "Email для уведомлений", + "通知配置": "Конфигурация уведомлений", + "通过划转功能将奖励额度转入到您的账户余额中": "Через функцию перевода переведите вознаграждение на баланс вашей учётной записи", + "通过密码注册时需要进行邮箱验证": "При регистрации через пароль требуется проверка электронной почты", + "通道 ${name} 余额更新成功!": "Баланс канала ${name} успешно обновлен!", + "通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。": "Канал ${name} успешно протестирован, модель ${model} заняла ${time.toFixed(2)} секунд.", + "通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Канал ${name} успешно протестирован, заняло ${time.toFixed(2)} секунд.", + "速率限制设置": "Настройки ограничения скорости", + "邀请": "Приглашение", + "邀请人": "Пригласивший", + "邀请人数": "Количество приглашённых", + "邀请信息": "Информация о приглашении", + "邀请奖励": "Вознаграждение за приглашение", + "邀请好友注册,好友充值后您可获得相应奖励": "Пригласите друзей для регистрации, после пополнения счёта друзьями вы получите соответствующее вознаграждение", + "邀请好友获得额外奖励": "Пригласите друзей для получения дополнительного вознаграждения", + "邀请新用户奖励额度": "Лимит вознаграждения за приглашение новых пользователей", + "邀请的好友越多,获得的奖励越多": "Чем больше друзей вы пригласите, тем больше вознаграждение получите", + "邀请码": "Код приглашения", + "邀请获得额度": "Получить лимит через приглашение", + "邀请链接": "Ссылка приглашения", + "邀请链接已复制到剪切板": "Ссылка приглашения скопирована в буфер обмена", + "邮件通知": "Email-уведомления", + "邮箱": "Электронная почта", + "邮箱地址": "Адрес электронной почты", + "邮箱域名格式不正确,请输入有效的域名,如 gmail.com": "Неверный формат домена электронной почты, введите действительный домен, например gmail.com", + "邮箱域名白名单格式不正确": "Неверный формат белого списка доменов электронной почты", + "邮箱账户绑定成功!": "Учётная запись электронной почты успешно привязана!", + "部分保存失败": "Частичное сохранение не удалось", + "部分保存失败,请重试": "Частичное сохранение не удалось, попробуйте снова", + "部分渠道测试失败:": "Частичный сбой тестирования каналов:", + "部署地区": "Регион развертывания", + "配置": "Конфигурация", + "配置 GitHub OAuth App": "Настроить GitHub OAuth App", + "配置 Linux DO OAuth": "Настроить Linux DO OAuth", + "配置 OIDC": "Настроить OIDC", + "配置 Passkey": "Настроить Passkey", + "配置 SMTP": "Настроить SMTP", + "配置 Telegram 登录": "Настроить вход через Telegram", + "配置 Turnstile": "Настроить Turnstile", + "配置 WeChat Server": "Настроить WeChat Server", + "配置和消息已全部重置": "Конфигурация и сообщения полностью сброшены", + "配置导入成功": "Конфигурация успешно импортирована", + "配置已导出到下载文件夹": "Конфигурация экспортирована в папку загрузок", + "配置已重置,对话消息已保留": "Конфигурация сброшена, сообщения диалога сохранены", + "配置文件同步": "Синхронизация файлов конфигурации", + "配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全": "Настроить защиту от подделки запросов на стороне сервера (SSRF) для защиты безопасности внутренних сетевых ресурсов", + "配置登录注册": "Настроить вход и регистрацию", + "配置说明": "Описание конфигурации", + "配置邮箱域名白名单": "Настроить белый список доменов электронной почты", + "重复提交": "Повторная отправка", + "重复的键名": "Повторяющееся имя ключа", + "重复的键名,此值将被后面的同名键覆盖": "Повторяющееся имя ключа, это значение будет перезаписано последующим ключом с тем же именем", + "重定向 URL 填": "Заполнить URL перенаправления", + "重新发送": "Отправить снова", + "重新生成": "Сгенерировать заново", + "重新生成备用码": "Сгенерировать резервные коды заново", + "重新生成备用码失败": "Не удалось сгенерировать резервные коды заново", + "重新生成备用码将使现有的备用码失效,请确保您已保存了当前的备用码。": "Повторная генерация резервных кодов сделает существующие резервные коды недействительными, убедитесь, что вы сохранили текущие резервные коды.", + "重绘": "Перерисовать", + "重置": "Сброс", + "重置 2FA": "Сброс 2FA", + "重置 Passkey": "Сброс Passkey", + "重置为默认": "Сбросить по умолчанию", + "重置模型倍率": "Сбросить коэффициенты моделей", + "重置选项": "Сбросить опции", + "重置邮件发送成功,请检查邮箱!": "Письмо о сбросе успешно отправлено, проверьте электронную почту!", + "重置配置": "Сбросить конфигурацию", + "重试": "Повторить попытку", + "钱包管理": "Управление кошельком", + "链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1": "В ссылке {key} будет автоматически заменен на sk-xxxx, {address} будет автоматически заменен на адрес сервера, установленный в системе, без / и /v1 в конце", + "错误": "Ошибка", + "键为分组名称,值为另一个 JSON 对象,键为分组名称,值为该分组的用户的特殊分组倍率,例如:{\"vip\": {\"default\": 0.5, \"test\": 1}},表示 vip 分组的用户在使用default分组的令牌时倍率为0.5,使用test分组时倍率为1": "Ключ - это имя группы, значение - другой JSON объект, ключ - имя группы, значение - специальный групповой коэффициент для пользователей этой группы, например: {\"vip\": {\"default\": 0.5, \"test\": 1}}, означает, что пользователи группы vip при использовании токенов группы default имеют коэффициент 0.5, при использовании группы test - коэффициент 1", + "键为原状态码,值为要复写的状态码,仅影响本地判断": "Ключ - исходный код состояния, значение - код состояния для перезаписи, влияет только на локальную проверку", + "键为端点类型,值为路径和方法对象": "Ключ - тип конечной точки, значение - объект пути и метода", + "键为请求中的模型名称,值为要替换的模型名称": "Ключ - имя модели в запросе, значение - имя модели для замены", + "键名": "Имя ключа", + "问题标题": "Заголовок проблемы", + "队列中": "В очереди", + "降低您账户的安全性": "Снижает безопасность вашего аккаунта", + "降级": "Понизить версию", + "限制周期": "Период ограничения", + "限制周期统一使用上方配置的“限制周期”值。": "Период ограничения равномерно использует значение 'Период ограничения', настроенное выше.", + "隐私政策": "Политика конфиденциальности", + "隐私政策已更新": "Политика конфиденциальности обновлена", + "隐私政策更新失败": "Не удалось обновить политику конфиденциальности", + "隐私设置": "Настройки конфиденциальности", + "隐藏操作项": "Скрыть элементы операций", + "隐藏调试": "Скрыть отладку", + "随机": "Случайный", + "随机模式": "Случайный режим", + "零一万物": "01.AI", + "需要安全验证": "Требуется проверка безопасности", + "需要添加的额度(支持负数)": "Квота для добавления (поддерживаются отрицательные значения)", + "需要登录访问": "Требуется вход для доступа", + "需要重新完整设置才能再次启用": "Требуется повторная полная настройка для повторного включения", + "非必要,不建议启用模型限制": "Необязательно, не рекомендуется включать ограничения моделей", + "非流": "Без потока", + "音频倍率(仅部分模型支持该计费)": "Аудиокоэффициент (только некоторые модели поддерживают эту тарификацию)", + "音频提示 {{input}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} = {{symbol}}{{total}}": "Аудиоввод {{input}} токенов / 1M токенов * {{symbol}}{{audioInputPrice}} + Аудиозавершение {{completion}} токенов / 1M токенов * {{symbol}}{{audioCompPrice}} = {{symbol}}{{total}}", + "音频提示价格:{{symbol}}{{price}} * {{audioRatio}} = {{symbol}}{{total}} / 1M tokens (音频倍率: {{audioRatio}})": "Цена аудиоввода: {{symbol}}{{price}} * {{audioRatio}} = {{symbol}}{{total}} / 1M токенов (аудиокоэффициент: {{audioRatio}})", + "音频补全价格:{{symbol}}{{price}} * {{audioRatio}} * {{audioCompRatio}} = {{symbol}}{{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})": "Цена аудиовывода: {{symbol}}{{price}} * {{audioRatio}} * {{audioCompRatio}} = {{symbol}}{{total}} / 1M токенов (коэффициент аудиовывода: {{audioCompRatio}})", + "音频补全倍率(仅部分模型支持该计费)": "Коэффициент аудиовывода (только некоторые модели поддерживают эту тарификацию)", + "音频输入相关的倍率设置,键为模型名称,值为倍率": "Настройки коэффициентов, связанные с аудиовводом, ключ - имя модели, значение - коэффициент", + "音频输出补全相关的倍率设置,键为模型名称,值为倍率": "Настройки коэффициентов, связанные с аудиовыводом и завершением, ключ - имя модели, значение - коэффициент", + "页脚": "Подвал", + "页面未找到,请检查您的浏览器地址是否正确": "Страница не найдена, пожалуйста, проверьте правильность адреса в браузере", + "顶栏管理": "Управление верхней панелью", + "项目": "Проект", + "项目内容": "Содержимое проекта", + "项目操作按钮组": "Группа кнопок операций проекта", + "预填组管理": "Управление группами предварительного заполнения", + "预览失败": "Ошибка предварительного просмотра", + "预览更新": "Обновление предварительного просмотра", + "预览请求体": "Предварительный просмотр тела запроса", + "预警阈值必须为正数": "Порог предупреждения должен быть положительным числом", + "频率限制的周期(分钟)": "Период ограничения частоты (минуты)", + "颜色": "Цвет", + "额度": "Квота", + "额度必须大于0": "Квота должна быть больше 0", + "额度提醒阈值": "Порог напоминания о квоте", + "额度查询接口返回令牌额度而非用户额度": "Интерфейс запроса квоты возвращает квоту токенов, а не квоту пользователя", + "额度设置": "Настройки квоты", + "额度预警阈值": "Порог предупреждения о квоте", + "首尾生视频": "Видео от начала до конца", + "首页": "Главная страница", + "首页内容": "Содержимое главной страницы", + "验证": "Проверить", + "验证 Passkey": "Проверить Passkey", + "验证失败,请重试": "Проверка не удалась, попробуйте еще раз", + "验证成功": "Проверка успешна", + "验证数据库连接状态": "Проверить состояние подключения к базе данных", + "验证码": "Код подтверждения", + "验证码发送成功,请检查邮箱!": "Код подтверждения успешно отправлен, проверьте электронную почту!", + "验证设置": "Настройки проверки", + "验证身份": "Подтвердить личность", + "验证配置错误": "Ошибка конфигурации проверки", + "高级设置": "Расширенные настройки", + "黑名单": "Черный список", + "默认": "По умолчанию", + "默认 API 版本": "Версия API по умолчанию", + "默认 Responses API 版本,为空则使用上方版本": "Версия Responses API по умолчанию, если пусто, используется версия выше", + "默认使用系统名称": "Использовать системное имя по умолчанию", + "默认区域": "Регион по умолчанию", + "默认区域,如: us-central1": "Регион по умолчанию, например: us-central1", + "默认折叠侧边栏": "Сворачивать боковую панель по умолчанию", + "默认测试模型": "Модель для тестирования по умолчанию", + "默认补全倍率": "Коэффициент вывода по умолчанию" + } +} diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index bbbd42243..bd43be977 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -1,38 +1,2069 @@ { - "首页": "首页", - "控制台": "控制台", - "定价": "定价", - "关于": "关于", - "登录": "登录", - "注册": "注册", - "退出": "退出", - "语言": "语言", - "展开侧边栏": "展开侧边栏", - "关闭侧边栏": "关闭侧边栏", - "注销成功!": "注销成功!", - "代理设置": "代理设置", - "更新Worker设置": "更新Worker设置", - "SSRF防护设置": "SSRF防护设置", - "配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全": "配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全", - "SSRF防护详细说明": "SSRF防护可防止恶意用户利用您的服务器访问内网资源。您可以配置受信任域名/IP的白名单,并限制允许的端口。适用于文件下载、Webhook回调和通知功能。", - "启用SSRF防护(推荐开启以保护服务器安全)": "启用SSRF防护(推荐开启以保护服务器安全)", - "SSRF防护开关详细说明": "总开关控制是否启用SSRF防护功能。关闭后将跳过所有SSRF检查,允许访问任意URL。⚠️ 仅在完全信任环境中关闭此功能。", - "允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)": "允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)", - "私有IP访问详细说明": "⚠️ 安全警告:启用此选项将允许访问内网资源(本地主机、私有网络)。仅在需要访问内部服务且了解安全风险的情况下启用。", - "域名白名单": "域名白名单", - "支持通配符格式,如:example.com, *.api.example.com": "支持通配符格式,如:example.com, *.api.example.com", - "域名白名单详细说明": "白名单中的域名将绕过所有SSRF检查,直接允许访问。支持精确域名(example.com)或通配符(*.api.example.com)匹配子域名。白名单为空时,所有域名都需要通过SSRF检查。", - "输入域名后回车,如:example.com": "输入域名后回车,如:example.com", - "IP白名单": "IP白名单", - "支持CIDR格式,如:8.8.8.8, 192.168.1.0/24": "支持CIDR格式,如:8.8.8.8, 192.168.1.0/24", - "IP白名单详细说明": "控制允许访问的IP地址。支持单个IP(8.8.8.8)或CIDR网段(192.168.1.0/24)。空白名单允许所有IP(但仍受私有IP设置限制),非空白名单仅允许列表中的IP访问。", - "输入IP地址后回车,如:8.8.8.8": "输入IP地址后回车,如:8.8.8.8", - "允许的端口": "允许的端口", - "支持单个端口和端口范围,如:80, 443, 8000-8999": "支持单个端口和端口范围,如:80, 443, 8000-8999", - "端口配置详细说明": "限制外部请求只能访问指定端口。支持单个端口(80, 443)或端口范围(8000-8999)。空列表允许所有端口。默认包含常用Web端口。", - "输入端口后回车,如:80 或 8000-8999": "输入端口后回车,如:80 或 8000-8999", - "更新SSRF防护设置": "更新SSRF防护设置", - "域名IP过滤详细说明": "⚠️此功能为实验性选项,域名可能解析到多个 IPv4/IPv6 地址,若开启,请确保 IP 过滤列表覆盖这些地址,否则可能导致访问失败。", - "Creem 介绍": "Creem 是一个简单的支付处理平台,支持固定金额产品销售,以及订阅销售。", - "Creem Setting Tips": "Creem 只支持预设的固定金额产品,这产品以及价格需要提前在Creem网站内创建配置,所以不支持自定义动态金额充值。在Creem端配置产品的名字以及价格,获取Product Id 后填到下面的产品,在new-api为该产品设置充值额度,以及展示价格。" -} + "translation": { + " + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_other": " + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " + 图片生成调用 {{symbol}}{{price}} / 1次 * {{ratioType}} {{ratio}}": " + 图片生成调用 {{symbol}}{{price}} / 1次 * {{ratioType}} {{ratio}}", + " + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}_other": " + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}", + " 个模型设置相同的值": " 个模型设置相同的值", + " 吗?": " 吗?", + " 秒": " 秒", + ",时间:": ",时间:", + ",点击更新": ",点击更新", + "(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)", + "(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}": "(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}", + "(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}": "(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}", + "(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}": "(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}", + "(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * {{symbol}}{{price}}": "(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * {{symbol}}{{price}}", + "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。": "[最多请求次数]和[最多请求完成次数]的最大值为2147483647。", + "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。": "[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。", + "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}": "{\n \"default\": [200, 100],\n \"vip\": [0, 1000]\n}", + "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}": "{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}", + "© {{currentYear}}": "© {{currentYear}}", + "| 基于": "| 基于", + "$/1M tokens": "$/1M tokens", + "0 - 最低": "0 - 最低", + "0.002-1之间的小数": "0.002-1之间的小数", + "0.1以上的小数": "0.1以上的小数", + "10 - 最高": "10 - 最高", + "2 - 低": "2 - 低", + "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"", + "360智脑": "360智脑", + "5 - 正常(默认)": "5 - 正常(默认)", + "8 - 高": "8 - 高", + "AGPL v3.0协议": "AGPL v3.0协议", + "AI 对话": "AI 对话", + "AI模型测试环境": "AI模型测试环境", + "AI模型配置": "AI模型配置", + "API Key 模式下不支持批量创建": "API Key 模式下不支持批量创建", + "API 地址和相关配置": "API 地址和相关配置", + "API 密钥": "API 密钥", + "API 文档": "API 文档", + "API 配置": "API 配置", + "API令牌管理": "API令牌管理", + "API使用记录": "API使用记录", + "API信息": "API信息", + "API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)": "API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)", + "API地址": "API地址", + "API渠道配置": "API渠道配置", + "API端点": "API端点", + "Authorization callback URL 填": "Authorization callback URL 填", + "Authorization Endpoint": "Authorization Endpoint", + "auto分组调用链路": "auto分组调用链路", + "Bark推送URL": "Bark推送URL", + "Bark推送URL必须以http://或https://开头": "Bark推送URL必须以http://或https://开头", + "Bark通知": "Bark通知", + "Changing batch type to:": "Changing batch type to:", + "Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比", + "Claude设置": "Claude设置", + "Claude请求头覆盖": "Claude请求头覆盖", + "Client ID": "Client ID", + "Client Secret": "Client Secret", + "common.changeLanguage": "common.changeLanguage", + "default为默认设置,可单独设置每个分类的安全等级": "default为默认设置,可单独设置每个分类的安全等级", + "default为默认设置,可单独设置每个模型的版本": "default为默认设置,可单独设置每个模型的版本", + "Dify渠道只适配chatflow和agent,并且agent不支持图片!": "Dify渠道只适配chatflow和agent,并且agent不支持图片!", + "false": "false", + "Gemini安全设置": "Gemini安全设置", + "Gemini思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Gemini思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比", + "Gemini思考适配设置": "Gemini思考适配设置", + "Gemini版本设置": "Gemini版本设置", + "Gemini设置": "Gemini设置", + "GitHub": "GitHub", + "GitHub Client ID": "GitHub Client ID", + "GitHub Client Secret": "GitHub Client Secret", + "GitHub ID": "GitHub ID", + "Gotify应用令牌": "Gotify应用令牌", + "Gotify服务器地址": "Gotify服务器地址", + "Gotify服务器地址必须以http://或https://开头": "Gotify服务器地址必须以http://或https://开头", + "Gotify通知": "Gotify通知", + "Homepage URL 填": "Homepage URL 填", + "ID": "ID", + "IP": "IP", + "IP白名单": "IP白名单", + "IP限制": "IP限制", + "IP黑名单": "IP黑名单", + "JSON": "JSON", + "JSON 模式支持手动输入或上传服务账号 JSON": "JSON 模式支持手动输入或上传服务账号 JSON", + "JSON格式密钥,请确保格式正确": "JSON格式密钥,请确保格式正确", + "JSON编辑": "JSON编辑", + "JSON解析错误:": "JSON解析错误:", + "Linux DO Client ID": "Linux DO Client ID", + "Linux DO Client Secret": "Linux DO Client Secret", + "LinuxDO": "LinuxDO", + "LinuxDO ID": "LinuxDO ID", + "Logo 图片地址": "Logo 图片地址", + "Midjourney 任务记录": "Midjourney 任务记录", + "MIT许可证": "MIT许可证", + "New API项目仓库地址:": "New API项目仓库地址:", + "OIDC": "OIDC", + "OIDC ID": "OIDC ID", + "Passkey": "Passkey", + "Passkey 已解绑": "Passkey 已解绑", + "Passkey 已重置": "Passkey 已重置", + "Passkey 是基于 WebAuthn 标准的无密码身份验证方法,支持指纹、面容、硬件密钥等认证方式": "Passkey 是基于 WebAuthn 标准的无密码身份验证方法,支持指纹、面容、硬件密钥等认证方式", + "Passkey 注册失败,请重试": "Passkey 注册失败,请重试", + "Passkey 注册成功": "Passkey 注册成功", + "Passkey 登录": "Passkey 登录", + "Ping间隔(秒)": "Ping间隔(秒)", + "price_xxx 的商品价格 ID,新建产品后可获得": "price_xxx 的商品价格 ID,新建产品后可获得", + "Reasoning Effort": "Reasoning Effort", + "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私", + "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用", + "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示", + "SMTP 发送者邮箱": "SMTP 发送者邮箱", + "SMTP 服务器地址": "SMTP 服务器地址", + "SMTP 端口": "SMTP 端口", + "SMTP 访问凭证": "SMTP 访问凭证", + "SMTP 账户": "SMTP 账户", + "SSRF防护开关详细说明": "总开关控制是否启用SSRF防护功能。关闭后将跳过所有SSRF检查,允许访问任意URL。⚠️ 仅在完全信任环境中关闭此功能。", + "SSRF防护设置": "SSRF防护设置", + "SSRF防护详细说明": "SSRF防护可防止恶意用户利用您的服务器访问内网资源。您可以配置受信任域名/IP的白名单,并限制允许的端口。适用于文件下载、Webhook回调和通知功能。", + "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用", + "Stripe 设置": "Stripe 设置", + "Telegram": "Telegram", + "Telegram Bot Token": "Telegram Bot Token", + "Telegram Bot 名称": "Telegram Bot 名称", + "Telegram ID": "Telegram ID", + "Token Endpoint": "Token Endpoint", + "true": "true", + "Turnstile Secret Key": "Turnstile Secret Key", + "Turnstile Site Key": "Turnstile Site Key", + "Unix时间戳": "Unix时间戳", + "Uptime Kuma地址": "Uptime Kuma地址", + "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)", + "URL链接": "URL链接", + "User Info Endpoint": "User Info Endpoint", + "Webhook 签名密钥": "Webhook 签名密钥", + "Webhook地址": "Webhook地址", + "Webhook地址必须以https://开头": "Webhook地址必须以https://开头", + "Webhook请求结构说明": "Webhook请求结构说明", + "Webhook通知": "Webhook通知", + "Web搜索价格:{{symbol}}{{price}} / 1K 次": "Web搜索价格:{{symbol}}{{price}} / 1K 次", + "WeChat Server 服务器地址": "WeChat Server 服务器地址", + "WeChat Server 访问凭证": "WeChat Server 访问凭证", + "Well-Known URL": "Well-Known URL", + "Well-Known URL 必须以 http:// 或 https:// 开头": "Well-Known URL 必须以 http:// 或 https:// 开头", + "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示": "whsec_xxx 的 Webhook 签名密钥,敏感信息不显示", + "Worker地址": "Worker地址", + "Worker密钥": "Worker密钥", + "一个月": "一个月", + "一天": "一天", + "一小时": "一小时", + "一次调用消耗多少刀,优先级大于模型倍率": "一次调用消耗多少刀,优先级大于模型倍率", + "一行一个,不区分大小写": "一行一个,不区分大小写", + "一行一个屏蔽词,不需要符号分割": "一行一个屏蔽词,不需要符号分割", + "一键填充到 FluentRead": "一键填充到 FluentRead", + "上一个表单块": "上一个表单块", + "上一步": "上一步", + "上次保存: ": "上次保存: ", + "上游倍率同步": "上游倍率同步", + "下一个表单块": "下一个表单块", + "下一步": "下一步", + "下午好": "下午好", + "不再提醒": "不再提醒", + "不同用户分组的价格信息": "不同用户分组的价格信息", + "不填则为模型列表第一个": "不填则为模型列表第一个", + "不建议使用": "不建议使用", + "不支持": "不支持", + "不是合法的 JSON 字符串": "不是合法的 JSON 字符串", + "不更改": "不更改", + "不限制": "不限制", + "与本地相同": "与本地相同", + "专属倍率": "专属倍率", + "两次输入的密码不一致": "两次输入的密码不一致", + "两次输入的密码不一致!": "两次输入的密码不一致!", + "两步验证": "两步验证", + "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。", + "两步验证启用成功!": "两步验证启用成功!", + "两步验证已禁用": "两步验证已禁用", + "两步验证设置": "两步验证设置", + "个": "个", + "个人中心": "个人中心", + "个人中心区域": "个人中心区域", + "个人信息设置": "个人信息设置", + "个人设置": "个人设置", + "个性化设置": "个性化设置", + "个性化设置左侧边栏的显示内容": "个性化设置左侧边栏的显示内容", + "个未配置模型": "个未配置模型", + "个模型": "个模型", + "中午好": "中午好", + "为一个 JSON 对象,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}": "为一个 JSON 对象,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}", + "为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]": "为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]", + "为一个 JSON 文本": "为一个 JSON 文本", + "为一个 JSON 文本,例如:": "为一个 JSON 文本,例如:", + "为一个 JSON 文本,键为分组名称,值为倍率": "为一个 JSON 文本,键为分组名称,值为倍率", + "为一个 JSON 文本,键为分组名称,值为分组描述": "为一个 JSON 文本,键为分组名称,值为分组描述", + "为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 \"gpt-4-gizmo-*\": 0.1,一次消耗0.1刀": "为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 \"gpt-4-gizmo-*\": 0.1,一次消耗0.1刀", + "为一个 JSON 文本,键为模型名称,值为倍率": "为一个 JSON 文本,键为模型名称,值为倍率", + "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-audio-preview\": 16}": "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-audio-preview\": 16}", + "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-realtime\": 2}": "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-realtime\": 2}", + "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-image-1\": 2}": "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-image-1\": 2}", + "为一个 JSON 文本,键为组名称,值为倍率": "为一个 JSON 文本,键为组名称,值为倍率", + "为了保护账户安全,请验证您的两步验证码。": "为了保护账户安全,请验证您的两步验证码。", + "为了保护账户安全,请验证您的身份。": "为了保护账户安全,请验证您的身份。", + "为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https": "为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https", + "主页链接填": "主页链接填", + "之前的所有日志": "之前的所有日志", + "二步验证已重置": "二步验证已重置", + "仅会覆盖你勾选的字段,未勾选的字段保持本地不变。": "仅会覆盖你勾选的字段,未勾选的字段保持本地不变。", + "仅供参考,以实际扣费为准": "仅供参考,以实际扣费为准", + "仅保存": "仅保存", + "仅修改展示粒度,统计精确到小时": "仅修改展示粒度,统计精确到小时", + "仅密钥": "仅密钥", + "仅对自定义模型有效": "仅对自定义模型有效", + "仅当自动禁用开启时有效,关闭后不会自动禁用该渠道": "仅当自动禁用开启时有效,关闭后不会自动禁用该渠道", + "仅支持": "仅支持", + "仅支持 JSON 文件": "仅支持 JSON 文件", + "仅支持 JSON 文件,支持多文件": "仅支持 JSON 文件,支持多文件", + "仅支持 OpenAI 接口格式": "仅支持 OpenAI 接口格式", + "仅显示矛盾倍率": "仅显示矛盾倍率", + "仅用于开发环境,生产环境应使用 HTTPS": "仅用于开发环境,生产环境应使用 HTTPS", + "仅重置配置": "仅重置配置", + "今日关闭": "今日关闭", + "从官方模型库同步": "从官方模型库同步", + "从认证器应用中获取验证码,或使用备用码": "从认证器应用中获取验证码,或使用备用码", + "从配置文件同步": "从配置文件同步", + "代理地址": "代理地址", + "代理设置": "代理设置", + "代码已复制到剪贴板": "代码已复制到剪贴板", + "令牌": "令牌", + "令牌分组": "令牌分组", + "令牌分组,默认为用户的分组": "令牌分组,默认为用户的分组", + "令牌创建成功,请在列表页面点击复制获取令牌!": "令牌创建成功,请在列表页面点击复制获取令牌!", + "令牌名称": "令牌名称", + "令牌已重置并已复制到剪贴板": "令牌已重置并已复制到剪贴板", + "令牌更新成功!": "令牌更新成功!", + "令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制": "令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制", + "令牌管理": "令牌管理", + "以下上游数据可能不可信:": "以下上游数据可能不可信:", + "以下文件解析失败,已忽略:{{list}}": "以下文件解析失败,已忽略:{{list}}", + "以及": "以及", + "仪表盘设置": "仪表盘设置", + "价格": "价格", + "价格:${{price}} * {{ratioType}}:{{ratio}}": "价格:${{price}} * {{ratioType}}:{{ratio}}", + "价格设置": "价格设置", + "价格设置方式": "价格设置方式", + "任务 ID": "任务 ID", + "任务ID": "任务ID", + "任务日志": "任务日志", + "任务状态": "任务状态", + "任务记录": "任务记录", + "企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选": "企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选", + "优先级": "优先级", + "优惠": "优惠", + "低于此额度时将发送邮件提醒用户": "低于此额度时将发送邮件提醒用户", + "余额": "余额", + "余额充值管理": "余额充值管理", + "你似乎并没有修改什么": "你似乎并没有修改什么", + "使用 GitHub 继续": "使用 GitHub 继续", + "使用 JSON 对象格式,格式为:{\"组名\": [最多请求次数, 最多请求完成次数]}": "使用 JSON 对象格式,格式为:{\"组名\": [最多请求次数, 最多请求完成次数]}", + "使用 LinuxDO 继续": "使用 LinuxDO 继续", + "使用 OIDC 继续": "使用 OIDC 继续", + "使用 Passkey 实现免密且更安全的登录体验": "使用 Passkey 实现免密且更安全的登录体验", + "使用 Passkey 登录": "使用 Passkey 登录", + "使用 Passkey 验证": "使用 Passkey 验证", + "使用 微信 继续": "使用 微信 继续", + "使用 用户名 注册": "使用 用户名 注册", + "使用 邮箱或用户名 登录": "使用 邮箱或用户名 登录", + "使用ID排序": "使用ID排序", + "使用日志": "使用日志", + "使用模式": "使用模式", + "使用统计": "使用统计", + "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:", + "使用认证器应用扫描二维码": "使用认证器应用扫描二维码", + "例如 €, £, Rp, ₩, ₹...": "例如 €, £, Rp, ₩, ₹...", + "例如 https://docs.newapi.pro": "例如 https://docs.newapi.pro", + "例如:": "例如:", + "例如: socks5://user:pass@host:port": "例如: socks5://user:pass@host:port", + "例如:0001": "例如:0001", + "例如:1000": "例如:1000", + "例如:2,就是最低充值2$": "例如:2,就是最低充值2$", + "例如:2000": "例如:2000", + "例如:7,就是7元/美金": "例如:7,就是7元/美金", + "例如:example.com": "例如:example.com", + "例如:https://yourdomain.com": "例如:https://yourdomain.com", + "例如:preview": "例如:preview", + "例如发卡网站的购买链接": "例如发卡网站的购买链接", + "供应商": "供应商", + "供应商介绍": "供应商介绍", + "供应商信息:": "供应商信息:", + "供应商创建成功!": "供应商创建成功!", + "供应商删除成功": "供应商删除成功", + "供应商名称": "供应商名称", + "供应商图标": "供应商图标", + "供应商更新成功!": "供应商更新成功!", + "侧边栏管理(全局控制)": "侧边栏管理(全局控制)", + "侧边栏设置保存成功": "侧边栏设置保存成功", + "保存": "保存", + "保存 GitHub OAuth 设置": "保存 GitHub OAuth 设置", + "保存 Linux DO OAuth 设置": "保存 Linux DO OAuth 设置", + "保存 OIDC 设置": "保存 OIDC 设置", + "保存 Passkey 设置": "保存 Passkey 设置", + "保存 SMTP 设置": "保存 SMTP 设置", + "保存 Telegram 登录设置": "保存 Telegram 登录设置", + "保存 Turnstile 设置": "保存 Turnstile 设置", + "保存 WeChat Server 设置": "保存 WeChat Server 设置", + "保存分组倍率设置": "保存分组倍率设置", + "保存备用码": "保存备用码", + "保存备用码以备不时之需": "保存备用码以备不时之需", + "保存失败": "保存失败", + "保存失败,请重试": "保存失败,请重试", + "保存失败:": "保存失败:", + "保存屏蔽词过滤设置": "保存屏蔽词过滤设置", + "保存成功": "保存成功", + "保存数据看板设置": "保存数据看板设置", + "保存日志设置": "保存日志设置", + "保存模型倍率设置": "保存模型倍率设置", + "保存模型速率限制": "保存模型速率限制", + "保存监控设置": "保存监控设置", + "保存绘图设置": "保存绘图设置", + "保存聊天设置": "保存聊天设置", + "保存设置": "保存设置", + "保存通用设置": "保存通用设置", + "保存邮箱域名白名单设置": "保存邮箱域名白名单设置", + "保存额度设置": "保存额度设置", + "修复数据库一致性": "修复数据库一致性", + "修改为": "修改为", + "修改子渠道优先级": "修改子渠道优先级", + "修改子渠道权重": "修改子渠道权重", + "修改密码": "修改密码", + "修改绑定": "修改绑定", + "倍率": "倍率", + "倍率信息": "倍率信息", + "倍率是为了方便换算不同价格的模型": "倍率是为了方便换算不同价格的模型", + "倍率模式": "倍率模式", + "倍率类型": "倍率类型", + "停止测试": "停止测试", + "允许 AccountFilter 参数": "允许 AccountFilter 参数", + "允许 HTTP 协议图片请求(适用于自部署代理)": "允许 HTTP 协议图片请求(适用于自部署代理)", + "允许 safety_identifier 透传": "允许 safety_identifier 透传", + "允许 service_tier 透传": "允许 service_tier 透传", + "允许 Turnstile 用户校验": "允许 Turnstile 用户校验", + "允许不安全的 Origin(HTTP)": "允许不安全的 Origin(HTTP)", + "允许回调(会泄露服务器 IP 地址)": "允许回调(会泄露服务器 IP 地址)", + "允许在 Stripe 支付中输入促销码": "允许在 Stripe 支付中输入促销码", + "允许新用户注册": "允许新用户注册", + "允许的 Origins": "允许的 Origins", + "允许的IP,一行一个,不填写则不限制": "允许的IP,一行一个,不填写则不限制", + "允许的端口": "允许的端口", + "允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)": "允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)", + "允许通过 GitHub 账户登录 & 注册": "允许通过 GitHub 账户登录 & 注册", + "允许通过 Linux DO 账户登录 & 注册": "允许通过 Linux DO 账户登录 & 注册", + "允许通过 OIDC 进行登录": "允许通过 OIDC 进行登录", + "允许通过 Passkey 登录 & 认证": "允许通过 Passkey 登录 & 认证", + "允许通过 Telegram 进行登录": "允许通过 Telegram 进行登录", + "允许通过密码进行注册": "允许通过密码进行注册", + "允许通过密码进行登录": "允许通过密码进行登录", + "允许通过微信登录 & 注册": "允许通过微信登录 & 注册", + "元": "元", + "充值": "充值", + "充值价格(x元/美金)": "充值价格(x元/美金)", + "充值价格显示": "充值价格显示", + "充值分组倍率": "充值分组倍率", + "充值分组倍率不是合法的 JSON 字符串": "充值分组倍率不是合法的 JSON 字符串", + "充值数量": "充值数量", + "充值数量,最低 ": "充值数量,最低 ", + "充值数量不能小于": "充值数量不能小于", + "充值方式设置": "充值方式设置", + "充值方式设置不是合法的 JSON 字符串": "充值方式设置不是合法的 JSON 字符串", + "充值确认": "充值确认", + "充值账单": "充值账单", + "充值金额折扣配置": "充值金额折扣配置", + "充值金额折扣配置不是合法的 JSON 对象": "充值金额折扣配置不是合法的 JSON 对象", + "充值链接": "充值链接", + "充值额度": "充值额度", + "兑换人ID": "兑换人ID", + "兑换成功!": "兑换成功!", + "兑换码充值": "兑换码充值", + "兑换码创建成功": "兑换码创建成功", + "兑换码创建成功,是否下载兑换码?": "兑换码创建成功,是否下载兑换码?", + "兑换码创建成功!": "兑换码创建成功!", + "兑换码将以文本文件的形式下载,文件名为兑换码的名称。": "兑换码将以文本文件的形式下载,文件名为兑换码的名称。", + "兑换码更新成功!": "兑换码更新成功!", + "兑换码生成管理": "兑换码生成管理", + "兑换码管理": "兑换码管理", + "兑换额度": "兑换额度", + "全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用": "全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用", + "全局设置": "全局设置", + "全选": "全选", + "全部": "全部", + "全部供应商": "全部供应商", + "全部分组": "全部分组", + "全部标签": "全部标签", + "全部模型": "全部模型", + "全部状态": "全部状态", + "全部端点": "全部端点", + "全部类型": "全部类型", + "公告": "公告", + "公告内容": "公告内容", + "公告已更新": "公告已更新", + "公告更新失败": "公告更新失败", + "公告类型": "公告类型", + "共": "共", + "共 {{count}} 个密钥_other": "共 {{count}} 个密钥", + "共 {{count}} 个模型": "共 {{count}} 个模型", + "共 {{total}} 项,当前显示 {{start}}-{{end}} 项": "共 {{total}} 项,当前显示 {{start}}-{{end}} 项", + "关": "关", + "关于": "关于", + "关于我们": "关于我们", + "关于系统的详细信息": "关于系统的详细信息", + "关于项目": "关于项目", + "关键字(id或者名称)": "关键字(id或者名称)", + "关闭": "关闭", + "关闭侧边栏": "关闭侧边栏", + "关闭公告": "关闭公告", + "关闭后,此模型将不会被“同步官方”自动覆盖或创建": "关闭后,此模型将不会被“同步官方”自动覆盖或创建", + "关闭弹窗,已停止批量测试": "关闭弹窗,已停止批量测试", + "其他": "其他", + "其他注册选项": "其他注册选项", + "其他登录选项": "其他登录选项", + "其他设置": "其他设置", + "内容": "内容", + "内容较大,已启用性能优化模式": "内容较大,已启用性能优化模式", + "内容较大,部分功能可能受限": "内容较大,部分功能可能受限", + "最低": "最低", + "最低充值美元数量": "最低充值美元数量", + "最后使用时间": "最后使用时间", + "最后请求": "最后请求", + "准备完成初始化": "准备完成初始化", + "分类名称": "分类名称", + "分组": "分组", + "分组与模型定价设置": "分组与模型定价设置", + "分组价格": "分组价格", + "分组倍率": "分组倍率", + "分组倍率设置": "分组倍率设置", + "分组倍率设置,可以在此处新增分组或修改现有分组的倍率,格式为 JSON 字符串,例如:{\"vip\": 0.5, \"test\": 1},表示 vip 分组的倍率为 0.5,test 分组的倍率为 1": "分组倍率设置,可以在此处新增分组或修改现有分组的倍率,格式为 JSON 字符串,例如:{\"vip\": 0.5, \"test\": 1},表示 vip 分组的倍率为 0.5,test 分组的倍率为 1", + "分组特殊倍率": "分组特殊倍率", + "分组设置": "分组设置", + "分组速率配置优先级高于全局速率限制。": "分组速率配置优先级高于全局速率限制。", + "分组速率限制": "分组速率限制", + "分钟": "分钟", + "切换为Assistant角色": "切换为Assistant角色", + "切换为System角色": "切换为System角色", + "切换为单密钥模式": "切换为单密钥模式", + "切换主题": "切换主题", + "划转到余额": "划转到余额", + "划转邀请额度": "划转邀请额度", + "划转金额最低为": "划转金额最低为", + "划转额度": "划转额度", + "列设置": "列设置", + "创建令牌默认选择auto分组,初始令牌也将设为auto(否则留空,为用户默认分组)": "创建令牌默认选择auto分组,初始令牌也将设为auto(否则留空,为用户默认分组)", + "创建失败": "创建失败", + "创建成功": "创建成功", + "创建新用户账户": "创建新用户账户", + "创建新的令牌": "创建新的令牌", + "创建新的兑换码": "创建新的兑换码", + "创建新的模型": "创建新的模型", + "创建新的渠道": "创建新的渠道", + "创建新的预填组": "创建新的预填组", + "创建时间": "创建时间", + "创建用户": "创建用户", + "初始化失败,请重试": "初始化失败,请重试", + "初始化系统": "初始化系统", + "删除": "删除", + "删除失败": "删除失败", + "删除密钥失败": "删除密钥失败", + "删除成功": "删除成功", + "删除所选": "删除所选", + "删除所选令牌": "删除所选令牌", + "删除所选通道": "删除所选通道", + "删除禁用密钥失败": "删除禁用密钥失败", + "删除禁用通道": "删除禁用通道", + "删除自动禁用密钥": "删除自动禁用密钥", + "删除账户": "删除账户", + "删除账户确认": "删除账户确认", + "刷新": "刷新", + "刷新失败": "刷新失败", + "前缀": "前缀", + "剩余备用码:": "剩余备用码:", + "剩余额度": "剩余额度", + "剩余额度/总额度": "剩余额度/总额度", + "剩余额度$": "剩余额度$", + "功能特性": "功能特性", + "加入预填组": "加入预填组", + "加载中...": "加载中...", + "加载供应商信息失败": "加载供应商信息失败", + "加载关于内容失败...": "加载关于内容失败...", + "加载分组失败": "加载分组失败", + "加载失败": "加载失败", + "加载模型信息失败": "加载模型信息失败", + "加载模型失败": "加载模型失败", + "加载用户协议内容失败...": "加载用户协议内容失败...", + "加载账单失败": "加载账单失败", + "加载隐私政策内容失败...": "加载隐私政策内容失败...", + "包含": "包含", + "包含来自未知或未标明供应商的AI模型,这些模型可能来自小型供应商或开源项目。": "包含来自未知或未标明供应商的AI模型,这些模型可能来自小型供应商或开源项目。", + "包括失败请求的次数,0代表不限制": "包括失败请求的次数,0代表不限制", + "匹配类型": "匹配类型", + "区域": "区域", + "历史消耗": "历史消耗", + "原价": "原价", + "原因:": "原因:", + "原密码": "原密码", + "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥", + "参与官方同步": "参与官方同步", + "参数": "参数", + "参数值": "参数值", + "参数覆盖": "参数覆盖", + "参照生视频": "参照生视频", + "友情链接": "友情链接", + "发布日期": "发布日期", + "发布时间": "发布时间", + "取消": "取消", + "取消全选": "取消全选", + "变换": "变换", + "变焦": "变焦", + "只包括请求成功的次数": "只包括请求成功的次数", + "只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求": "只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求", + "只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录": "只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录", + "可信": "可信", + "可在设置页面设置关于内容,支持 HTML & Markdown": "可在设置页面设置关于内容,支持 HTML & Markdown", + "可用令牌分组": "可用令牌分组", + "可用分组": "可用分组", + "可用模型": "可用模型", + "可用端点类型": "可用端点类型", + "可用邀请额度": "可用邀请额度", + "可视化": "可视化", + "可视化倍率设置": "可视化倍率设置", + "可视化编辑": "可视化编辑", + "可选,公告的补充说明": "可选,公告的补充说明", + "可选值": "可选值", + "同时重置消息": "同时重置消息", + "同步": "同步", + "同步向导": "同步向导", + "同步失败": "同步失败", + "同步成功": "同步成功", + "同步接口": "同步接口", + "名称": "名称", + "名称+密钥": "名称+密钥", + "名称不能为空": "名称不能为空", + "名称匹配类型": "名称匹配类型", + "后端请求失败": "后端请求失败", + "后缀": "后缀", + "否": "否", + "启动时间": "启动时间", + "启用": "启用", + "启用 Prompt 检查": "启用 Prompt 检查", + "启用2FA失败": "启用2FA失败", + "启用Claude思考适配(-thinking后缀)": "启用Claude思考适配(-thinking后缀)", + "启用Gemini思考后缀适配": "启用Gemini思考后缀适配", + "启用Ping间隔": "启用Ping间隔", + "启用SMTP SSL": "启用SMTP SSL", + "启用SSRF防护(推荐开启以保护服务器安全)": "启用SSRF防护(推荐开启以保护服务器安全)", + "启用全部": "启用全部", + "启用密钥失败": "启用密钥失败", + "启用屏蔽词过滤功能": "启用屏蔽词过滤功能", + "启用所有密钥失败": "启用所有密钥失败", + "启用数据看板(实验性)": "启用数据看板(实验性)", + "启用用户模型请求速率限制(可能会影响高并发性能)": "启用用户模型请求速率限制(可能会影响高并发性能)", + "启用绘图功能": "启用绘图功能", + "启用请求体透传功能": "启用请求体透传功能", + "启用请求透传": "启用请求透传", + "启用额度消费日志记录": "启用额度消费日志记录", + "启用验证": "启用验证", + "周": "周", + "和": "和", + "响应": "响应", + "响应时间": "响应时间", + "商品价格 ID": "商品价格 ID", + "回答内容": "回答内容", + "回调 URL 填": "回调 URL 填", + "回调地址": "回调地址", + "固定价格": "固定价格", + "固定价格(每次)": "固定价格(每次)", + "固定价格值": "固定价格值", + "图像生成": "图像生成", + "图标": "图标", + "图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ": "图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ", + "图混合": "图混合", + "图片生成调用:{{symbol}}{{price}} / 1次": "图片生成调用:{{symbol}}{{price}} / 1次", + "图片输入: {{imageRatio}}": "图片输入: {{imageRatio}}", + "图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})": "图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})", + "图片输入倍率(仅部分模型支持该计费)": "图片输入倍率(仅部分模型支持该计费)", + "图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费": "图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费", + "图生文": "图生文", + "图生视频": "图生视频", + "在Gotify服务器创建应用后获得的令牌,用于发送通知": "在Gotify服务器创建应用后获得的令牌,用于发送通知", + "在Gotify服务器的应用管理中创建新应用": "在Gotify服务器的应用管理中创建新应用", + "在找兑换码?": "在找兑换码?", + "在此输入 Logo 图片地址": "在此输入 Logo 图片地址", + "在此输入新的公告内容,支持 Markdown & HTML 代码": "在此输入新的公告内容,支持 Markdown & HTML 代码", + "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面": "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面", + "在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码": "在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码", + "在此输入用户协议内容,支持 Markdown & HTML 代码": "在此输入用户协议内容,支持 Markdown & HTML 代码", + "在此输入系统名称": "在此输入系统名称", + "在此输入隐私政策内容,支持 Markdown & HTML 代码": "在此输入隐私政策内容,支持 Markdown & HTML 代码", + "在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页", + "域名IP过滤详细说明": "⚠️此功能为实验性选项,域名可能解析到多个 IPv4/IPv6 地址,若开启,请确保 IP 过滤列表覆盖这些地址,否则可能导致访问失败。", + "域名白名单": "域名白名单", + "域名黑名单": "域名黑名单", + "基本信息": "基本信息", + "填入": "填入", + "填入所有模型": "填入所有模型", + "填入模板": "填入模板", + "填入相关模型": "填入相关模型", + "填写Gotify服务器的完整URL地址": "填写Gotify服务器的完整URL地址", + "填写带https的域名,逗号分隔": "填写带https的域名,逗号分隔", + "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议", + "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策", + "备份支持": "备份支持", + "备份状态": "备份状态", + "备注": "备注", + "备用恢复代码": "备用恢复代码", + "备用码已复制到剪贴板": "备用码已复制到剪贴板", + "备用码重新生成成功": "备用码重新生成成功", + "复制": "复制", + "复制代码": "复制代码", + "复制令牌": "复制令牌", + "复制全部": "复制全部", + "复制名称": "复制名称", + "复制失败": "复制失败", + "复制失败,请手动复制": "复制失败,请手动复制", + "复制已选": "复制已选", + "复制应用的令牌(Token)并填写到上方的应用令牌字段": "复制应用的令牌(Token)并填写到上方的应用令牌字段", + "复制成功": "复制成功", + "复制所有代码": "复制所有代码", + "复制所有模型": "复制所有模型", + "复制所选令牌": "复制所选令牌", + "复制所选兑换码到剪贴板": "复制所选兑换码到剪贴板", + "复制渠道的所有信息": "复制渠道的所有信息", + "外接设备": "外接设备", + "多密钥渠道操作项目组": "多密钥渠道操作项目组", + "多密钥管理": "多密钥管理", + "多种充值方式,安全便捷": "多种充值方式,安全便捷", + "天": "天", + "天前": "天前", + "失败": "失败", + "失败原因": "失败原因", + "失败时自动禁用通道": "失败时自动禁用通道", + "失败重试次数": "失败重试次数", + "奖励说明": "奖励说明", + "如:大带宽批量分析图片推荐": "如:大带宽批量分析图片推荐", + "如:香港线路": "如:香港线路", + "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。", + "如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面", + "始终使用浅色主题": "始终使用浅色主题", + "始终使用深色主题": "始终使用深色主题", + "字段透传控制": "字段透传控制", + "存在重复的键名:": "存在重复的键名:", + "安全提醒": "安全提醒", + "安全设置": "安全设置", + "安全验证": "安全验证", + "安全验证级别": "安全验证级别", + "安装指南": "安装指南", + "完成": "完成", + "完成初始化": "完成初始化", + "完成设置并启用两步验证": "完成设置并启用两步验证", + "完整的 Base URL,支持变量{model}": "完整的 Base URL,支持变量{model}", + "官方": "官方", + "官方文档": "官方文档", + "官方模型同步": "官方模型同步", + "定价模式": "定价模式", + "定时测试所有通道": "定时测试所有通道", + "定期更改密码可以提高账户安全性": "定期更改密码可以提高账户安全性", + "实付": "实付", + "实付金额": "实付金额", + "实付金额:": "实付金额:", + "实际模型": "实际模型", + "实际请求体": "实际请求体", + "密码": "密码", + "密码修改成功!": "密码修改成功!", + "密码已复制到剪贴板:": "密码已复制到剪贴板:", + "密码已重置并已复制到剪贴板:": "密码已重置并已复制到剪贴板:", + "密码管理": "密码管理", + "密码重置": "密码重置", + "密码重置完成": "密码重置完成", + "密码重置确认": "密码重置确认", + "密码长度至少为8个字符": "密码长度至少为8个字符", + "密钥": "密钥", + "密钥(编辑模式下,保存的密钥不会显示)": "密钥(编辑模式下,保存的密钥不会显示)", + "密钥去重": "密钥去重", + "密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性": "密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性", + "密钥已删除": "密钥已删除", + "密钥已启用": "密钥已启用", + "密钥已复制到剪贴板": "密钥已复制到剪贴板", + "密钥已禁用": "密钥已禁用", + "密钥文件 (.json)": "密钥文件 (.json)", + "密钥更新模式": "密钥更新模式", + "密钥格式": "密钥格式", + "密钥格式无效,请输入有效的 JSON 格式密钥": "密钥格式无效,请输入有效的 JSON 格式密钥", + "密钥聚合模式": "密钥聚合模式", + "密钥获取成功": "密钥获取成功", + "密钥输入方式": "密钥输入方式", + "密钥预览": "密钥预览", + "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写", + "对域名启用 IP 过滤(实验性)": "对域名启用 IP 过滤(实验性)", + "对外运营模式": "对外运营模式", + "导入": "导入", + "导入的配置将覆盖当前设置,是否继续?": "导入的配置将覆盖当前设置,是否继续?", + "导入配置": "导入配置", + "导入配置失败: ": "导入配置失败: ", + "导出": "导出", + "导出配置": "导出配置", + "导出配置失败: ": "导出配置失败: ", + "将 reasoning_content 转换为 标签拼接到内容中": "将 reasoning_content 转换为 标签拼接到内容中", + "将为选中的 ": "将为选中的 ", + "将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "将仅保留第一个密钥文件,其余文件将被移除,是否继续?", + "将删除": "将删除", + "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。", + "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?": "将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?", + "将清除选定时间之前的所有日志": "将清除选定时间之前的所有日志", + "小时": "小时", + "尚未使用": "尚未使用", + "局部重绘-提交": "局部重绘-提交", + "屏蔽词列表": "屏蔽词列表", + "屏蔽词过滤设置": "屏蔽词过滤设置", + "展开": "展开", + "展开更多": "展开更多", + "左侧边栏个人设置": "左侧边栏个人设置", + "已为 {{count}} 个模型设置{{type}}_other": "已为 {{count}} 个模型设置{{type}}", + "已为 ${count} 个渠道设置标签!": "已为 ${count} 个渠道设置标签!", + "已修复 ${success} 个通道,失败 ${fails} 个通道。": "已修复 ${success} 个通道,失败 ${fails} 个通道。", + "已停止批量测试": "已停止批量测试", + "已关闭后续提醒": "已关闭后续提醒", + "已切换为Assistant角色": "已切换为Assistant角色", + "已切换为System角色": "已切换为System角色", + "已切换至最优倍率视图,每个模型使用其最低倍率分组": "已切换至最优倍率视图,每个模型使用其最低倍率分组", + "已初始化": "已初始化", + "已删除 {{count}} 个令牌!": "已删除 {{count}} 个令牌!", + "已删除 {{count}} 条失效兑换码_other": "已删除 {{count}} 条失效兑换码", + "已删除 ${data} 个通道!": "已删除 ${data} 个通道!", + "已删除所有禁用渠道,共计 ${data} 个": "已删除所有禁用渠道,共计 ${data} 个", + "已删除消息及其回复": "已删除消息及其回复", + "已发送到 Fluent": "已发送到 Fluent", + "已取消 Passkey 注册": "已取消 Passkey 注册", + "已启用": "已启用", + "已启用 Passkey,无需密码即可登录": "已启用 Passkey,无需密码即可登录", + "已启用所有密钥": "已启用所有密钥", + "已备份": "已备份", + "已复制": "已复制", + "已复制 ${count} 个模型": "已复制 ${count} 个模型", + "已复制:": "已复制:", + "已复制:{{name}}": "已复制:{{name}}", + "已复制到剪切板": "已复制到剪切板", + "已复制到剪贴板": "已复制到剪贴板", + "已复制到剪贴板!": "已复制到剪贴板!", + "已复制模型名称": "已复制模型名称", + "已成功开始测试所有已启用通道,请刷新页面查看结果。": "已成功开始测试所有已启用通道,请刷新页面查看结果。", + "已提交": "已提交", + "已新增 {{count}} 个模型:{{list}}_other": "已新增 {{count}} 个模型:{{list}}", + "已更新完毕所有已启用通道余额!": "已更新完毕所有已启用通道余额!", + "已有保存的配置": "已有保存的配置", + "已有的模型": "已有的模型", + "已有账户?": "已有账户?", + "已注销": "已注销", + "已添加到白名单": "已添加到白名单", + "已清空测试结果": "已清空测试结果", + "已用/剩余": "已用/剩余", + "已用额度": "已用额度", + "已禁用": "已禁用", + "已禁用所有密钥": "已禁用所有密钥", + "已绑定": "已绑定", + "已绑定渠道": "已绑定渠道", + "已耗尽": "已耗尽", + "已过期": "已过期", + "已选择 {{count}} 个模型_other": "已选择 {{count}} 个模型", + "已选择 {{selected}} / {{total}}": "已选择 {{selected}} / {{total}}", + "已选择 ${count} 个渠道": "已选择 ${count} 个渠道", + "已重置为默认配置": "已重置为默认配置", + "常见问答": "常见问答", + "常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)": "常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)", + "平台": "平台", + "平均RPM": "平均RPM", + "平均TPM": "平均TPM", + "平移": "平移", + "应用同步": "应用同步", + "应用更改": "应用更改", + "应用覆盖": "应用覆盖", + "建立连接时发生错误": "建立连接时发生错误", + "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。", + "开": "开", + "开启之后会清除用户提示词中的": "开启之后会清除用户提示词中的", + "开启之后将上游地址替换为服务器地址": "开启之后将上游地址替换为服务器地址", + "开启后,仅\"消费\"和\"错误\"日志将记录您的客户端IP地址": "开启后,仅\"消费\"和\"错误\"日志将记录您的客户端IP地址", + "开启后,将定期发送ping数据保持连接活跃": "开启后,将定期发送ping数据保持连接活跃", + "开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启": "开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启", + "开启后不限制:必须设置模型倍率": "开启后不限制:必须设置模型倍率", + "开启后未登录用户无法访问模型广场": "开启后未登录用户无法访问模型广场", + "开启批量操作": "开启批量操作", + "开始同步": "开始同步", + "开始批量测试 ${count} 个模型,已清空上次结果...": "开始批量测试 ${count} 个模型,已清空上次结果...", + "开始时间": "开始时间", + "弱变换": "弱变换", + "强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)": "强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)", + "强制格式化": "强制格式化", + "强制要求": "强制要求", + "强变换": "强变换", + "当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道": "当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道", + "当前余额": "当前余额", + "当前值": "当前值", + "当前分组为 auto,会自动选择最优分组,当一个组不可用时自动降级到下一个组(熔断机制)": "当前分组为 auto,会自动选择最优分组,当一个组不可用时自动降级到下一个组(熔断机制)", + "当前时间": "当前时间", + "当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。": "当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。", + "当前查看的分组为:{{group}},倍率为:{{ratio}}": "当前查看的分组为:{{group}},倍率为:{{ratio}}", + "当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。": "当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。", + "当前版本": "当前版本", + "当前计费": "当前计费", + "当前设备不支持 Passkey": "当前设备不支持 Passkey", + "当前设置类型: ": "当前设置类型: ", + "当前跟随系统": "当前跟随系统", + "当剩余额度低于此数值时,系统将通过选择的方式发送通知": "当剩余额度低于此数值时,系统将通过选择的方式发送通知", + "当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用": "当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用", + "当运行通道全部测试时,超过此时间将自动禁用通道": "当运行通道全部测试时,超过此时间将自动禁用通道", + "待使用收益": "待使用收益", + "微信": "微信", + "微信公众号二维码图片链接": "微信公众号二维码图片链接", + "微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)", + "微信扫码登录": "微信扫码登录", + "微信账户绑定成功!": "微信账户绑定成功!", + "必须是有效的 JSON 字符串数组,例如:[\"g1\",\"g2\"]": "必须是有效的 JSON 字符串数组,例如:[\"g1\",\"g2\"]", + "忘记密码?": "忘记密码?", + "快速开始": "快速开始", + "思考中...": "思考中...", + "思考内容转换": "思考内容转换", + "思考过程": "思考过程", + "思考适配 BudgetTokens 百分比": "思考适配 BudgetTokens 百分比", + "思考预算占比": "思考预算占比", + "性能指标": "性能指标", + "总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}": "总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}", + "总密钥数": "总密钥数", + "总收益": "总收益", + "总计": "总计", + "总额度": "总额度", + "您可以个性化设置侧边栏的要显示功能": "您可以个性化设置侧边栏的要显示功能", + "您无权访问此页面,请联系管理员": "您无权访问此页面,请联系管理员", + "您正在使用 MySQL 数据库。MySQL 是一个可靠的关系型数据库管理系统,适合生产环境使用。": "您正在使用 MySQL 数据库。MySQL 是一个可靠的关系型数据库管理系统,适合生产环境使用。", + "您正在使用 PostgreSQL 数据库。PostgreSQL 是一个功能强大的开源关系型数据库系统,提供了出色的可靠性和数据完整性,适合生产环境使用。": "您正在使用 PostgreSQL 数据库。PostgreSQL 是一个功能强大的开源关系型数据库系统,提供了出色的可靠性和数据完整性,适合生产环境使用。", + "您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!": "您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!", + "您正在删除自己的帐户,将清空所有数据且不可恢复": "您正在删除自己的帐户,将清空所有数据且不可恢复", + "您的数据将安全地存储在本地计算机上。所有配置、用户信息和使用记录都会自动保存,关闭应用后不会丢失。": "您的数据将安全地存储在本地计算机上。所有配置、用户信息和使用记录都会自动保存,关闭应用后不会丢失。", + "您确定要取消密码登录功能吗?这可能会影响用户的登录方式。": "您确定要取消密码登录功能吗?这可能会影响用户的登录方式。", + "您需要先启用两步验证或 Passkey 才能执行此操作": "您需要先启用两步验证或 Passkey 才能执行此操作", + "您需要先启用两步验证或 Passkey 才能查看敏感信息。": "您需要先启用两步验证或 Passkey 才能查看敏感信息。", + "想起来了?": "想起来了?", + "成功": "成功", + "成功兑换额度:": "成功兑换额度:", + "成功时自动启用通道": "成功时自动启用通道", + "我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销", + "我已阅读并同意": "我已阅读并同意", + "或": "或", + "或其兼容new-api-worker格式的其他版本": "或其兼容new-api-worker格式的其他版本", + "或手动输入密钥:": "或手动输入密钥:", + "所有上游数据均可信": "所有上游数据均可信", + "所有密钥已复制到剪贴板": "所有密钥已复制到剪贴板", + "所有编辑均为覆盖操作,留空则不更改": "所有编辑均为覆盖操作,留空则不更改", + "手动禁用": "手动禁用", + "手动编辑": "手动编辑", + "手动输入": "手动输入", + "打开侧边栏": "打开侧边栏", + "执行中": "执行中", + "扫描二维码": "扫描二维码", + "批量创建": "批量创建", + "批量创建时会在名称后自动添加随机后缀": "批量创建时会在名称后自动添加随机后缀", + "批量创建模式下仅支持文件上传,不支持手动输入": "批量创建模式下仅支持文件上传,不支持手动输入", + "批量删除": "批量删除", + "批量删除令牌": "批量删除令牌", + "批量删除失败": "批量删除失败", + "批量删除模型": "批量删除模型", + "批量操作": "批量操作", + "批量测试${count}个模型": "批量测试${count}个模型", + "批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}": "批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}", + "批量测试已停止": "批量测试已停止", + "批量测试过程中发生错误: ": "批量测试过程中发生错误: ", + "批量设置": "批量设置", + "批量设置成功": "批量设置成功", + "批量设置标签": "批量设置标签", + "批量设置模型参数": "批量设置模型参数", + "折": "折", + "按K显示单位": "按K显示单位", + "按价格设置": "按价格设置", + "按倍率类型筛选": "按倍率类型筛选", + "按倍率设置": "按倍率设置", + "按次计费": "按次计费", + "按量计费": "按量计费", + "按顺序替换content中的变量占位符": "按顺序替换content中的变量占位符", + "换脸": "换脸", + "授权,需在遵守": "授权,需在遵守", + "授权失败": "授权失败", + "排队中": "排队中", + "接受未设置价格模型": "接受未设置价格模型", + "接口凭证": "接口凭证", + "控制台": "控制台", + "控制台区域": "控制台区域", + "控制顶栏模块显示状态,全局生效": "控制顶栏模块显示状态,全局生效", + "推荐:用户可以选择是否使用指纹等验证": "推荐:用户可以选择是否使用指纹等验证", + "推荐使用(用户可选)": "推荐使用(用户可选)", + "描述": "描述", + "提交": "提交", + "提交时间": "提交时间", + "提交结果": "提交结果", + "提升": "提升", + "提示": "提示", + "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}", + "提示:如需备份数据,只需复制上述目录即可": "提示:如需备份数据,只需复制上述目录即可", + "提示:链接中的{key}将被替换为API密钥,{address}将被替换为服务器地址": "提示:链接中的{key}将被替换为API密钥,{address}将被替换为服务器地址", + "提示价格:{{symbol}}{{price}} / 1M tokens": "提示价格:{{symbol}}{{price}} / 1M tokens", + "提示缓存倍率": "提示缓存倍率", + "搜索供应商": "搜索供应商", + "搜索关键字": "搜索关键字", + "搜索无结果": "搜索无结果", + "搜索条件": "搜索条件", + "搜索模型": "搜索模型", + "搜索模型...": "搜索模型...", + "搜索模型名称": "搜索模型名称", + "搜索模型失败": "搜索模型失败", + "搜索渠道名称或地址": "搜索渠道名称或地址", + "搜索聊天应用名称": "搜索聊天应用名称", + "操作": "操作", + "操作失败": "操作失败", + "操作失败,请重试": "操作失败,请重试", + "操作成功完成!": "操作成功完成!", + "操作暂时被禁用": "操作暂时被禁用", + "操练场": "操练场", + "操练场和聊天功能": "操练场和聊天功能", + "支付地址": "支付地址", + "支付宝": "支付宝", + "支付方式": "支付方式", + "支付设置": "支付设置", + "支付请求失败": "支付请求失败", + "支付金额": "支付金额", + "支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。": "支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。", + "支持CIDR格式,如:8.8.8.8, 192.168.1.0/24": "支持CIDR格式,如:8.8.8.8, 192.168.1.0/24", + "支持HTTP和HTTPS,填写Gotify服务器的完整URL地址": "支持HTTP和HTTPS,填写Gotify服务器的完整URL地址", + "支持HTTP和HTTPS,模板变量: {{title}} (通知标题), {{content}} (通知内容)": "支持HTTP和HTTPS,模板变量: {{title}} (通知标题), {{content}} (通知内容)", + "支持众多的大模型供应商": "支持众多的大模型供应商", + "支持单个端口和端口范围,如:80, 443, 8000-8999": "支持单个端口和端口范围,如:80, 443, 8000-8999", + "支持变量:": "支持变量:", + "支持备份": "支持备份", + "支持搜索用户的 ID、用户名、显示名称和邮箱地址": "支持搜索用户的 ID、用户名、显示名称和邮箱地址", + "支持的图像模型": "支持的图像模型", + "支持通配符格式,如:example.com, *.api.example.com": "支持通配符格式,如:example.com, *.api.example.com", + "收益": "收益", + "收益统计": "收益统计", + "收起": "收起", + "收起侧边栏": "收起侧边栏", + "收起内容": "收起内容", + "放大": "放大", + "放大编辑": "放大编辑", + "敏感信息不会发送到前端显示": "敏感信息不会发送到前端显示", + "数据存储位置:": "数据存储位置:", + "数据库信息": "数据库信息", + "数据库检查": "数据库检查", + "数据库类型": "数据库类型", + "数据库警告": "数据库警告", + "数据格式错误": "数据格式错误", + "数据看板": "数据看板", + "数据看板更新间隔": "数据看板更新间隔", + "数据看板设置": "数据看板设置", + "数据看板默认时间粒度": "数据看板默认时间粒度", + "数据管理和日志查看": "数据管理和日志查看", + "文件上传": "文件上传", + "文件搜索价格:{{symbol}}{{price}} / 1K 次": "文件搜索价格:{{symbol}}{{price}} / 1K 次", + "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", + "文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}": "文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}", + "文字输入": "文字输入", + "文字输出": "文字输出", + "文心一言": "文心一言", + "文档": "文档", + "文档地址": "文档地址", + "文生视频": "文生视频", + "新增供应商": "新增供应商", + "新密码": "新密码", + "新密码需要和原密码不一致!": "新密码需要和原密码不一致!", + "新建": "新建", + "新建数量": "新建数量", + "新建组": "新建组", + "新格式(支持条件判断与json自定义):": "新格式(支持条件判断与json自定义):", + "新格式模板": "新格式模板", + "新版本": "新版本", + "新用户使用邀请码奖励额度": "新用户使用邀请码奖励额度", + "新用户初始额度": "新用户初始额度", + "新的备用恢复代码": "新的备用恢复代码", + "新的备用码已生成": "新的备用码已生成", + "新获取的模型": "新获取的模型", + "新额度:": "新额度:", + "无": "无", + "无冲突项": "无冲突项", + "无效的重置链接,请重新发起密码重置请求": "无效的重置链接,请重新发起密码重置请求", + "无法发起 Passkey 注册": "无法发起 Passkey 注册", + "无法复制到剪贴板,请手动复制": "无法复制到剪贴板,请手动复制", + "无邀请人": "无邀请人", + "无限制": "无限制", + "无限额度": "无限额度", + "日志清理失败:": "日志清理失败:", + "日志类型": "日志类型", + "日志设置": "日志设置", + "日志详情": "日志详情", + "旧格式(直接覆盖):": "旧格式(直接覆盖):", + "旧格式模板": "旧格式模板", + "旧的备用码已失效,请保存新的备用码": "旧的备用码已失效,请保存新的备用码", + "早上好": "早上好", + "时间": "时间", + "时间粒度": "时间粒度", + "易支付商户ID": "易支付商户ID", + "易支付商户密钥": "易支付商户密钥", + "是": "是", + "是否为企业账户": "是否为企业账户", + "是否同时重置对话消息?选择\"是\"将清空所有对话记录并恢复默认示例;选择\"否\"将保留当前对话记录。": "是否同时重置对话消息?选择\"是\"将清空所有对话记录并恢复默认示例;选择\"否\"将保留当前对话记录。", + "是否将该订单标记为成功并为用户入账?": "是否将该订单标记为成功并为用户入账?", + "是否自动禁用": "是否自动禁用", + "是否要求指纹/面容等生物识别": "是否要求指纹/面容等生物识别", + "显示倍率": "显示倍率", + "显示最新20条": "显示最新20条", + "显示名称": "显示名称", + "显示完整内容": "显示完整内容", + "显示操作项": "显示操作项", + "显示更多": "显示更多", + "显示第": "显示第", + "显示设置": "显示设置", + "显示调试": "显示调试", + "晚上好": "晚上好", + "普通用户": "普通用户", + "智能体ID": "智能体ID", + "智能熔断": "智能熔断", + "智谱": "智谱", + "暂无API信息": "暂无API信息", + "暂无保存的配置": "暂无保存的配置", + "暂无充值记录": "暂无充值记录", + "暂无公告": "暂无公告", + "暂无匹配模型": "暂无匹配模型", + "暂无可用的支付方式,请联系管理员配置": "暂无可用的支付方式,请联系管理员配置", + "暂无响应数据": "暂无响应数据", + "暂无密钥数据": "暂无密钥数据", + "暂无差异化倍率显示": "暂无差异化倍率显示", + "暂无常见问答": "暂无常见问答", + "暂无成功模型": "暂无成功模型", + "暂无数据": "暂无数据", + "暂无数据,点击下方按钮添加键值对": "暂无数据,点击下方按钮添加键值对", + "暂无模型描述": "暂无模型描述", + "暂无监控数据": "暂无监控数据", + "暂无系统公告": "暂无系统公告", + "暂无缺失模型": "暂无缺失模型", + "暂无请求数据": "暂无请求数据", + "暂无项目": "暂无项目", + "暂无预填组": "暂无预填组", + "暴露倍率接口": "暴露倍率接口", + "更多": "更多", + "更多信息请参考": "更多信息请参考", + "更多参数请参考": "更多参数请参考", + "更好的价格,更好的稳定性,只需要将模型基址替换为:": "更好的价格,更好的稳定性,只需要将模型基址替换为:", + "更新": "更新", + "更新 Stripe 设置": "更新 Stripe 设置", + "更新SSRF防护设置": "更新SSRF防护设置", + "更新Worker设置": "更新Worker设置", + "更新令牌信息": "更新令牌信息", + "更新兑换码信息": "更新兑换码信息", + "更新失败": "更新失败", + "更新成功": "更新成功", + "更新所有已启用通道余额": "更新所有已启用通道余额", + "更新支付设置": "更新支付设置", + "更新时间": "更新时间", + "更新服务器地址": "更新服务器地址", + "更新模型信息": "更新模型信息", + "更新渠道信息": "更新渠道信息", + "更新预填组": "更新预填组", + "服务可用性": "服务可用性", + "服务器地址": "服务器地址", + "服务显示名称": "服务显示名称", + "未发现新增模型": "未发现新增模型", + "未发现重复密钥": "未发现重复密钥", + "未启动": "未启动", + "未启用": "未启用", + "未命名": "未命名", + "未备份": "未备份", + "未开始": "未开始", + "未找到匹配的模型": "未找到匹配的模型", + "未找到差异化倍率,无需同步": "未找到差异化倍率,无需同步", + "未提交": "未提交", + "未检测到 Fluent 容器": "未检测到 Fluent 容器", + "未检测到 FluentRead(流畅阅读),请确认扩展已启用": "未检测到 FluentRead(流畅阅读),请确认扩展已启用", + "未测试": "未测试", + "未登录或登录已过期,请重新登录": "未登录或登录已过期,请重新登录", + "未知": "未知", + "未知供应商": "未知供应商", + "未知模型": "未知模型", + "未知渠道": "未知渠道", + "未知状态": "未知状态", + "未知类型": "未知类型", + "未知身份": "未知身份", + "未绑定": "未绑定", + "未获取到授权码": "未获取到授权码", + "未设置": "未设置", + "未设置倍率模型": "未设置倍率模型", + "未配置模型": "未配置模型", + "未配置的模型列表": "未配置的模型列表", + "本地": "本地", + "本地数据存储": "本地数据存储", + "本设备:手机指纹/面容,外接:USB安全密钥": "本设备:手机指纹/面容,外接:USB安全密钥", + "本设备内置": "本设备内置", + "本项目根据": "本项目根据", + "权重": "权重", + "权限设置": "权限设置", + "条": "条", + "条 - 第": "条 - 第", + "条,共": "条,共", + "条日志已清理!": "条日志已清理!", + "查看": "查看", + "查看图片": "查看图片", + "查看密钥": "查看密钥", + "查看当前可用的所有模型": "查看当前可用的所有模型", + "查看所有可用的AI模型供应商,包括众多知名供应商的模型。": "查看所有可用的AI模型供应商,包括众多知名供应商的模型。", + "查看渠道密钥": "查看渠道密钥", + "查询": "查询", + "标签": "标签", + "标签不能为空!": "标签不能为空!", + "标签信息": "标签信息", + "标签名称": "标签名称", + "标签的基本配置": "标签的基本配置", + "标签组": "标签组", + "标签聚合": "标签聚合", + "标签聚合模式": "标签聚合模式", + "标识颜色": "标识颜色", + "根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含": "根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含", + "格式示例:": "格式示例:", + "检查更新": "检查更新", + "检测到 FluentRead(流畅阅读)": "检测到 FluentRead(流畅阅读)", + "检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。": "检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。", + "检测到该消息后有AI回复,是否删除后续回复并重新生成?": "检测到该消息后有AI回复,是否删除后续回复并重新生成?", + "检测必须等待绘图成功才能进行放大等操作": "检测必须等待绘图成功才能进行放大等操作", + "模型": "模型", + "模型: {{ratio}}": "模型: {{ratio}}", + "模型专用区域": "模型专用区域", + "模型价格": "模型价格", + "模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}": "模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}", + "模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}": "模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}", + "模型倍率": "模型倍率", + "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}": "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}", + "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次": "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次", + "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}": "模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}", + "模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}": "模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}", + "模型倍率值": "模型倍率值", + "模型倍率和补全倍率": "模型倍率和补全倍率", + "模型倍率和补全倍率同时设置": "模型倍率和补全倍率同时设置", + "模型倍率设置": "模型倍率设置", + "模型关键字": "模型关键字", + "模型列表已复制到剪贴板": "模型列表已复制到剪贴板", + "模型列表已更新": "模型列表已更新", + "模型创建成功!": "模型创建成功!", + "模型名称": "模型名称", + "模型名称已存在": "模型名称已存在", + "模型固定价格": "模型固定价格", + "模型图标": "模型图标", + "模型定价,需要登录访问": "模型定价,需要登录访问", + "模型广场": "模型广场", + "模型支持的接口端点信息": "模型支持的接口端点信息", + "模型数据分析": "模型数据分析", + "模型映射必须是合法的 JSON 格式!": "模型映射必须是合法的 JSON 格式!", + "模型更新成功!": "模型更新成功!", + "模型消耗分布": "模型消耗分布", + "模型消耗趋势": "模型消耗趋势", + "模型版本": "模型版本", + "模型的详细描述和基本特性": "模型的详细描述和基本特性", + "模型相关设置": "模型相关设置", + "模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:": "模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:", + "模型管理": "模型管理", + "模型组": "模型组", + "模型补全倍率(仅对自定义模型有效)": "模型补全倍率(仅对自定义模型有效)", + "模型请求速率限制": "模型请求速率限制", + "模型调用次数占比": "模型调用次数占比", + "模型调用次数排行": "模型调用次数排行", + "模型选择和映射设置": "模型选择和映射设置", + "模型配置": "模型配置", + "模型重定向": "模型重定向", + "模型限制列表": "模型限制列表", + "模板示例": "模板示例", + "模糊搜索模型名称": "模糊搜索模型名称", + "次": "次", + "欢迎使用,请完成以下设置以开始使用系统": "欢迎使用,请完成以下设置以开始使用系统", + "正在处理大内容...": "正在处理大内容...", + "正在提交": "正在提交", + "正在构造请求体预览...": "正在构造请求体预览...", + "正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)": "正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)", + "正在跳转...": "正在跳转...", + "此代理仅用于图片请求转发,Webhook通知发送等,AI API请求仍然由服务器直接发出,可在渠道设置中单独配置代理": "此代理仅用于图片请求转发,Webhook通知发送等,AI API请求仍然由服务器直接发出,可在渠道设置中单独配置代理", + "此修改将不可逆": "此修改将不可逆", + "此操作不可恢复,请仔细确认时间后再操作!": "此操作不可恢复,请仔细确认时间后再操作!", + "此操作不可撤销,将永久删除已自动禁用的密钥": "此操作不可撤销,将永久删除已自动禁用的密钥", + "此操作不可撤销,将永久删除该密钥": "此操作不可撤销,将永久删除该密钥", + "此操作不可逆,所有数据将被永久删除": "此操作不可逆,所有数据将被永久删除", + "此操作将启用用户账户": "此操作将启用用户账户", + "此操作将提升用户的权限级别": "此操作将提升用户的权限级别", + "此操作将禁用用户账户": "此操作将禁用用户账户", + "此操作将禁用该用户当前的两步验证配置,下次登录将不再强制输入验证码,直到用户重新启用。": "此操作将禁用该用户当前的两步验证配置,下次登录将不再强制输入验证码,直到用户重新启用。", + "此操作将解绑用户当前的 Passkey,下次登录需要重新注册。": "此操作将解绑用户当前的 Passkey,下次登录需要重新注册。", + "此操作将降低用户的权限级别": "此操作将降低用户的权限级别", + "此支付方式最低充值金额为": "此支付方式最低充值金额为", + "此设置用于系统内部计算,默认值500000是为了精确到6位小数点设计,不推荐修改。": "此设置用于系统内部计算,默认值500000是为了精确到6位小数点设计,不推荐修改。", + "此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除": "此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除", + "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改", + "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:": "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:", + "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改": "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改", + "此项可选,用于复写返回的状态码,仅影响本地判断,不修改返回到上游的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:": "此项可选,用于复写返回的状态码,仅影响本地判断,不修改返回到上游的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:", + "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数": "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数", + "此项可选,用于覆盖请求头参数": "此项可选,用于覆盖请求头参数", + "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/", + "每隔多少分钟测试一次所有通道": "每隔多少分钟测试一次所有通道", + "永不过期": "永不过期", + "永久删除您的两步验证设置": "永久删除您的两步验证设置", + "永久删除所有备用码(包括未使用的)": "永久删除所有备用码(包括未使用的)", + "没有可用令牌用于填充": "没有可用令牌用于填充", + "没有可用模型": "没有可用模型", + "没有找到匹配的模型": "没有找到匹配的模型", + "没有未设置的模型": "没有未设置的模型", + "没有模型可以复制": "没有模型可以复制", + "没有账户?": "没有账户?", + "注 册": "注 册", + "注册": "注册", + "注册 Passkey": "注册 Passkey", + "注意": "注意", + "注意:JSON中重复的键只会保留最后一个同名键的值": "注意:JSON中重复的键只会保留最后一个同名键的值", + "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用", + "注销": "注销", + "注销成功!": "注销成功!", + "流": "流", + "浅色": "浅色", + "浅色模式": "浅色模式", + "测试": "测试", + "测试中": "测试中", + "测试中...": "测试中...", + "测试单个渠道操作项目组": "测试单个渠道操作项目组", + "测试失败": "测试失败", + "测试所有渠道的最长响应时间": "测试所有渠道的最长响应时间", + "测试所有通道": "测试所有通道", + "测速": "测速", + "消息优先级": "消息优先级", + "消息优先级,范围0-10,默认为5": "消息优先级,范围0-10,默认为5", + "消息已删除": "消息已删除", + "消息已复制到剪贴板": "消息已复制到剪贴板", + "消息已更新": "消息已更新", + "消息已编辑": "消息已编辑", + "消耗分布": "消耗分布", + "消耗趋势": "消耗趋势", + "消耗额度": "消耗额度", + "消费": "消费", + "深色": "深色", + "深色模式": "深色模式", + "添加": "添加", + "添加API": "添加API", + "添加令牌": "添加令牌", + "添加兑换码": "添加兑换码", + "添加公告": "添加公告", + "添加分类": "添加分类", + "添加成功": "添加成功", + "添加模型": "添加模型", + "添加模型区域": "添加模型区域", + "添加渠道": "添加渠道", + "添加用户": "添加用户", + "添加聊天配置": "添加聊天配置", + "添加键值对": "添加键值对", + "添加问答": "添加问答", + "添加额度": "添加额度", + "清空重定向": "清空重定向", + "清除历史日志": "清除历史日志", + "清除失效兑换码": "清除失效兑换码", + "清除所有模型": "清除所有模型", + "渠道": "渠道", + "渠道 ID": "渠道 ID", + "渠道ID,名称,密钥,API地址": "渠道ID,名称,密钥,API地址", + "渠道优先级": "渠道优先级", + "渠道信息": "渠道信息", + "渠道创建成功!": "渠道创建成功!", + "渠道复制失败": "渠道复制失败", + "渠道复制失败: ": "渠道复制失败: ", + "渠道复制成功": "渠道复制成功", + "渠道密钥": "渠道密钥", + "渠道密钥信息": "渠道密钥信息", + "渠道密钥列表": "渠道密钥列表", + "渠道更新成功!": "渠道更新成功!", + "渠道权重": "渠道权重", + "渠道标签": "渠道标签", + "渠道模型信息不完整": "渠道模型信息不完整", + "渠道的基本配置信息": "渠道的基本配置信息", + "渠道的模型测试": "渠道的模型测试", + "渠道的高级配置选项": "渠道的高级配置选项", + "渠道管理": "渠道管理", + "渠道额外设置": "渠道额外设置", + "源地址": "源地址", + "演示站点": "演示站点", + "演示站点模式": "演示站点模式", + "点击上传文件或拖拽文件到这里": "点击上传文件或拖拽文件到这里", + "点击下方按钮通过 Telegram 完成绑定": "点击下方按钮通过 Telegram 完成绑定", + "点击复制模型名称": "点击复制模型名称", + "点击查看差异": "点击查看差异", + "点击此处": "点击此处", + "点击预览视频": "点击预览视频", + "点击验证按钮,使用您的生物特征或安全密钥": "点击验证按钮,使用您的生物特征或安全密钥", + "版权所有": "版权所有", + "状态": "状态", + "状态码复写": "状态码复写", + "状态筛选": "状态筛选", + "状态页面Slug": "状态页面Slug", + "生成令牌": "生成令牌", + "生成数量": "生成数量", + "生成数量必须大于0": "生成数量必须大于0", + "生成新的备用码": "生成新的备用码", + "生成歌词": "生成歌词", + "生成音乐": "生成音乐", + "用于API调用的身份验证令牌,请妥善保管": "用于API调用的身份验证令牌,请妥善保管", + "用于配置网络代理,支持 socks5 协议": "用于配置网络代理,支持 socks5 协议", + "用以支持基于 WebAuthn 的无密码登录注册": "用以支持基于 WebAuthn 的无密码登录注册", + "用以支持用户校验": "用以支持用户校验", + "用以支持系统的邮件发送": "用以支持系统的邮件发送", + "用以支持通过 GitHub 进行登录注册": "用以支持通过 GitHub 进行登录注册", + "用以支持通过 Linux DO 进行登录注册": "用以支持通过 Linux DO 进行登录注册", + "用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP": "用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP", + "用以支持通过 Telegram 进行登录注册": "用以支持通过 Telegram 进行登录注册", + "用以支持通过微信进行登录注册": "用以支持通过微信进行登录注册", + "用以防止恶意用户利用临时邮箱批量注册": "用以防止恶意用户利用临时邮箱批量注册", + "用户": "用户", + "用户个人功能": "用户个人功能", + "用户主页,展示系统信息": "用户主页,展示系统信息", + "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置", + "用户信息": "用户信息", + "用户信息更新成功!": "用户信息更新成功!", + "用户分组": "用户分组", + "用户分组和额度管理": "用户分组和额度管理", + "用户分组配置": "用户分组配置", + "用户协议": "用户协议", + "用户协议已更新": "用户协议已更新", + "用户协议更新失败": "用户协议更新失败", + "用户可选分组": "用户可选分组", + "用户名": "用户名", + "用户名或邮箱": "用户名或邮箱", + "用户名称": "用户名称", + "用户控制面板,管理账户": "用户控制面板,管理账户", + "用户新建令牌时可选的分组,格式为 JSON 字符串,例如:{\"vip\": \"VIP 用户\", \"test\": \"测试\"},表示用户可以选择 vip 分组和 test 分组": "用户新建令牌时可选的分组,格式为 JSON 字符串,例如:{\"vip\": \"VIP 用户\", \"test\": \"测试\"},表示用户可以选择 vip 分组和 test 分组", + "用户每周期最多请求完成次数": "用户每周期最多请求完成次数", + "用户每周期最多请求次数": "用户每周期最多请求次数", + "用户注册时看到的网站名称,比如'我的网站'": "用户注册时看到的网站名称,比如'我的网站'", + "用户的基本账户信息": "用户的基本账户信息", + "用户管理": "用户管理", + "用户组": "用户组", + "用户账户创建成功!": "用户账户创建成功!", + "用户账户管理": "用户账户管理", + "用时/首字": "用时/首字", + "留空则使用账号绑定的邮箱": "留空则使用账号绑定的邮箱", + "留空则使用默认端点;支持 {path, method}": "留空则使用默认端点;支持 {path, method}", + "留空则默认使用服务器地址,注意不能携带http://或者https://": "留空则默认使用服务器地址,注意不能携带http://或者https://", + "登 录": "登 录", + "登录": "登录", + "登录成功!": "登录成功!", + "登录过期,请重新登录!": "登录过期,请重新登录!", + "白名单": "白名单", + "的前提下使用。": "的前提下使用。", + "监控设置": "监控设置", + "目标用户:{{username}}": "目标用户:{{username}}", + "相关项目": "相关项目", + "相当于删除用户,此修改将不可逆": "相当于删除用户,此修改将不可逆", + "矛盾": "矛盾", + "知识库 ID": "知识库 ID", + "确定": "确定", + "确定?": "确定?", + "确定删除此组?": "确定删除此组?", + "确定导入": "确定导入", + "确定是否要修复数据库一致性?": "确定是否要修复数据库一致性?", + "确定是否要删除所选通道?": "确定是否要删除所选通道?", + "确定是否要删除此令牌?": "确定是否要删除此令牌?", + "确定是否要删除此兑换码?": "确定是否要删除此兑换码?", + "确定是否要删除此模型?": "确定是否要删除此模型?", + "确定是否要删除此渠道?": "确定是否要删除此渠道?", + "确定是否要删除禁用通道?": "确定是否要删除禁用通道?", + "确定是否要复制此渠道?": "确定是否要复制此渠道?", + "确定是否要注销此用户?": "确定是否要注销此用户?", + "确定清除所有失效兑换码?": "确定清除所有失效兑换码?", + "确定要修改所有子渠道优先级为 ": "确定要修改所有子渠道优先级为 ", + "确定要修改所有子渠道权重为 ": "确定要修改所有子渠道权重为 ", + "确定要删除供应商 \"{{name}}\" 吗?此操作不可撤销。": "确定要删除供应商 \"{{name}}\" 吗?此操作不可撤销。", + "确定要删除所有已自动禁用的密钥吗?": "确定要删除所有已自动禁用的密钥吗?", + "确定要删除所选的 {{count}} 个令牌吗?_other": "确定要删除所选的 {{count}} 个令牌吗?", + "确定要删除所选的 {{count}} 个模型吗?_other": "确定要删除所选的 {{count}} 个模型吗?", + "确定要删除此API信息吗?": "确定要删除此API信息吗?", + "确定要删除此公告吗?": "确定要删除此公告吗?", + "确定要删除此分类吗?": "确定要删除此分类吗?", + "确定要删除此密钥吗?": "确定要删除此密钥吗?", + "确定要删除此问答吗?": "确定要删除此问答吗?", + "确定要删除这条消息吗?": "确定要删除这条消息吗?", + "确定要启用所有密钥吗?": "确定要启用所有密钥吗?", + "确定要启用此用户吗?": "确定要启用此用户吗?", + "确定要提升此用户吗?": "确定要提升此用户吗?", + "确定要更新所有已启用通道余额吗?": "确定要更新所有已启用通道余额吗?", + "确定要测试所有通道吗?": "确定要测试所有通道吗?", + "确定要禁用所有的密钥吗?": "确定要禁用所有的密钥吗?", + "确定要禁用此用户吗?": "确定要禁用此用户吗?", + "确定要降级此用户吗?": "确定要降级此用户吗?", + "确定重置": "确定重置", + "确定重置模型倍率吗?": "确定重置模型倍率吗?", + "确认": "确认", + "确认冲突项修改": "确认冲突项修改", + "确认删除": "确认删除", + "确认取消密码登录": "确认取消密码登录", + "确认密码": "确认密码", + "确认导入配置": "确认导入配置", + "确认新密码": "确认新密码", + "确认清除历史日志": "确认清除历史日志", + "确认禁用": "确认禁用", + "确认补单": "确认补单", + "确认解绑": "确认解绑", + "确认解绑 Passkey": "确认解绑 Passkey", + "确认设置并完成初始化": "确认设置并完成初始化", + "确认重置 Passkey": "确认重置 Passkey", + "确认重置两步验证": "确认重置两步验证", + "确认重置密码": "确认重置密码", + "示例": "示例", + "示例:{\"default\": [200, 100], \"vip\": [0, 1000]}。": "示例:{\"default\": [200, 100], \"vip\": [0, 1000]}。", + "视频": "视频", + "禁用": "禁用", + "禁用 store 透传": "禁用 store 透传", + "禁用2FA失败": "禁用2FA失败", + "禁用两步验证": "禁用两步验证", + "禁用全部": "禁用全部", + "禁用原因": "禁用原因", + "禁用后的影响:": "禁用后的影响:", + "禁用密钥失败": "禁用密钥失败", + "禁用所有密钥失败": "禁用所有密钥失败", + "禁用时间": "禁用时间", + "私有IP访问详细说明": "⚠️ 安全警告:启用此选项将允许访问内网资源(本地主机、私有网络)。仅在需要访问内部服务且了解安全风险的情况下启用。", + "私有部署地址": "私有部署地址", + "秒": "秒", + "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目", + "窗口处理": "窗口处理", + "窗口等待": "窗口等待", + "站点额度展示类型及汇率": "站点额度展示类型及汇率", + "端口配置详细说明": "限制外部请求只能访问指定端口。支持单个端口(80, 443)或端口范围(8000-8999)。空列表允许所有端口。默认包含常用Web端口。", + "端点": "端点", + "端点映射": "端点映射", + "端点类型": "端点类型", + "端点组": "端点组", + "第三方账户绑定状态(只读)": "第三方账户绑定状态(只读)", + "等价金额:": "等价金额:", + "等待中": "等待中", + "等待获取邮箱信息...": "等待获取邮箱信息...", + "筛选": "筛选", + "管理": "管理", + "管理你的 LinuxDO OAuth App": "管理你的 LinuxDO OAuth App", + "管理员": "管理员", + "管理员区域": "管理员区域", + "管理员暂时未设置任何关于内容": "管理员暂时未设置任何关于内容", + "管理员未开启Stripe充值!": "管理员未开启Stripe充值!", + "管理员未开启在线充值!": "管理员未开启在线充值!", + "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。", + "管理员未设置用户可选分组": "管理员未设置用户可选分组", + "管理员设置了外部链接,点击下方按钮访问": "管理员设置了外部链接,点击下方按钮访问", + "管理员账号": "管理员账号", + "管理员账号已经初始化过,请继续设置其他参数": "管理员账号已经初始化过,请继续设置其他参数", + "管理模型、标签、端点等预填组": "管理模型、标签、端点等预填组", + "类型": "类型", + "精确": "精确", + "系统": "系统", + "系统令牌已复制到剪切板": "系统令牌已复制到剪切板", + "系统任务记录": "系统任务记录", + "系统信息": "系统信息", + "系统公告": "系统公告", + "系统公告管理,可以发布系统通知和重要消息(最多100个,前端显示最新20条)": "系统公告管理,可以发布系统通知和重要消息(最多100个,前端显示最新20条)", + "系统初始化": "系统初始化", + "系统初始化失败,请重试": "系统初始化失败,请重试", + "系统初始化成功,正在跳转...": "系统初始化成功,正在跳转...", + "系统参数配置": "系统参数配置", + "系统名称": "系统名称", + "系统名称已更新": "系统名称已更新", + "系统名称更新失败": "系统名称更新失败", + "系统提示覆盖": "系统提示覆盖", + "系统提示词": "系统提示词", + "系统提示词拼接": "系统提示词拼接", + "系统数据统计": "系统数据统计", + "系统文档和帮助信息": "系统文档和帮助信息", + "系统消息": "系统消息", + "系统管理功能": "系统管理功能", + "系统设置": "系统设置", + "系统访问令牌": "系统访问令牌", + "约": "约", + "索引": "索引", + "紧凑列表": "紧凑列表", + "线路描述": "线路描述", + "组列表": "组列表", + "组名": "组名", + "组织": "组织", + "组织,不填则为默认组织": "组织,不填则为默认组织", + "绑定": "绑定", + "绑定 Telegram": "绑定 Telegram", + "绑定信息": "绑定信息", + "绑定微信账户": "绑定微信账户", + "绑定成功!": "绑定成功!", + "绑定邮箱地址": "绑定邮箱地址", + "结束时间": "结束时间", + "结果图片": "结果图片", + "绘图": "绘图", + "绘图任务记录": "绘图任务记录", + "绘图日志": "绘图日志", + "绘图设置": "绘图设置", + "统计Tokens": "统计Tokens", + "统计次数": "统计次数", + "统计额度": "统计额度", + "继续": "继续", + "缓存 Tokens": "缓存 Tokens", + "缓存: {{cacheRatio}}": "缓存: {{cacheRatio}}", + "缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})", + "缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})", + "缓存倍率": "缓存倍率", + "缓存创建 Tokens": "缓存创建 Tokens", + "缓存创建: {{cacheCreationRatio}}": "缓存创建: {{cacheCreationRatio}}", + "缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})": "缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})", + "编辑": "编辑", + "编辑API": "编辑API", + "编辑供应商": "编辑供应商", + "编辑公告": "编辑公告", + "编辑公告内容": "编辑公告内容", + "编辑分类": "编辑分类", + "编辑成功": "编辑成功", + "编辑标签": "编辑标签", + "编辑模型": "编辑模型", + "编辑模式": "编辑模式", + "编辑用户": "编辑用户", + "编辑聊天配置": "编辑聊天配置", + "编辑问答": "编辑问答", + "缩词": "缩词", + "缺省 MaxTokens": "缺省 MaxTokens", + "网站地址": "网站地址", + "网站域名标识": "网站域名标识", + "网络错误": "网络错误", + "置信度": "置信度", + "聊天": "聊天", + "聊天会话管理": "聊天会话管理", + "聊天区域": "聊天区域", + "聊天应用名称": "聊天应用名称", + "聊天应用名称已存在,请使用其他名称": "聊天应用名称已存在,请使用其他名称", + "聊天设置": "聊天设置", + "聊天配置": "聊天配置", + "聊天链接配置错误,请联系管理员": "聊天链接配置错误,请联系管理员", + "联系我们": "联系我们", + "腾讯混元": "腾讯混元", + "自动分组auto,从第一个开始选择": "自动分组auto,从第一个开始选择", + "自动检测": "自动检测", + "自动模式": "自动模式", + "自动测试所有通道间隔时间": "自动测试所有通道间隔时间", + "自动禁用": "自动禁用", + "自动禁用关键词": "自动禁用关键词", + "自动选择": "自动选择", + "自定义充值数量选项": "自定义充值数量选项", + "自定义充值数量选项不是合法的 JSON 数组": "自定义充值数量选项不是合法的 JSON 数组", + "自定义变焦-提交": "自定义变焦-提交", + "自定义模型名称": "自定义模型名称", + "自定义货币": "自定义货币", + "自定义货币符号": "自定义货币符号", + "自用模式": "自用模式", + "自适应列表": "自适应列表", + "节省": "节省", + "花费": "花费", + "花费时间": "花费时间", + "若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置": "若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置", + "获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确": "获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确", + "获取 OIDC 配置成功!": "获取 OIDC 配置成功!", + "获取2FA状态失败": "获取2FA状态失败", + "获取初始化状态失败": "获取初始化状态失败", + "获取启用模型失败": "获取启用模型失败", + "获取启用模型失败:": "获取启用模型失败:", + "获取密钥": "获取密钥", + "获取密钥失败": "获取密钥失败", + "获取密钥状态失败": "获取密钥状态失败", + "获取未配置模型失败": "获取未配置模型失败", + "获取模型列表": "获取模型列表", + "获取模型列表失败": "获取模型列表失败", + "获取渠道失败:": "获取渠道失败:", + "获取组列表失败": "获取组列表失败", + "获取金额失败": "获取金额失败", + "获取验证码": "获取验证码", + "补全": "补全", + "补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})", + "补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens": "补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens", + "补全倍率": "补全倍率", + "补全倍率值": "补全倍率值", + "补单": "补单", + "补单失败": "补单失败", + "补单成功": "补单成功", + "表单引用错误,请刷新页面重试": "表单引用错误,请刷新页面重试", + "表格视图": "表格视图", + "覆盖模式:将完全替换现有的所有密钥": "覆盖模式:将完全替换现有的所有密钥", + "覆盖现有密钥": "覆盖现有密钥", + "角色": "角色", + "解析响应数据时发生错误": "解析响应数据时发生错误", + "解析密钥文件失败: {{msg}}": "解析密钥文件失败: {{msg}}", + "解绑 Passkey": "解绑 Passkey", + "解绑后将无法使用 Passkey 登录,确定要继续吗?": "解绑后将无法使用 Passkey 登录,确定要继续吗?", + "计费类型": "计费类型", + "计费过程": "计费过程", + "订单号": "订单号", + "讯飞星火": "讯飞星火", + "记录请求与错误日志IP": "记录请求与错误日志IP", + "设备类型偏好": "设备类型偏好", + "设置 Logo": "设置 Logo", + "设置2FA失败": "设置2FA失败", + "设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}": "设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{\"100\": 0.95, \"200\": 0.9, \"500\": 0.85}", + "设置两步验证": "设置两步验证", + "设置令牌可用额度和数量": "设置令牌可用额度和数量", + "设置令牌的基本信息": "设置令牌的基本信息", + "设置令牌的访问限制": "设置令牌的访问限制", + "设置保存失败": "设置保存失败", + "设置保存成功": "设置保存成功", + "设置兑换码的基本信息": "设置兑换码的基本信息", + "设置兑换码的额度和数量": "设置兑换码的额度和数量", + "设置公告": "设置公告", + "设置关于": "设置关于", + "设置已保存": "设置已保存", + "设置模型的基本信息": "设置模型的基本信息", + "设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱": "设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱", + "设置用户协议": "设置用户协议", + "设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]": "设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]", + "设置管理员登录信息": "设置管理员登录信息", + "设置类型": "设置类型", + "设置系统名称": "设置系统名称", + "设置过短会影响数据库性能": "设置过短会影响数据库性能", + "设置隐私政策": "设置隐私政策", + "设置页脚": "设置页脚", + "设置预填组的基本信息": "设置预填组的基本信息", + "设置首页内容": "设置首页内容", + "设置默认地区和特定模型的专用地区": "设置默认地区和特定模型的专用地区", + "设计与开发由": "设计与开发由", + "访问限制": "访问限制", + "该供应商提供多种AI模型,适用于不同的应用场景。": "该供应商提供多种AI模型,适用于不同的应用场景。", + "该分类下没有可用模型": "该分类下没有可用模型", + "该域名已存在于白名单中": "该域名已存在于白名单中", + "该数据可能不可信,请谨慎使用": "该数据可能不可信,请谨慎使用", + "该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置": "该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置", + "该模型存在固定价格与倍率计费方式冲突,请确认选择": "该模型存在固定价格与倍率计费方式冲突,请确认选择", + "详情": "详情", + "语音输入": "语音输入", + "语音输出": "语音输出", + "说明": "说明", + "说明:": "说明:", + "说明信息": "说明信息", + "请上传密钥文件": "请上传密钥文件", + "请上传密钥文件!": "请上传密钥文件!", + "请为渠道命名": "请为渠道命名", + "请先填写服务器地址": "请先填写服务器地址", + "请先输入密钥": "请先输入密钥", + "请先选择同步渠道": "请先选择同步渠道", + "请先选择模型!": "请先选择模型!", + "请先选择要删除的令牌!": "请先选择要删除的令牌!", + "请先选择要删除的通道!": "请先选择要删除的通道!", + "请先选择要设置标签的渠道!": "请先选择要设置标签的渠道!", + "请先选择需要批量设置的模型": "请先选择需要批量设置的模型", + "请先阅读并同意用户协议和隐私政策": "请先阅读并同意用户协议和隐私政策", + "请再次输入新密码": "请再次输入新密码", + "请前往个人设置 → 安全设置进行配置。": "请前往个人设置 → 安全设置进行配置。", + "请勿过度信任此功能,IP可能被伪造": "请勿过度信任此功能,IP可能被伪造", + "请在系统设置页面编辑分组倍率以添加新的分组:": "请在系统设置页面编辑分组倍率以添加新的分组:", + "请填写完整的管理员账号信息": "请填写完整的管理员账号信息", + "请填写密钥": "请填写密钥", + "请填写渠道名称和渠道密钥!": "请填写渠道名称和渠道密钥!", + "请填写部署地区": "请填写部署地区", + "请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。": "请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。", + "请检查渠道配置或刷新重试": "请检查渠道配置或刷新重试", + "请检查表单填写是否正确": "请检查表单填写是否正确", + "请检查输入": "请检查输入", + "请求发生错误": "请求发生错误", + "请求发生错误: ": "请求发生错误: ", + "请求后端接口失败:": "请求后端接口失败:", + "请求失败": "请求失败", + "请求头覆盖": "请求头覆盖", + "请求并计费模型": "请求并计费模型", + "请求路径": "请求路径", + "请求时长: ${time}s": "请求时长: ${time}s", + "请求次数": "请求次数", + "请求结束后多退少补": "请求结束后多退少补", + "请求预扣费额度": "请求预扣费额度", + "请点击我": "请点击我", + "请确认以下设置信息,点击\"初始化系统\"开始配置": "请确认以下设置信息,点击\"初始化系统\"开始配置", + "请确认您已了解禁用两步验证的后果": "请确认您已了解禁用两步验证的后果", + "请确认管理员密码": "请确认管理员密码", + "请稍后几秒重试,Turnstile 正在检查用户环境!": "请稍后几秒重试,Turnstile 正在检查用户环境!", + "请联系管理员在系统设置中配置API信息": "请联系管理员在系统设置中配置API信息", + "请联系管理员在系统设置中配置Uptime": "请联系管理员在系统设置中配置Uptime", + "请联系管理员在系统设置中配置公告信息": "请联系管理员在系统设置中配置公告信息", + "请联系管理员在系统设置中配置常见问答": "请联系管理员在系统设置中配置常见问答", + "请联系管理员配置聊天链接": "请联系管理员配置聊天链接", + "请至少选择一个令牌!": "请至少选择一个令牌!", + "请至少选择一个兑换码!": "请至少选择一个兑换码!", + "请至少选择一个模型": "请至少选择一个模型", + "请至少选择一个模型!": "请至少选择一个模型!", + "请至少选择一个渠道": "请至少选择一个渠道", + "请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com", + "请输入 JSON 格式的密钥内容,例如:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}": "请输入 JSON 格式的密钥内容,例如:\n{\n \"type\": \"service_account\",\n \"project_id\": \"your-project-id\",\n \"private_key_id\": \"...\",\n \"private_key\": \"...\",\n \"client_email\": \"...\",\n \"client_id\": \"...\",\n \"auth_uri\": \"...\",\n \"token_uri\": \"...\",\n \"auth_provider_x509_cert_url\": \"...\",\n \"client_x509_cert_url\": \"...\"\n}", + "请输入 OIDC 的 Well-Known URL": "请输入 OIDC 的 Well-Known URL", + "请输入6位验证码或8位备用码": "请输入6位验证码或8位备用码", + "请输入API地址": "请输入API地址", + "请输入API地址!": "请输入API地址!", + "请输入Bark推送URL": "请输入Bark推送URL", + "请输入Bark推送URL,例如: https://api.day.app/yourkey/{{title}}/{{content}}": "请输入Bark推送URL,例如: https://api.day.app/yourkey/{{title}}/{{content}}", + "请输入Gotify应用令牌": "请输入Gotify应用令牌", + "请输入Gotify服务器地址": "请输入Gotify服务器地址", + "请输入Gotify服务器地址,例如: https://gotify.example.com": "请输入Gotify服务器地址,例如: https://gotify.example.com", + "请输入Uptime Kuma地址": "请输入Uptime Kuma地址", + "请输入Uptime Kuma服务地址,如:https://status.example.com": "请输入Uptime Kuma服务地址,如:https://status.example.com", + "请输入URL链接": "请输入URL链接", + "请输入Webhook地址": "请输入Webhook地址", + "请输入Webhook地址,例如: https://example.com/webhook": "请输入Webhook地址,例如: https://example.com/webhook", + "请输入你的账户名以确认删除!": "请输入你的账户名以确认删除!", + "请输入供应商名称": "请输入供应商名称", + "请输入供应商名称,如:OpenAI": "请输入供应商名称,如:OpenAI", + "请输入供应商描述": "请输入供应商描述", + "请输入兑换码": "请输入兑换码", + "请输入兑换码!": "请输入兑换码!", + "请输入公告内容": "请输入公告内容", + "请输入公告内容(支持 Markdown/HTML)": "请输入公告内容(支持 Markdown/HTML)", + "请输入分类名称": "请输入分类名称", + "请输入分类名称,如:OpenAI、Claude等": "请输入分类名称,如:OpenAI、Claude等", + "请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com": "请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com", + "请输入原密码": "请输入原密码", + "请输入原密码!": "请输入原密码!", + "请输入名称": "请输入名称", + "请输入回答内容": "请输入回答内容", + "请输入回答内容(支持 Markdown/HTML)": "请输入回答内容(支持 Markdown/HTML)", + "请输入图标名称": "请输入图标名称", + "请输入填充值": "请输入填充值", + "请输入备注(仅管理员可见)": "请输入备注(仅管理员可见)", + "请输入完整的 JSON 格式密钥内容": "请输入完整的 JSON 格式密钥内容", + "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions", + "请输入完整的URL链接": "请输入完整的URL链接", + "请输入密码": "请输入密码", + "请输入密钥": "请输入密钥", + "请输入密钥,一行一个": "请输入密钥,一行一个", + "请输入密钥!": "请输入密钥!", + "请输入您的密码": "请输入您的密码", + "请输入您的用户名以确认删除": "请输入您的用户名以确认删除", + "请输入您的用户名或邮箱地址": "请输入您的用户名或邮箱地址", + "请输入您的邮箱地址": "请输入您的邮箱地址", + "请输入您的问题...": "请输入您的问题...", + "请输入数值": "请输入数值", + "请输入数字": "请输入数字", + "请输入新密码": "请输入新密码", + "请输入新密码!": "请输入新密码!", + "请输入新建数量": "请输入新建数量", + "请输入新标签,留空则解散标签": "请输入新标签,留空则解散标签", + "请输入新的剩余额度": "请输入新的剩余额度", + "请输入新的密码,最短 8 位": "请输入新的密码,最短 8 位", + "请输入新的显示名称": "请输入新的显示名称", + "请输入新的用户名": "请输入新的用户名", + "请输入显示名称": "请输入显示名称", + "请输入有效的数字": "请输入有效的数字", + "请输入标签名称": "请输入标签名称", + "请输入模型倍率": "请输入模型倍率", + "请输入模型倍率和补全倍率": "请输入模型倍率和补全倍率", + "请输入模型名称": "请输入模型名称", + "请输入模型名称,如:gpt-4": "请输入模型名称,如:gpt-4", + "请输入模型描述": "请输入模型描述", + "请输入消息内容...": "请输入消息内容...", + "请输入状态页面Slug": "请输入状态页面Slug", + "请输入状态页面的Slug,如:my-status": "请输入状态页面的Slug,如:my-status", + "请输入生成数量": "请输入生成数量", + "请输入用户名": "请输入用户名", + "请输入私有部署地址,格式为:https://fastgpt.run/api/openapi": "请输入私有部署地址,格式为:https://fastgpt.run/api/openapi", + "请输入管理员密码": "请输入管理员密码", + "请输入管理员用户名": "请输入管理员用户名", + "请输入线路描述": "请输入线路描述", + "请输入组名": "请输入组名", + "请输入组描述": "请输入组描述", + "请输入组织org-xxx": "请输入组织org-xxx", + "请输入聊天应用名称": "请输入聊天应用名称", + "请输入补全倍率": "请输入补全倍率", + "请输入要设置的标签名称": "请输入要设置的标签名称", + "请输入认证器验证码": "请输入认证器验证码", + "请输入认证器验证码或备用码": "请输入认证器验证码或备用码", + "请输入说明": "请输入说明", + "请输入邮箱!": "请输入邮箱!", + "请输入邮箱地址": "请输入邮箱地址", + "请输入邮箱验证码!": "请输入邮箱验证码!", + "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n \"default\": \"us-central1\",\n \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}", + "请输入问题标题": "请输入问题标题", + "请输入预警阈值": "请输入预警阈值", + "请输入预警额度": "请输入预警额度", + "请输入额度": "请输入额度", + "请输入验证码": "请输入验证码", + "请输入验证码或备用码": "请输入验证码或备用码", + "请输入默认 API 版本,例如:2025-04-01-preview": "请输入默认 API 版本,例如:2025-04-01-preview", + "请选择API地址": "请选择API地址", + "请选择你的复制方式": "请选择你的复制方式", + "请选择使用模式": "请选择使用模式", + "请选择分组": "请选择分组", + "请选择发布日期": "请选择发布日期", + "请选择可以使用该渠道的分组": "请选择可以使用该渠道的分组", + "请选择可以使用该渠道的分组,留空则不更改": "请选择可以使用该渠道的分组,留空则不更改", + "请选择同步语言": "请选择同步语言", + "请选择名称匹配类型": "请选择名称匹配类型", + "请选择多密钥使用策略": "请选择多密钥使用策略", + "请选择密钥更新模式": "请选择密钥更新模式", + "请选择密钥格式": "请选择密钥格式", + "请选择日志记录时间": "请选择日志记录时间", + "请选择模型": "请选择模型", + "请选择模型。": "请选择模型。", + "请选择消息优先级": "请选择消息优先级", + "请选择渠道类型": "请选择渠道类型", + "请选择组类型": "请选择组类型", + "请选择该令牌支持的模型,留空支持所有模型": "请选择该令牌支持的模型,留空支持所有模型", + "请选择该渠道所支持的模型": "请选择该渠道所支持的模型", + "请选择该渠道所支持的模型,留空则不更改": "请选择该渠道所支持的模型,留空则不更改", + "请选择过期时间": "请选择过期时间", + "请选择通知方式": "请选择通知方式", + "调用次数": "调用次数", + "调用次数分布": "调用次数分布", + "调用次数排行": "调用次数排行", + "调试信息": "调试信息", + "谨慎": "谨慎", + "警告": "警告", + "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔": "警告:启用保活后,如果已经写入保活数据后渠道出错,系统无法重试,如果必须开启,推荐设置尽可能大的Ping间隔", + "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!", + "豆包": "豆包", + "账单": "账单", + "账户充值": "账户充值", + "账户已删除!": "账户已删除!", + "账户已锁定": "账户已锁定", + "账户数据": "账户数据", + "账户管理": "账户管理", + "账户绑定": "账户绑定", + "账户绑定、安全设置和身份验证": "账户绑定、安全设置和身份验证", + "账户统计": "账户统计", + "货币单位": "货币单位", + "购买兑换码": "购买兑换码", + "资源消耗": "资源消耗", + "起始时间": "起始时间", + "超级管理员": "超级管理员", + "超级管理员未设置充值链接!": "超级管理员未设置充值链接!", + "跟随系统主题设置": "跟随系统主题设置", + "跳转": "跳转", + "轮询": "轮询", + "轮询模式": "轮询模式", + "轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能": "轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能", + "输入": "输入", + "输入 OIDC 的 Authorization Endpoint": "输入 OIDC 的 Authorization Endpoint", + "输入 OIDC 的 Client ID": "输入 OIDC 的 Client ID", + "输入 OIDC 的 Token Endpoint": "输入 OIDC 的 Token Endpoint", + "输入 OIDC 的 Userinfo Endpoint": "输入 OIDC 的 Userinfo Endpoint", + "输入IP地址后回车,如:8.8.8.8": "输入IP地址后回车,如:8.8.8.8", + "输入JSON对象": "输入JSON对象", + "输入价格": "输入价格", + "输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}": "输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}", + "输入你注册的 LinuxDO OAuth APP 的 ID": "输入你注册的 LinuxDO OAuth APP 的 ID", + "输入你的账户名{{username}}以确认删除": "输入你的账户名{{username}}以确认删除", + "输入域名后回车": "输入域名后回车", + "输入域名后回车,如:example.com": "输入域名后回车,如:example.com", + "输入密码,最短 8 位,最长 20 位": "输入密码,最短 8 位,最长 20 位", + "输入数字": "输入数字", + "输入标签或使用\",\"分隔多个标签": "输入标签或使用\",\"分隔多个标签", + "输入模型倍率": "输入模型倍率", + "输入每次价格": "输入每次价格", + "输入端口后回车,如:80 或 8000-8999": "输入端口后回车,如:80 或 8000-8999", + "输入系统提示词,用户的系统提示词将优先于此设置": "输入系统提示词,用户的系统提示词将优先于此设置", + "输入自定义模型名称": "输入自定义模型名称", + "输入补全价格": "输入补全价格", + "输入补全倍率": "输入补全倍率", + "输入要添加的邮箱域名": "输入要添加的邮箱域名", + "输入认证器应用显示的6位数字验证码": "输入认证器应用显示的6位数字验证码", + "输入邮箱地址": "输入邮箱地址", + "输入项目名称,按回车添加": "输入项目名称,按回车添加", + "输入验证码": "输入验证码", + "输入验证码完成设置": "输入验证码完成设置", + "输出": "输出", + "输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}": "输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}", + "输出价格": "输出价格", + "输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})": "输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})", + "边栏设置": "边栏设置", + "过期时间": "过期时间", + "过期时间不能早于当前时间!": "过期时间不能早于当前时间!", + "过期时间快捷设置": "过期时间快捷设置", + "过期时间格式错误!": "过期时间格式错误!", + "运营设置": "运营设置", + "返回登录": "返回登录", + "这是重复键中的最后一个,其值将被使用": "这是重复键中的最后一个,其值将被使用", + "进度": "进度", + "进行中": "进行中", + "进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用", + "连接保活设置": "连接保活设置", + "连接已断开": "连接已断开", + "追加到现有密钥": "追加到现有密钥", + "追加模式:将新密钥添加到现有密钥列表末尾": "追加模式:将新密钥添加到现有密钥列表末尾", + "追加模式:新密钥将添加到现有密钥列表的末尾": "追加模式:新密钥将添加到现有密钥列表的末尾", + "退出": "退出", + "适用于个人使用的场景,不需要设置模型价格": "适用于个人使用的场景,不需要设置模型价格", + "适用于为多个用户提供服务的场景": "适用于为多个用户提供服务的场景", + "适用于展示系统功能的场景,提供基础功能演示": "适用于展示系统功能的场景,提供基础功能演示", + "适配 -thinking、-thinking-预算数字 和 -nothinking 后缀": "适配 -thinking、-thinking-预算数字 和 -nothinking 后缀", + "选择充值额度": "选择充值额度", + "选择分组": "选择分组", + "选择同步来源": "选择同步来源", + "选择同步渠道": "选择同步渠道", + "选择同步语言": "选择同步语言", + "选择成功": "选择成功", + "选择支付方式": "选择支付方式", + "选择支持的认证设备类型": "选择支持的认证设备类型", + "选择方式": "选择方式", + "选择时间": "选择时间", + "选择模型": "选择模型", + "选择模型供应商": "选择模型供应商", + "选择模型后可一键填充当前选中令牌(或本页第一个令牌)。": "选择模型后可一键填充当前选中令牌(或本页第一个令牌)。", + "选择模型开始对话": "选择模型开始对话", + "选择端点类型": "选择端点类型", + "选择系统运行模式": "选择系统运行模式", + "选择组类型": "请选择组类型", + "选择要覆盖的冲突项": "选择要覆盖的冲突项", + "选择语言": "选择语言", + "选择过期时间(可选,留空为永久)": "选择过期时间(可选,留空为永久)", + "透传请求体": "透传请求体", + "通义千问": "通义千问", + "通用设置": "通用设置", + "通知": "通知", + "通知、价格和隐私相关设置": "通知、价格和隐私相关设置", + "通知内容": "通知内容", + "通知内容,支持 {{value}} 变量占位符": "通知内容,支持 {{value}} 变量占位符", + "通知方式": "通知方式", + "通知标题": "通知标题", + "通知类型 (quota_exceed: 额度预警)": "通知类型 (quota_exceed: 额度预警)", + "通知邮箱": "通知邮箱", + "通知配置": "通知配置", + "通过划转功能将奖励额度转入到您的账户余额中": "通过划转功能将奖励额度转入到您的账户余额中", + "通过密码注册时需要进行邮箱验证": "通过密码注册时需要进行邮箱验证", + "通道 ${name} 余额更新成功!": "通道 ${name} 余额更新成功!", + "通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。": "通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。", + "通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。", + "速率限制设置": "速率限制设置", + "邀请": "邀请", + "邀请人": "邀请人", + "邀请人数": "邀请人数", + "邀请信息": "邀请信息", + "邀请奖励": "邀请奖励", + "邀请好友注册,好友充值后您可获得相应奖励": "邀请好友注册,好友充值后您可获得相应奖励", + "邀请好友获得额外奖励": "邀请好友获得额外奖励", + "邀请新用户奖励额度": "邀请新用户奖励额度", + "邀请的好友越多,获得的奖励越多": "邀请的好友越多,获得的奖励越多", + "邀请码": "邀请码", + "邀请获得额度": "邀请获得额度", + "邀请链接": "邀请链接", + "邀请链接已复制到剪切板": "邀请链接已复制到剪切板", + "邮件通知": "邮件通知", + "邮箱": "邮箱", + "邮箱地址": "邮箱地址", + "邮箱域名格式不正确,请输入有效的域名,如 gmail.com": "邮箱域名格式不正确,请输入有效的域名,如 gmail.com", + "邮箱域名白名单格式不正确": "邮箱域名白名单格式不正确", + "邮箱账户绑定成功!": "邮箱账户绑定成功!", + "部分保存失败": "部分保存失败", + "部分保存失败,请重试": "部分保存失败,请重试", + "部分渠道测试失败:": "部分渠道测试失败:", + "部署地区": "部署地区", + "配置": "配置", + "配置 GitHub OAuth App": "配置 GitHub OAuth App", + "配置 Linux DO OAuth": "配置 Linux DO OAuth", + "配置 OIDC": "配置 OIDC", + "配置 Passkey": "配置 Passkey", + "配置 SMTP": "配置 SMTP", + "配置 Telegram 登录": "配置 Telegram 登录", + "配置 Turnstile": "配置 Turnstile", + "配置 WeChat Server": "配置 WeChat Server", + "配置和消息已全部重置": "配置和消息已全部重置", + "配置导入成功": "配置导入成功", + "配置已导出到下载文件夹": "配置已导出到下载文件夹", + "配置已重置,对话消息已保留": "配置已重置,对话消息已保留", + "配置文件同步": "配置文件同步", + "配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全": "配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全", + "配置登录注册": "配置登录注册", + "配置说明": "配置说明", + "配置邮箱域名白名单": "配置邮箱域名白名单", + "重复提交": "重复提交", + "重复的键名": "重复的键名", + "重复的键名,此值将被后面的同名键覆盖": "重复的键名,此值将被后面的同名键覆盖", + "重定向 URL 填": "重定向 URL 填", + "重新发送": "重新发送", + "重新生成": "重新生成", + "重新生成备用码": "重新生成备用码", + "重新生成备用码失败": "重新生成备用码失败", + "重新生成备用码将使现有的备用码失效,请确保您已保存了当前的备用码。": "重新生成备用码将使现有的备用码失效,请确保您已保存了当前的备用码。", + "重绘": "重绘", + "重置": "重置", + "重置 2FA": "重置 2FA", + "重置 Passkey": "重置 Passkey", + "重置为默认": "重置为默认", + "重置模型倍率": "重置模型倍率", + "重置选项": "重置选项", + "重置邮件发送成功,请检查邮箱!": "重置邮件发送成功,请检查邮箱!", + "重置配置": "重置配置", + "重试": "重试", + "钱包管理": "钱包管理", + "链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1": "链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1", + "错误": "错误", + "键为分组名称,值为另一个 JSON 对象,键为分组名称,值为该分组的用户的特殊分组倍率,例如:{\"vip\": {\"default\": 0.5, \"test\": 1}},表示 vip 分组的用户在使用default分组的令牌时倍率为0.5,使用test分组时倍率为1": "键为分组名称,值为另一个 JSON 对象,键为分组名称,值为该分组的用户的特殊分组倍率,例如:{\"vip\": {\"default\": 0.5, \"test\": 1}},表示 vip 分组的用户在使用default分组的令牌时倍率为0.5,使用test分组时倍率为1", + "键为原状态码,值为要复写的状态码,仅影响本地判断": "键为原状态码,值为要复写的状态码,仅影响本地判断", + "键为端点类型,值为路径和方法对象": "键为端点类型,值为路径和方法对象", + "键为请求中的模型名称,值为要替换的模型名称": "键为请求中的模型名称,值为要替换的模型名称", + "键名": "键名", + "问题标题": "问题标题", + "队列中": "队列中", + "降低您账户的安全性": "降低您账户的安全性", + "降级": "降级", + "限制周期": "限制周期", + "限制周期统一使用上方配置的“限制周期”值。": "限制周期统一使用上方配置的“限制周期”值。", + "隐私政策": "隐私政策", + "隐私政策已更新": "隐私政策已更新", + "隐私政策更新失败": "隐私政策更新失败", + "隐私设置": "隐私设置", + "隐藏操作项": "隐藏操作项", + "隐藏调试": "隐藏调试", + "随机": "随机", + "随机模式": "随机模式", + "零一万物": "零一万物", + "需要安全验证": "需要安全验证", + "需要添加的额度(支持负数)": "需要添加的额度(支持负数)", + "需要登录访问": "需要登录访问", + "需要重新完整设置才能再次启用": "需要重新完整设置才能再次启用", + "非必要,不建议启用模型限制": "非必要,不建议启用模型限制", + "非流": "非流", + "音频倍率(仅部分模型支持该计费)": "音频倍率(仅部分模型支持该计费)", + "音频提示 {{input}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} = {{symbol}}{{total}}": "音频提示 {{input}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} = {{symbol}}{{total}}", + "音频提示价格:{{symbol}}{{price}} * {{audioRatio}} = {{symbol}}{{total}} / 1M tokens (音频倍率: {{audioRatio}})": "音频提示价格:{{symbol}}{{price}} * {{audioRatio}} = {{symbol}}{{total}} / 1M tokens (音频倍率: {{audioRatio}})", + "音频补全价格:{{symbol}}{{price}} * {{audioRatio}} * {{audioCompRatio}} = {{symbol}}{{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})": "音频补全价格:{{symbol}}{{price}} * {{audioRatio}} * {{audioCompRatio}} = {{symbol}}{{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})", + "音频补全倍率(仅部分模型支持该计费)": "音频补全倍率(仅部分模型支持该计费)", + "音频输入相关的倍率设置,键为模型名称,值为倍率": "音频输入相关的倍率设置,键为模型名称,值为倍率", + "音频输出补全相关的倍率设置,键为模型名称,值为倍率": "音频输出补全相关的倍率设置,键为模型名称,值为倍率", + "页脚": "页脚", + "页面未找到,请检查您的浏览器地址是否正确": "页面未找到,请检查您的浏览器地址是否正确", + "顶栏管理": "顶栏管理", + "项目": "项目", + "项目内容": "项目内容", + "项目操作按钮组": "项目操作按钮组", + "预填组管理": "预填组管理", + "预览失败": "预览失败", + "预览更新": "预览更新", + "预览请求体": "预览请求体", + "预警阈值必须为正数": "预警阈值必须为正数", + "频率限制的周期(分钟)": "频率限制的周期(分钟)", + "颜色": "颜色", + "额度": "额度", + "额度必须大于0": "额度必须大于0", + "额度提醒阈值": "额度提醒阈值", + "额度查询接口返回令牌额度而非用户额度": "额度查询接口返回令牌额度而非用户额度", + "额度设置": "额度设置", + "额度预警阈值": "额度预警阈值", + "首尾生视频": "首尾生视频", + "首页": "首页", + "首页内容": "首页内容", + "验证": "验证", + "验证 Passkey": "验证 Passkey", + "验证失败,请重试": "验证失败,请重试", + "验证成功": "验证成功", + "验证数据库连接状态": "验证数据库连接状态", + "验证码": "验证码", + "验证码发送成功,请检查邮箱!": "验证码发送成功,请检查邮箱!", + "验证设置": "验证设置", + "验证身份": "验证身份", + "验证配置错误": "验证配置错误", + "高级设置": "高级设置", + "黑名单": "黑名单", + "默认": "默认", + "默认 API 版本": "默认 API 版本", + "默认 Responses API 版本,为空则使用上方版本": "默认 Responses API 版本,为空则使用上方版本", + "默认使用系统名称": "默认使用系统名称", + "默认区域": "默认区域", + "默认区域,如: us-central1": "默认区域,如: us-central1", + "默认折叠侧边栏": "默认折叠侧边栏", + "默认测试模型": "默认测试模型", + "默认补全倍率": "默认补全倍率", + "Creem 介绍": "Creem 是一个简单的支付处理平台,支持固定金额产品销售,以及订阅销售。", + "Creem Setting Tips": "Creem 只支持预设的固定金额产品,这产品以及价格需要提前在Creem网站内创建配置,所以不支持自定义动态金额充值。在Creem端配置产品的名字以及价格,获取Product Id 后填到下面的产品,在new-api为该产品设置充值额度,以及展示价格。" + } +} \ No newline at end of file diff --git a/web/src/pages/PrivacyPolicy/index.jsx b/web/src/pages/PrivacyPolicy/index.jsx new file mode 100644 index 000000000..026290b18 --- /dev/null +++ b/web/src/pages/PrivacyPolicy/index.jsx @@ -0,0 +1,37 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import DocumentRenderer from '../../components/common/DocumentRenderer'; + +const PrivacyPolicy = () => { + const { t } = useTranslation(); + + return ( + + ); +}; + +export default PrivacyPolicy; \ No newline at end of file diff --git a/web/src/pages/Setting/Chat/SettingsChats.jsx b/web/src/pages/Setting/Chat/SettingsChats.jsx index dec27beac..f7f309ac9 100644 --- a/web/src/pages/Setting/Chat/SettingsChats.jsx +++ b/web/src/pages/Setting/Chat/SettingsChats.jsx @@ -18,7 +18,26 @@ For commercial licensing, please contact support@quantumnous.com */ import React, { useEffect, useState, useRef } from 'react'; -import { Banner, Button, Form, Space, Spin } from '@douyinfe/semi-ui'; +import { + Banner, + Button, + Form, + Space, + Spin, + RadioGroup, + Radio, + Table, + Modal, + Input, + Divider, +} from '@douyinfe/semi-ui'; +import { + IconPlus, + IconEdit, + IconDelete, + IconSearch, + IconSaveStroked, +} from '@douyinfe/semi-icons'; import { compareObjects, API, @@ -37,6 +56,52 @@ export default function SettingsChats(props) { }); const refForm = useRef(); const [inputsRow, setInputsRow] = useState(inputs); + const [editMode, setEditMode] = useState('visual'); + const [chatConfigs, setChatConfigs] = useState([]); + const [modalVisible, setModalVisible] = useState(false); + const [editingConfig, setEditingConfig] = useState(null); + const [isEdit, setIsEdit] = useState(false); + const [searchText, setSearchText] = useState(''); + const modalFormRef = useRef(); + + const jsonToConfigs = (jsonString) => { + try { + const configs = JSON.parse(jsonString); + return Array.isArray(configs) + ? configs.map((config, index) => ({ + id: index, + name: Object.keys(config)[0] || '', + url: Object.values(config)[0] || '', + })) + : []; + } catch (error) { + console.error('JSON parse error:', error); + return []; + } + }; + + const configsToJson = (configs) => { + const jsonArray = configs.map((config) => ({ + [config.name]: config.url, + })); + return JSON.stringify(jsonArray, null, 2); + }; + + const syncJsonToConfigs = () => { + const configs = jsonToConfigs(inputs.Chats); + setChatConfigs(configs); + }; + + const syncConfigsToJson = (configs) => { + const jsonString = configsToJson(configs); + setInputs((prev) => ({ + ...prev, + Chats: jsonString, + })); + if (refForm.current && editMode === 'json') { + refForm.current.setValues({ Chats: jsonString }); + } + }; async function onSubmit() { try { @@ -103,16 +168,184 @@ export default function SettingsChats(props) { } setInputs(currentInputs); setInputsRow(structuredClone(currentInputs)); - refForm.current.setValues(currentInputs); + if (refForm.current) { + refForm.current.setValues(currentInputs); + } + + // 同步到可视化配置 + const configs = jsonToConfigs(currentInputs.Chats || '[]'); + setChatConfigs(configs); }, [props.options]); + useEffect(() => { + if (editMode === 'visual') { + syncJsonToConfigs(); + } + }, [inputs.Chats, editMode]); + + useEffect(() => { + if (refForm.current && editMode === 'json') { + refForm.current.setValues(inputs); + } + }, [editMode, inputs]); + + const handleAddConfig = () => { + setEditingConfig({ name: '', url: '' }); + setIsEdit(false); + setModalVisible(true); + setTimeout(() => { + if (modalFormRef.current) { + modalFormRef.current.setValues({ name: '', url: '' }); + } + }, 100); + }; + + const handleEditConfig = (config) => { + setEditingConfig({ ...config }); + setIsEdit(true); + setModalVisible(true); + setTimeout(() => { + if (modalFormRef.current) { + modalFormRef.current.setValues(config); + } + }, 100); + }; + + const handleDeleteConfig = (id) => { + const newConfigs = chatConfigs.filter((config) => config.id !== id); + setChatConfigs(newConfigs); + syncConfigsToJson(newConfigs); + showSuccess(t('删除成功')); + }; + + const handleModalOk = () => { + if (modalFormRef.current) { + modalFormRef.current + .validate() + .then((values) => { + // 检查名称是否重复 + const isDuplicate = chatConfigs.some( + (config) => + config.name === values.name && + (!isEdit || config.id !== editingConfig.id), + ); + + if (isDuplicate) { + showError(t('聊天应用名称已存在,请使用其他名称')); + return; + } + + if (isEdit) { + const newConfigs = chatConfigs.map((config) => + config.id === editingConfig.id + ? { ...editingConfig, name: values.name, url: values.url } + : config, + ); + setChatConfigs(newConfigs); + syncConfigsToJson(newConfigs); + } else { + const maxId = + chatConfigs.length > 0 + ? Math.max(...chatConfigs.map((c) => c.id)) + : -1; + const newConfig = { + id: maxId + 1, + name: values.name, + url: values.url, + }; + const newConfigs = [...chatConfigs, newConfig]; + setChatConfigs(newConfigs); + syncConfigsToJson(newConfigs); + } + setModalVisible(false); + setEditingConfig(null); + showSuccess(isEdit ? t('编辑成功') : t('添加成功')); + }) + .catch((error) => { + console.error('Modal form validation error:', error); + }); + } + }; + + const handleModalCancel = () => { + setModalVisible(false); + setEditingConfig(null); + }; + + const filteredConfigs = chatConfigs.filter( + (config) => + !searchText || + config.name.toLowerCase().includes(searchText.toLowerCase()), + ); + + const highlightKeywords = (text) => { + if (!text) return text; + + const parts = text.split(/(\{address\}|\{key\})/g); + return parts.map((part, index) => { + if (part === '{address}') { + return ( + + {part} + + ); + } else if (part === '{key}') { + return ( + + {part} + + ); + } + return part; + }); + }; + + const columns = [ + { + title: t('聊天应用名称'), + dataIndex: 'name', + key: 'name', + render: (text) => text || t('未命名'), + }, + { + title: t('URL链接'), + dataIndex: 'url', + key: 'url', + render: (text) => ( +
+ {highlightKeywords(text)} +
+ ), + }, + { + title: t('操作'), + key: 'action', + render: (_, record) => ( + + + + + ), + }, + ]; + return ( -
(refForm.current = formAPI)} - style={{ marginBottom: 15 }} - > + - { - return verifyJSON(value); - }, - message: t('不是合法的 JSON 字符串'), - }, - ]} - onChange={(value) => - setInputs({ - ...inputs, - Chats: value, - }) - } - /> + + + +
+ + {t('编辑模式')}: + + { + const newMode = e.target.value; + setEditMode(newMode); + + // 确保模式切换时数据正确同步 + setTimeout(() => { + if (newMode === 'json' && refForm.current) { + refForm.current.setValues(inputs); + } + }, 100); + }} + > + {t('可视化编辑')} + {t('JSON编辑')} + +
+ + {editMode === 'visual' ? ( +
+ + + + } + placeholder={t('搜索聊天应用名称')} + value={searchText} + onChange={(value) => setSearchText(value)} + style={{ width: 250 }} + showClear + /> + + +
+ t('共 {{total}} 项,当前显示 {{start}}-{{end}} 项', { + total, + start: range[0], + end: range[1], + }), + }} + /> + + ) : ( + (refForm.current = formAPI)} + > + { + return verifyJSON(value); + }, + message: t('不是合法的 JSON 字符串'), + }, + ]} + onChange={(value) => + setInputs({ + ...inputs, + Chats: value, + }) + } + /> + + )} - - - + + {editMode === 'json' && ( + + + + )} + + +
(modalFormRef.current = api)}> + + + + +
); } diff --git a/web/src/pages/Setting/Model/SettingClaudeModel.jsx b/web/src/pages/Setting/Model/SettingClaudeModel.jsx index 04d7956aa..688fc2d3e 100644 --- a/web/src/pages/Setting/Model/SettingClaudeModel.jsx +++ b/web/src/pages/Setting/Model/SettingClaudeModel.jsx @@ -202,9 +202,8 @@ export default function SettingClaudeModel(props) { label={t('思考适配 BudgetTokens 百分比')} field={'claude.thinking_adapter_budget_tokens_percentage'} initValue={''} - extraText={t('0.1-1之间的小数')} + extraText={t('0.1以上的小数')} min={0.1} - max={1} onChange={(value) => setInputs({ ...inputs, diff --git a/web/src/pages/Setting/Model/SettingGlobalModel.jsx b/web/src/pages/Setting/Model/SettingGlobalModel.jsx index 5b82fac04..a23125f90 100644 --- a/web/src/pages/Setting/Model/SettingGlobalModel.jsx +++ b/web/src/pages/Setting/Model/SettingGlobalModel.jsx @@ -105,7 +105,7 @@ export default function SettingGlobalModel(props) { }) } extraText={ - '开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启' + t('开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启') } /> @@ -116,7 +116,7 @@ export default function SettingGlobalModel(props) {
@@ -131,7 +131,7 @@ export default function SettingGlobalModel(props) { 'general_setting.ping_interval_enabled': value, }) } - extraText={'开启后,将定期发送ping数据保持连接活跃'} + extraText={t('开启后,将定期发送ping数据保持连接活跃')} /> diff --git a/web/src/pages/Setting/Operation/SettingsCreditLimit.jsx b/web/src/pages/Setting/Operation/SettingsCreditLimit.jsx index 131ade445..a5208a502 100644 --- a/web/src/pages/Setting/Operation/SettingsCreditLimit.jsx +++ b/web/src/pages/Setting/Operation/SettingsCreditLimit.jsx @@ -36,6 +36,7 @@ export default function SettingsCreditLimit(props) { PreConsumedQuota: '', QuotaForInviter: '', QuotaForInvitee: '', + 'quota_setting.enable_free_model_pre_consume': true, }); const refForm = useRef(); const [inputsRow, setInputsRow] = useState(inputs); @@ -166,6 +167,21 @@ export default function SettingsCreditLimit(props) { /> + + + + setInputs({ + ...inputs, + 'quota_setting.enable_free_model_pre_consume': value, + }) + } + /> + + - setShowQuotaWarning(true)} - /> - - )} - - - + {/* 单位美元额度已合入汇率组合控件(TOKENS 模式下编辑),不再单独展示 */} - - - + + + + + + + + + + - - +

+ {t('当前时间')}: + + {currentTime} + +

+

+ {t('选择时间')}: + + {targetTime} + + {daysDiff > 0 && ( + + {' '} + ({t('约')} {daysDiff} {t('天前')}) + + )} +

+
+ + ⚠️ {t('注意')}: + + {t('将删除')} + + {targetTime} + + {daysDiff > 0 && ( + + {' '} + ({t('约')} {daysDiff} {t('天前')}) + + )} + {t('之前的所有日志')} +
+

+ + {t('此操作不可恢复,请仔细确认时间后再操作!')} + +

+ + ), + okText: t('确认删除'), + cancelText: t('取消'), + okType: 'danger', + onOk: async () => { + try { + setLoadingCleanHistoryLog(true); + const res = await API.delete( + `/api/log/?target_timestamp=${Date.parse(inputs.historyTimestamp) / 1000}`, + ); + const { success, message, data } = res.data; + if (success) { + showSuccess(`${data} ${t('条日志已清理!')}`); + return; + } else { + throw new Error(t('日志清理失败:') + message); + } + } catch (error) { + showError(error.message); + } finally { + setLoadingCleanHistoryLog(false); + } + }, + }); } useEffect(() => { @@ -138,7 +219,7 @@ export default function SettingsLog(props) {
- diff --git a/web/src/pages/Setting/Operation/SettingsMonitoring.jsx b/web/src/pages/Setting/Operation/SettingsMonitoring.jsx index d64f19b63..b93a5ff09 100644 --- a/web/src/pages/Setting/Operation/SettingsMonitoring.jsx +++ b/web/src/pages/Setting/Operation/SettingsMonitoring.jsx @@ -128,7 +128,8 @@ export default function SettingsMonitoring(props) { onChange={(value) => setInputs({ ...inputs, - 'monitor_setting.auto_test_channel_minutes': parseInt(value), + 'monitor_setting.auto_test_channel_minutes': + parseInt(value), }) } /> diff --git a/web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx b/web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx index d681b6a27..a4f1029a1 100644 --- a/web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx +++ b/web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx @@ -118,14 +118,20 @@ export default function SettingsPaymentGateway(props) { } } - if (originInputs['AmountOptions'] !== inputs.AmountOptions && inputs.AmountOptions.trim() !== '') { + if ( + originInputs['AmountOptions'] !== inputs.AmountOptions && + inputs.AmountOptions.trim() !== '' + ) { if (!verifyJSON(inputs.AmountOptions)) { showError(t('自定义充值数量选项不是合法的 JSON 数组')); return; } } - if (originInputs['AmountDiscount'] !== inputs.AmountDiscount && inputs.AmountDiscount.trim() !== '') { + if ( + originInputs['AmountDiscount'] !== inputs.AmountDiscount && + inputs.AmountDiscount.trim() !== '' + ) { if (!verifyJSON(inputs.AmountDiscount)) { showError(t('充值金额折扣配置不是合法的 JSON 对象')); return; @@ -163,10 +169,16 @@ export default function SettingsPaymentGateway(props) { options.push({ key: 'PayMethods', value: inputs.PayMethods }); } if (originInputs['AmountOptions'] !== inputs.AmountOptions) { - options.push({ key: 'payment_setting.amount_options', value: inputs.AmountOptions }); + options.push({ + key: 'payment_setting.amount_options', + value: inputs.AmountOptions, + }); } if (originInputs['AmountDiscount'] !== inputs.AmountDiscount) { - options.push({ key: 'payment_setting.amount_discount', value: inputs.AmountDiscount }); + options.push({ + key: 'payment_setting.amount_discount', + value: inputs.AmountDiscount, + }); } // 发送请求 @@ -273,7 +285,7 @@ export default function SettingsPaymentGateway(props) { placeholder={t('为一个 JSON 文本')} autosize /> - + - + - + diff --git a/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx b/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx index 2f4ea210e..e4ddea110 100644 --- a/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx +++ b/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx @@ -45,6 +45,7 @@ export default function SettingsPaymentGateway(props) { StripePriceId: '', StripeUnitPrice: 8.0, StripeMinTopUp: 1, + StripePromotionCodesEnabled: false, }); const [originInputs, setOriginInputs] = useState({}); const formApiRef = useRef(null); @@ -63,6 +64,10 @@ export default function SettingsPaymentGateway(props) { props.options.StripeMinTopUp !== undefined ? parseFloat(props.options.StripeMinTopUp) : 1, + StripePromotionCodesEnabled: + props.options.StripePromotionCodesEnabled !== undefined + ? props.options.StripePromotionCodesEnabled + : false, }; setInputs(currentInputs); setOriginInputs({ ...currentInputs }); @@ -114,6 +119,16 @@ export default function SettingsPaymentGateway(props) { value: inputs.StripeMinTopUp.toString(), }); } + if ( + originInputs['StripePromotionCodesEnabled'] !== + inputs.StripePromotionCodesEnabled && + inputs.StripePromotionCodesEnabled !== undefined + ) { + options.push({ + key: 'StripePromotionCodesEnabled', + value: inputs.StripePromotionCodesEnabled ? 'true' : 'false', + }); + } // 发送请求 const requestQueue = options.map((opt) => @@ -225,6 +240,15 @@ export default function SettingsPaymentGateway(props) { placeholder={t('例如:2,就是最低充值2$')} /> + + + diff --git a/web/src/pages/Setting/Ratio/ModelRatioSettings.jsx b/web/src/pages/Setting/Ratio/ModelRatioSettings.jsx index ed982edcf..b298cc787 100644 --- a/web/src/pages/Setting/Ratio/ModelRatioSettings.jsx +++ b/web/src/pages/Setting/Ratio/ModelRatioSettings.jsx @@ -226,8 +226,12 @@ export default function ModelRatioSettings(props) { - setInputs({ ...inputs, ImageRatio: value }) - } + onChange={(value) => setInputs({ ...inputs, ImageRatio: value })} /> @@ -249,7 +251,9 @@ export default function ModelRatioSettings(props) { - setInputs({ ...inputs, AudioRatio: value }) - } + onChange={(value) => setInputs({ ...inputs, AudioRatio: value })} /> @@ -270,8 +272,12 @@ export default function ModelRatioSettings(props) { { tab: ( - {t('倍率设置')} + {t('分组与模型定价设置')} ), content: , diff --git a/web/src/pages/UserAgreement/index.jsx b/web/src/pages/UserAgreement/index.jsx new file mode 100644 index 000000000..965a0dd54 --- /dev/null +++ b/web/src/pages/UserAgreement/index.jsx @@ -0,0 +1,37 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import DocumentRenderer from '../../components/common/DocumentRenderer'; + +const UserAgreement = () => { + const { t } = useTranslation(); + + return ( + + ); +}; + +export default UserAgreement; \ No newline at end of file diff --git a/web/src/services/secureVerification.js b/web/src/services/secureVerification.js new file mode 100644 index 000000000..51f871a96 --- /dev/null +++ b/web/src/services/secureVerification.js @@ -0,0 +1,232 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import { API, showError } from '../helpers'; +import { + prepareCredentialRequestOptions, + buildAssertionResult, + isPasskeySupported, +} from '../helpers/passkey'; + +/** + * 通用安全验证服务 + * 验证状态完全由后端 Session 控制,前端不存储任何状态 + */ +export class SecureVerificationService { + /** + * 检查用户可用的验证方式 + * @returns {Promise<{has2FA: boolean, hasPasskey: boolean, passkeySupported: boolean}>} + */ + static async checkAvailableVerificationMethods() { + try { + const [twoFAResponse, passkeyResponse, passkeySupported] = + await Promise.all([ + API.get('/api/user/2fa/status'), + API.get('/api/user/passkey'), + isPasskeySupported(), + ]); + + console.log('=== DEBUGGING VERIFICATION METHODS ==='); + console.log('2FA Response:', JSON.stringify(twoFAResponse, null, 2)); + console.log( + 'Passkey Response:', + JSON.stringify(passkeyResponse, null, 2), + ); + + const has2FA = + twoFAResponse.data?.success && + twoFAResponse.data?.data?.enabled === true; + const hasPasskey = + passkeyResponse.data?.success && + passkeyResponse.data?.data?.enabled === true; + + console.log('has2FA calculation:', { + success: twoFAResponse.data?.success, + dataExists: !!twoFAResponse.data?.data, + enabled: twoFAResponse.data?.data?.enabled, + result: has2FA, + }); + + console.log('hasPasskey calculation:', { + success: passkeyResponse.data?.success, + dataExists: !!passkeyResponse.data?.data, + enabled: passkeyResponse.data?.data?.enabled, + result: hasPasskey, + }); + + const result = { + has2FA, + hasPasskey, + passkeySupported, + }; + + return result; + } catch (error) { + console.error('Failed to check verification methods:', error); + return { + has2FA: false, + hasPasskey: false, + passkeySupported: false, + }; + } + } + + /** + * 执行2FA验证 + * @param {string} code - 验证码 + * @returns {Promise} + */ + static async verify2FA(code) { + if (!code?.trim()) { + throw new Error('请输入验证码或备用码'); + } + + // 调用通用验证 API,验证成功后后端会设置 session + const verifyResponse = await API.post('/api/verify', { + method: '2fa', + code: code.trim(), + }); + + if (!verifyResponse.data?.success) { + throw new Error(verifyResponse.data?.message || '验证失败'); + } + + // 验证成功,session 已在后端设置 + } + + /** + * 执行Passkey验证 + * @returns {Promise} + */ + static async verifyPasskey() { + try { + // 开始Passkey验证 + const beginResponse = await API.post('/api/user/passkey/verify/begin'); + if (!beginResponse.data?.success) { + throw new Error(beginResponse.data?.message || '开始验证失败'); + } + + // 准备WebAuthn选项 + const publicKey = prepareCredentialRequestOptions( + beginResponse.data.data.options, + ); + + // 执行WebAuthn验证 + const credential = await navigator.credentials.get({ publicKey }); + if (!credential) { + throw new Error('Passkey 验证被取消'); + } + + // 构建验证结果 + const assertionResult = buildAssertionResult(credential); + + // 完成验证 + const finishResponse = await API.post( + '/api/user/passkey/verify/finish', + assertionResult, + ); + if (!finishResponse.data?.success) { + throw new Error(finishResponse.data?.message || '验证失败'); + } + + // 调用通用验证 API 设置 session(Passkey 验证已完成) + const verifyResponse = await API.post('/api/verify', { + method: 'passkey', + }); + + if (!verifyResponse.data?.success) { + throw new Error(verifyResponse.data?.message || '验证失败'); + } + + // 验证成功,session 已在后端设置 + } catch (error) { + if (error.name === 'NotAllowedError') { + throw new Error('Passkey 验证被取消或超时'); + } else if (error.name === 'InvalidStateError') { + throw new Error('Passkey 验证状态无效'); + } else { + throw error; + } + } + } + + /** + * 通用验证方法,根据验证类型执行相应的验证流程 + * @param {string} method - 验证方式: '2fa' | 'passkey' + * @param {string} code - 2FA验证码(当method为'2fa'时必需) + * @returns {Promise} + */ + static async verify(method, code = '') { + switch (method) { + case '2fa': + return await this.verify2FA(code); + case 'passkey': + return await this.verifyPasskey(); + default: + throw new Error(`不支持的验证方式: ${method}`); + } + } +} + +/** + * 预设的API调用函数工厂 + */ +export const createApiCalls = { + /** + * 创建查看渠道密钥的API调用 + * @param {number} channelId - 渠道ID + */ + viewChannelKey: (channelId) => async () => { + // 新系统中,验证已通过中间件处理,直接调用 API 即可 + const response = await API.post(`/api/channel/${channelId}/key`, {}); + return response.data; + }, + + /** + * 创建自定义API调用 + * @param {string} url - API URL + * @param {string} method - HTTP方法,默认为 'POST' + * @param {Object} extraData - 额外的请求数据 + */ + custom: + (url, method = 'POST', extraData = {}) => + async () => { + // 新系统中,验证已通过中间件处理 + const data = extraData; + + let response; + switch (method.toUpperCase()) { + case 'GET': + response = await API.get(url, { params: data }); + break; + case 'POST': + response = await API.post(url, data); + break; + case 'PUT': + response = await API.put(url, data); + break; + case 'DELETE': + response = await API.delete(url, { data }); + break; + default: + throw new Error(`不支持的HTTP方法: ${method}`); + } + return response.data; + }, +};