diff --git a/.dockerignore b/.dockerignore index 0670cd7d1bba..781a7b550008 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 000000000000..5c2506180e04 --- /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 000000000000..cdfc43f0d7c0 --- /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 4f6e41ac2b9f..7403f6c0020c 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/electron-build.yml b/.github/workflows/electron-build.yml new file mode 100644 index 000000000000..285bc4bb0f19 --- /dev/null +++ b/.github/workflows/electron-build.yml @@ -0,0 +1,142 @@ +name: Build Electron App + +on: + push: + tags: + - '*' # Triggers on version tags like v1.0.0 + 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 'one-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 'one-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: Create Release + uses: softprops/action-gh-release@v2 + with: + files: | + windows-build/* + draft: false + prerelease: false + overwrite_files: true + 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 3e3ddc53b11c..000000000000 --- 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 8eaf2d67a58b..000000000000 --- 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 000000000000..4f7df84e22f6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,142 @@ +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 'one-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 'one-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-* + draft: true + generate_release_notes: true + 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 'one-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-* + draft: true + generate_release_notes: true + 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 'one-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 + draft: true + generate_release_notes: true + 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 000000000000..4f515a188dbe --- /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 30e864f34415..000000000000 --- 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 1382829fd7d4..0cf4ce09af64 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/README.en.md b/README.en.md index 60d4f6a02862..54f4274a57bb 100644 --- a/README.en.md +++ b/README.en.md @@ -89,22 +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. 🔄 Request format conversion functionality, supporting the following three format conversions: +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 -19. 💰 Cache billing support, which allows billing at a set ratio when cache is hit: +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: @@ -134,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` @@ -188,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 @@ -198,22 +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) +- [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 index d069800533e9..b308660cf6ac 100644 --- a/README.fr.md +++ b/README.fr.md @@ -89,22 +89,23 @@ New API offre un large éventail de fonctionnalités, veuillez vous référer à 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 Claude Messages, [Documentation de l'API](https://docs.newapi.pro/api/anthropic-chat) -14. Prise en charge de l'accès à l'interface de discussion via la route /chat2link -15. 🧠 Prise en charge de la définition de l'effort de raisonnement via les suffixes de nom de modèle : +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`) -16. 🔄 Fonctionnalité de la pensée au contenu -17. 🔄 Limitation du débit du modèle pour les utilisateurs -18. 🔄 Fonctionnalité de conversion de format de requête, prenant en charge les trois conversions de format suivantes : +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 -19. 💰 Prise en charge de la facturation du cache, qui permet de facturer à un ratio défini lorsque le cache est atteint : +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 : @@ -134,14 +135,12 @@ Pour des instructions de configuration détaillées, veuillez vous référer à - `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` -- `FORCE_STREAM_OPTION` : S'il faut remplacer le paramètre client stream_options, 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` -- `COHERE_SAFETY_SETTING` : Paramètres de sécurité du modèle Cohere, les options sont `NONE`, `CONTEXTUAL`, `STRICT`, la valeur par défaut est `NONE` - `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 +- `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` @@ -188,7 +187,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234 ``` ## 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`. Il est **recommandé d'activer la mise en 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 @@ -198,22 +197,21 @@ La fonctionnalité de nouvelle tentative de canal a été implémentée, vous po 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](https://docs.newapi.pro/api/openai-chat) -- [API d'image](https://docs.newapi.pro/api/openai-image) -- [API de rerank](https://docs.newapi.pro/api/jinaai-rerank) -- [API en temps réel](https://docs.newapi.pro/api/openai-realtime) +- [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 -- [chatnio](https://github.com/Deeptrain-Community/chatnio) : Solution B/C unique d'IA de nouvelle génération - [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 -- [VoAPI](https://github.com/VoAPI/VoAPI) : Version embellie du frontend basée sur New API ## Aide et support diff --git a/README.ja.md b/README.ja.md index 13049e86d7f8..315518c911ba 100644 --- a/README.ja.md +++ b/README.ja.md @@ -89,22 +89,23 @@ New APIは豊富な機能を提供しています。詳細な機能について 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. ⚡ Claude Messages形式をサポート、[APIドキュメント](https://docs.newapi.pro/api/anthropic-chat) -14. /chat2linkルートを使用してチャット画面に入ることをサポート -15. 🧠 モデル名のサフィックスを通じてreasoning effortを設定することをサポート: +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`) -16. 🔄 思考からコンテンツへの機能 -17. 🔄 ユーザーに対するモデルレート制限機能 -18. 🔄 リクエストフォーマット変換機能、以下の3つのフォーマット変換をサポート: +17. 🔄 思考からコンテンツへの機能 +18. 🔄 ユーザーに対するモデルレート制限機能 +19. 🔄 リクエストフォーマット変換機能、以下の3つのフォーマット変換をサポート: 1. OpenAI Chat Completions => Claude Messages 2. Claude Messages => OpenAI Chat Completions(Claude Codeがサードパーティモデルを呼び出す際に使用可能) 3. OpenAI Chat Completions => Gemini Chat -19. 💰 キャッシュ課金サポート、有効にするとキャッシュがヒットした際に設定された比率で課金できます: +20. 💰 キャッシュ課金サポート、有効にするとキャッシュがヒットした際に設定された比率で課金できます: 1. `システム設定-運営設定`で`プロンプトキャッシュ倍率`オプションを設定 2. チャネルで`プロンプトキャッシュ倍率`を設定、範囲は0-1、例えば0.5に設定するとキャッシュがヒットした際に50%で課金 3. サポートされているチャネル: @@ -196,7 +197,8 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234 詳細なAPIドキュメントについては[APIドキュメント](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) diff --git a/README.md b/README.md index af2b64b44427..feeff86f555a 100644 --- a/README.md +++ b/README.md @@ -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. 支持的渠道: @@ -192,7 +193,8 @@ 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) diff --git a/common/constants.go b/common/constants.go index e6d59d101eed..2ef2b7df2f92 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/database.go b/common/database.go index 38a54d5e6f27..71dbd94d588c 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/gin.go b/common/gin.go index 2cb358444bc1..91a3a441a202 100644 --- a/common/gin.go +++ b/common/gin.go @@ -3,6 +3,7 @@ package common import ( "bytes" "io" + "mime/multipart" "net/http" "one-api/constant" "strings" @@ -113,3 +114,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/constant/api_type.go b/constant/api_type.go index 0ea5048f2e78..130ae945555f 100644 --- a/constant/api_type.go +++ b/constant/api_type.go @@ -31,7 +31,7 @@ const ( APITypeXai APITypeCoze APITypeJimeng - APITypeMoonshot - APITypeSubmodel - APITypeDummy // this one is only for count, do not add any channel after this + APITypeMoonshot + APITypeSubmodel + 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 7d8893c1d0dc..426477e13064 100644 --- a/constant/channel.go +++ b/constant/channel.go @@ -52,6 +52,7 @@ const ( ChannelTypeVidu = 52 ChannelTypeSubmodel = 53 ChannelTypeDoubaoVideo = 54 + ChannelTypeSora = 55 ChannelTypeDummy // this one is only for count, do not add any channel after this ) @@ -112,4 +113,67 @@ var ChannelBaseURLs = []string{ "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/controller/billing.go b/controller/billing.go index 1fb83633e4da..db3b62b1eafe 100644 --- a/controller/billing.go +++ b/controller/billing.go @@ -5,6 +5,7 @@ import ( "one-api/common" "one-api/dto" "one-api/model" + "one-api/setting/operation_setting" ) 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 1082b9e73ecf..a7dfe950845e 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -127,6 +127,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 ff1e8cef480c..5bbc20ebcc9f 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -28,6 +28,7 @@ import ( "time" "github.com/bytedance/gopkg/util/gopool" + "github.com/samber/lo" "github.com/gin-gonic/gin" ) @@ -40,51 +41,39 @@ type testResult struct { func testChannel(channel *model.Channel, testModel string, endpointType string) testResult { tik := time.Now() - if channel.Type == constant.ChannelTypeMidjourney { + var unsupportedTestChannelTypes = []int{ + constant.ChannelTypeMidjourney, + constant.ChannelTypeMidjourneyPlus, + constant.ChannelTypeSunoAPI, + constant.ChannelTypeKling, + constant.ChannelTypeJimeng, + constant.ChannelTypeDoubaoVideo, + constant.ChannelTypeVidu, + } + if lo.Contains(unsupportedTestChannelTypes, channel.Type) { + channelTypeName := constant.GetChannelTypeName(channel.Type) return testResult{ - localErr: errors.New("midjourney channel test is not supported"), - newAPIError: nil, - } - } - if channel.Type == constant.ChannelTypeMidjourneyPlus { - return testResult{ - localErr: errors.New("midjourney plus channel test is not supported"), - newAPIError: nil, - } - } - if channel.Type == constant.ChannelTypeSunoAPI { - return testResult{ - localErr: errors.New("suno channel test is not supported"), - newAPIError: nil, - } - } - if channel.Type == constant.ChannelTypeKling { - return testResult{ - localErr: errors.New("kling channel test is not supported"), - newAPIError: nil, - } - } - if channel.Type == constant.ChannelTypeJimeng { - return testResult{ - localErr: errors.New("jimeng channel test is not supported"), - newAPIError: nil, - } - } - if channel.Type == constant.ChannelTypeDoubaoVideo { - return testResult{ - localErr: errors.New("doubao video 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) + 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" + } + } + } + requestPath := "/v1/chat/completions" // 如果指定了端点类型,使用指定的端点类型 @@ -116,18 +105,6 @@ func testChannel(channel *model.Channel, testModel string, endpointType string) 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" - } - } - } - cache, err := model.GetUserCache(1) if err != nil { return testResult{ @@ -645,10 +622,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 4aedee3b37ae..24cfa02df04b 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -198,9 +198,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 { diff --git a/controller/misc.go b/controller/misc.go index 07f7d3f058b3..8654848d5e32 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -43,6 +43,7 @@ func GetStatus(c *gin.Context) { defer common.OptionMapRWMutex.RUnlock() passkeySetting := system_setting.GetPasskeySettings() + legalSetting := system_setting.GetLegalSettings() data := gin.H{ "version": common.Version, @@ -66,18 +67,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, @@ -104,6 +109,8 @@ func GetStatus(c *gin.Context) { "passkey_user_verification": passkeySetting.UserVerification, "passkey_attachment": passkeySetting.AttachmentPreference, "setup": constant.Setup, + "user_agreement_enabled": legalSetting.UserAgreement != "", + "privacy_policy_enabled": legalSetting.PrivacyPolicy != "", } // 根据启用状态注入可选内容 @@ -147,6 +154,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/setup.go b/controller/setup.go index 3ae255e94b50..8b7fa3e7a405 100644 --- a/controller/setup.go +++ b/controller/setup.go @@ -178,4 +178,4 @@ func boolToString(b bool) string { return "true" } return "false" -} \ No newline at end of file +} diff --git a/controller/task_video.go b/controller/task_video.go index ded011fe9ed4..8d4acdda83d8 100644 --- a/controller/task_video.go +++ b/controller/task_video.go @@ -47,6 +47,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())) @@ -92,6 +97,7 @@ func updateVideoSingleTask(ctx context.Context, adaptor channel.TaskAdaptor, cha 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 { diff --git a/controller/topup.go b/controller/topup.go index 243e67940d98..62bc676dae62 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -86,8 +86,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) } @@ -115,7 +116,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()) @@ -176,18 +177,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 { @@ -235,8 +237,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 { @@ -312,3 +314,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 9a568d857e42..a4bdf064d341 100644 --- a/controller/topup_stripe.go +++ b/controller/topup_stripe.go @@ -83,12 +83,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 { @@ -258,7 +259,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 @@ -279,7 +280,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/video_proxy.go b/controller/video_proxy.go new file mode 100644 index 000000000000..55ba707c054d --- /dev/null +++ b/controller/video_proxy.go @@ -0,0 +1,129 @@ +package controller + +import ( + "fmt" + "io" + "net/http" + "one-api/logger" + "one-api/model" + "time" + + "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/docs/translation-glossary.md b/docs/translation-glossary.md new file mode 100644 index 000000000000..2a617072a5e0 --- /dev/null +++ b/docs/translation-glossary.md @@ -0,0 +1,72 @@ +# 翻译术语表 (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 | + +## 翻译注意事项 (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/dto/gemini.go b/dto/gemini.go index fdeb2793d1a8..b91701723c9f 100644 --- a/dto/gemini.go +++ b/dto/gemini.go @@ -293,12 +293,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 { @@ -328,6 +329,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 5aece25f206f..16e9a175d982 100644 --- a/dto/openai_image.go +++ b/dto/openai_image.go @@ -74,14 +74,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 dbdfad446ea3..d477ee6c6726 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -87,6 +87,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 { diff --git a/dto/openai_response.go b/dto/openai_response.go index 6353c15ffe82..7a3ddc68e2b1 100644 --- a/dto/openai_response.go +++ b/dto/openai_response.go @@ -233,6 +233,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/electron/README.md b/electron/README.md new file mode 100644 index 000000000000..88463b8aefd9 --- /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 000000000000..cef714328d5a --- /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 000000000000..517393b2e1d6 --- /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 000000000000..a00aebcd06ae --- /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 000000000000..c63ac2d77598 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 000000000000..210a4565852d --- /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 000000000000..c96d425a0502 --- /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 000000000000..f861c91cb436 --- /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 000000000000..ac971fd0a1c4 --- /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 000000000000..57df8ead0314 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 000000000000..187741e10d7a 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 000000000000..d5666a04e55b Binary files /dev/null and b/electron/tray-iconTemplate@2x.png differ diff --git a/go.mod b/go.mod index 4e743d2318f4..125cc5c749b7 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/go-playground/validator/v10 v10.20.0 github.com/go-redis/redis/v8 v8.11.5 github.com/go-webauthn/webauthn v0.14.0 - github.com/golang-jwt/jwt v3.2.2+incompatible + 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 @@ -68,7 +68,6 @@ require ( 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/golang-jwt/jwt/v5 v5.3.0 // 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 diff --git a/logger/logger.go b/logger/logger.go index d59e51cb8de9..68f564370e9f 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -7,6 +7,7 @@ import ( "io" "log" "one-api/common" + "one-api/setting/operation_setting" "os" "path/filepath" "sync" @@ -92,18 +93,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) } } diff --git a/middleware/distributor.go b/middleware/distributor.go index 3d929df493d4..a33ca5af95eb 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -165,6 +165,38 @@ 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 { diff --git a/model/channel.go b/model/channel.go index 534e2f3f2319..8d1616a9b4c6 100644 --- a/model/channel.go +++ b/model/channel.go @@ -46,7 +46,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/option.go b/model/option.go index 9ace8fece1c7..77525ea25239 100644 --- a/model/option.go +++ b/model/option.go @@ -240,7 +240,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": diff --git a/model/topup.go b/model/topup.go index 802c866f7718..380f5851d9a3 100644 --- a/model/topup.go +++ b/model/topup.go @@ -6,18 +6,20 @@ import ( "one-api/common" "one-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,3 +101,206 @@ 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 +} diff --git a/relay/channel/adapter.go b/relay/channel/adapter.go index 02de99567f8d..964c33256d71 100644 --- a/relay/channel/adapter.go +++ b/relay/channel/adapter.go @@ -4,6 +4,7 @@ import ( "io" "net/http" "one-api/dto" + "one-api/model" relaycommon "one-api/relay/common" "one-api/types" @@ -49,3 +50,7 @@ type TaskAdaptor interface { ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, error) } + +type OpenAIVideoConverter interface { + ConvertToOpenAIVideo(originTask *model.Task) (*relaycommon.OpenAIVideo, error) +} diff --git a/relay/channel/gemini/adaptor.go b/relay/channel/gemini/adaptor.go index 57542aa5ac57..0295f8ecbdd6 100644 --- a/relay/channel/gemini/adaptor.go +++ b/relay/channel/gemini/adaptor.go @@ -67,8 +67,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 +95,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.go b/relay/channel/gemini/relay-gemini.go index c8e9c7579556..a8247217b5a7 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -961,9 +961,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 { @@ -1044,7 +1050,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/ollama/adaptor.go b/relay/channel/ollama/adaptor.go index bafe73b92bd1..d88e04d6feb6 100644 --- a/relay/channel/ollama/adaptor.go +++ b/relay/channel/ollama/adaptor.go @@ -18,7 +18,9 @@ import ( type Adaptor struct { } -func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) { return nil, errors.New("not implemented") } +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, request *dto.ClaudeRequest) (any, error) { openaiAdaptor := openai.Adaptor{} @@ -33,17 +35,25 @@ func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayIn return openAIChatToOllamaChat(c, openaiRequest.(*dto.GeneralOpenAIRequest)) } -func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { return nil, errors.New("not implemented") } +func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { + return nil, errors.New("not implemented") +} -func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { return nil, errors.New("not implemented") } +func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { + return nil, errors.New("not implemented") +} func (a *Adaptor) Init(info *relaycommon.RelayInfo) { } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { - if info.RelayMode == relayconstant.RelayModeEmbeddings { return info.ChannelBaseUrl + "/api/embed", nil } - if strings.Contains(info.RequestURLPath, "/v1/completions") || info.RelayMode == relayconstant.RelayModeCompletions { return info.ChannelBaseUrl + "/api/generate", nil } - return info.ChannelBaseUrl + "/api/chat", nil + if info.RelayMode == relayconstant.RelayModeEmbeddings { + return info.ChannelBaseUrl + "/api/embed", 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 { @@ -53,7 +63,9 @@ 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) { - if request == nil { return nil, errors.New("request is nil") } + if request == nil { + return nil, errors.New("request is nil") + } // decide generate or chat if strings.Contains(info.RequestURLPath, "/v1/completions") || info.RelayMode == relayconstant.RelayModeCompletions { return openAIToGenerate(c, request) @@ -69,7 +81,9 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela return requestOpenAI2Embeddings(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) 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) diff --git a/relay/channel/ollama/dto.go b/relay/channel/ollama/dto.go index 45e49ab431b7..2434a4cbc08a 100644 --- a/relay/channel/ollama/dto.go +++ b/relay/channel/ollama/dto.go @@ -5,12 +5,12 @@ import ( ) 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"` + 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 OllamaToolFunction struct { @@ -20,7 +20,7 @@ type OllamaToolFunction struct { } type OllamaTool struct { - Type string `json:"type"` + Type string `json:"type"` Function OllamaToolFunction `json:"function"` } @@ -43,28 +43,27 @@ type OllamaChatRequest struct { } 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"` + 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"` - Input interface{} `json:"input"` - Options map[string]any `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"` - Embeddings [][]float64 `json:"embeddings"` - PromptEvalCount int `json:"prompt_eval_count,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 3b67f952521c..f94a654c74a5 100644 --- a/relay/channel/ollama/relay-ollama.go +++ b/relay/channel/ollama/relay-ollama.go @@ -35,13 +35,27 @@ func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*Ollam } // 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.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) { @@ -50,21 +64,27 @@ func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*Ollam 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) } } - if len(arr)>0 { chatReq.Options["stop"] = arr } + arr := make([]string, 0, len(v)) + for _, i := range v { + if s, ok := i.(string); ok { + arr = append(arr, s) + } + } + if len(arr) > 0 { + chatReq.Options["stop"] = arr + } } } if len(r.Tools) > 0 { - tools := make([]OllamaTool,0,len(r.Tools)) + 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 } - chatReq.Messages = make([]OllamaChatMessage,0,len(r.Messages)) + chatReq.Messages = make([]OllamaChatMessage, 0, len(r.Messages)) for _, m := range r.Messages { var textBuilder strings.Builder var images []string @@ -79,14 +99,20 @@ func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*Ollam 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 } + 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:] } + 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) } + if base64Data != "" { + images = append(images, base64Data) + } } } else if part.Type == dto.ContentTypeText { textBuilder.WriteString(part.Text) @@ -94,16 +120,24 @@ func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*Ollam } } 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 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)) + 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{} } + 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 @@ -132,28 +166,67 @@ func openAIToGenerate(c *gin.Context, r *dto.GeneralOpenAIRequest) (*OllamaGener gen.Prompt = v case []any: var sb strings.Builder - for _, it := range v { if s,ok:=it.(string); ok { sb.WriteString(s) } } + 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.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.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 } + 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 @@ -161,30 +234,51 @@ func openAIToGenerate(c *gin.Context, r *dto.GeneralOpenAIRequest) (*OllamaGener 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 } + 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} + 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 oResp OllamaEmbeddingResponse body, err := io.ReadAll(resp.Body) - if err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } + if err != nil { + return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } service.CloseResponseBodyGracefully(resp) - if err = common.Unmarshal(body, &oResp); err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } - if oResp.Error != "" { return nil, types.NewOpenAIError(fmt.Errorf("ollama error: %s", oResp.Error), types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } - 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}) } - usage := &dto.Usage{PromptTokens: oResp.PromptEvalCount, CompletionTokens:0, TotalTokens: oResp.PromptEvalCount} - embResp := &dto.OpenAIEmbeddingResponse{Object:"list", Data:data, Model: info.UpstreamModelName, Usage:*usage} + if err = common.Unmarshal(body, &oResp); err != nil { + return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } + if oResp.Error != "" { + return nil, types.NewOpenAIError(fmt.Errorf("ollama error: %s", oResp.Error), types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } + 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}) + } + 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 } - diff --git a/relay/channel/ollama/stream.go b/relay/channel/ollama/stream.go index 964f11d90a78..9a98f7b69960 100644 --- a/relay/channel/ollama/stream.go +++ b/relay/channel/ollama/stream.go @@ -1,210 +1,278 @@ package ollama import ( - "bufio" - "encoding/json" - "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" - "time" + "bufio" + "encoding/json" + "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" + "time" - "github.com/gin-gonic/gin" + "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"` + 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() + 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) + 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)) } + 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) + 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" { 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 + 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" { + 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) } + 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" { reasoningBuilder.WriteString(raw) } - } - if ck.Message != nil && ck.Message.Content != "" { aggContent.WriteString(ck.Message.Content) } else if ck.Response != "" { aggContent.WriteString(ck.Response) } - } + 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" { + 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" { reasoningBuilder.WriteString(raw) } } - aggContent.WriteString(single.Message.Content) - } else { aggContent.WriteString(single.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" { + 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" } + 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 + 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 } +func contentPtr(s string) *string { + if s == "" { + return nil + } + return &s +} diff --git a/relay/channel/openai/relay-openai.go b/relay/channel/openai/relay-openai.go index a88b68502c97..6619fb16005a 100644 --- a/relay/channel/openai/relay-openai.go +++ b/relay/channel/openai/relay-openai.go @@ -163,13 +163,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 @@ -233,6 +230,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 { @@ -631,5 +630,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/perplexity/adaptor.go b/relay/channel/perplexity/adaptor.go index 8ab9c854712e..8a561bdec625 100644 --- a/relay/channel/perplexity/adaptor.go +++ b/relay/channel/perplexity/adaptor.go @@ -22,10 +22,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 +79,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 f9f030e0940a..d37c3b8779f6 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 7ebadd0f9efc..d2119973c243 100644 --- a/relay/channel/perplexity/relay-perplexity.go +++ b/relay/channel/perplexity/relay-perplexity.go @@ -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 4c176c0887c7..41ab763277f0 100644 --- a/relay/channel/siliconflow/adaptor.go +++ b/relay/channel/siliconflow/adaptor.go @@ -61,6 +61,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 } diff --git a/relay/channel/submodel/constants.go b/relay/channel/submodel/constants.go index f5e1feb84e4f..72d6fee3176d 100644 --- a/relay/channel/submodel/constants.go +++ b/relay/channel/submodel/constants.go @@ -13,4 +13,4 @@ var ModelList = []string{ "deepseek-ai/DeepSeek-V3.1", } -const ChannelName = "submodel" \ No newline at end of file +const ChannelName = "submodel" diff --git a/relay/channel/task/kling/adaptor.go b/relay/channel/task/kling/adaptor.go index fec3396aec19..4befd8e2ee29 100644 --- a/relay/channel/task/kling/adaptor.go +++ b/relay/channel/task/kling/adaptor.go @@ -7,13 +7,15 @@ import ( "io" "net/http" "one-api/model" + "strconv" "strings" "time" + "github.com/bytedance/gopkg/util/logger" "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" @@ -303,14 +305,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 +363,50 @@ 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) (*relaycommon.OpenAIVideo, error) { + var klingResp responsePayload + if err := json.Unmarshal(originTask.Data, &klingResp); err != nil { + return nil, errors.Wrap(err, "unmarshal kling task data failed") + } + + convertProgress := func(progress string) int { + progress = strings.TrimSuffix(progress, "%") + p, err := strconv.Atoi(progress) + if err != nil { + logger.Warnf("convert progress failed, progress: %s, err: %v", progress, err) + } + return p + } + + openAIVideo := &relaycommon.OpenAIVideo{ + ID: klingResp.Data.TaskId, + Object: "video", + //Model: "kling-v1", //todo save model + Status: string(originTask.Status), + CreatedAt: klingResp.Data.CreatedAt, + CompletedAt: klingResp.Data.UpdatedAt, + Metadata: make(map[string]any), + Progress: convertProgress(originTask.Progress), + } + + // 处理视频 URL + if len(klingResp.Data.TaskResult.Videos) > 0 { + video := klingResp.Data.TaskResult.Videos[0] + if video.Url != "" { + openAIVideo.Metadata["url"] = video.Url + } + if video.Duration != "" { + openAIVideo.Seconds = video.Duration + } + } + + if klingResp.Code != 0 && klingResp.Message != "" { + openAIVideo.Error = &relaycommon.OpenAIVideoError{ + Message: klingResp.Message, + Code: fmt.Sprintf("%d", klingResp.Code), + } + } + + return openAIVideo, nil +} diff --git a/relay/channel/task/sora/adaptor.go b/relay/channel/task/sora/adaptor.go new file mode 100644 index 000000000000..3ceed42d8623 --- /dev/null +++ b/relay/channel/task/sora/adaptor.go @@ -0,0 +1,195 @@ +package sora + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "one-api/common" + "one-api/dto" + "one-api/model" + "one-api/relay/channel" + relaycommon "one-api/relay/common" + "one-api/service" + "one-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 := 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 == "" { + 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 := json.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) (*relaycommon.OpenAIVideo, error) { + openAIVideo := &relaycommon.OpenAIVideo{} + err := json.Unmarshal(task.Data, openAIVideo) + if err != nil { + return nil, errors.Wrap(err, "unmarshal to OpenAIVideo failed") + } + return openAIVideo, nil +} diff --git a/relay/channel/task/sora/constants.go b/relay/channel/task/sora/constants.go new file mode 100644 index 000000000000..e2f6536eafcd --- /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/vertex/service_account.go b/relay/channel/vertex/service_account.go index f90d5454dd90..14a7ea8ce7c6 100644 --- a/relay/channel/vertex/service_account.go +++ b/relay/channel/vertex/service_account.go @@ -13,7 +13,7 @@ import ( "strings" "github.com/bytedance/gopkg/cache/asynccache" - "github.com/golang-jwt/jwt" + "github.com/golang-jwt/jwt/v5" "fmt" "time" diff --git a/relay/channel/zhipu/relay-zhipu.go b/relay/channel/zhipu/relay-zhipu.go index 8eb0dcc1302b..86ec1eafb7e9 100644 --- a/relay/channel/zhipu/relay-zhipu.go +++ b/relay/channel/zhipu/relay-zhipu.go @@ -17,7 +17,7 @@ import ( "time" "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/common/relay_info.go b/relay/common/relay_info.go index cc860abd9fbf..cb3a6709f06c 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -261,6 +261,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 { @@ -549,3 +550,24 @@ func RemoveDisabledFields(jsonData []byte, channelOtherSettings dto.ChannelOther } return jsonDataAfter, nil } + +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"` + 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:"metadata,omitempty"` +} +type OpenAIVideoError struct { + Message string `json:"message"` + Code string `json:"code"` +} diff --git a/relay/common/relay_utils.go b/relay/common/relay_utils.go index 3a721b47937e..9a6161bae9b0 100644 --- a/relay/common/relay_utils.go +++ b/relay/common/relay_utils.go @@ -6,9 +6,11 @@ import ( "one-api/common" "one-api/constant" "one-api/dto" + "strconv" "strings" "github.com/gin-gonic/gin" + "github.com/samber/lo" ) type HasPrompt interface { @@ -52,7 +54,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 +66,167 @@ func validatePrompt(prompt string) *dto.TaskError { return nil } +func validateMultipartTaskRequest(c *gin.Context, info *RelayInfo, action string) (TaskSubmitReq, error) { + var req TaskSubmitReq + 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 err := common.UnmarshalBodyReusable(c, &req); err != nil { + 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/helper/price.go b/relay/helper/price.go index c23c068b3e3c..08ae78c4c130 100644 --- a/relay/helper/price.go +++ b/relay/helper/price.go @@ -114,7 +114,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/valid_request.go b/relay/helper/valid_request.go index f4a290ec6496..c9d3ed0b7d2e 100644 --- a/relay/helper/valid_request.go +++ b/relay/helper/valid_request.go @@ -275,7 +275,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: diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index c8fd51a115a4..2017c9a6fdec 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -29,6 +29,7 @@ import ( taskdoubao "one-api/relay/channel/task/doubao" taskjimeng "one-api/relay/channel/task/jimeng" "one-api/relay/channel/task/kling" + tasksora "one-api/relay/channel/task/sora" "one-api/relay/channel/task/suno" taskvertex "one-api/relay/channel/task/vertex" taskVidu "one-api/relay/channel/task/vidu" @@ -137,6 +138,8 @@ func GetTaskAdaptor(platform constant.TaskPlatform) channel.TaskAdaptor { return &taskVidu.TaskAdaptor{} case constant.ChannelTypeDoubaoVideo: return &taskdoubao.TaskAdaptor{} + case constant.ChannelTypeSora: + return &tasksora.TaskAdaptor{} } } return nil diff --git a/relay/relay_task.go b/relay/relay_task.go index 9cb8cd5c8508..fdf6f47cd1d1 100644 --- a/relay/relay_task.go +++ b/relay/relay_task.go @@ -11,6 +11,7 @@ import ( "one-api/constant" "one-api/dto" "one-api/model" + "one-api/relay/channel" relaycommon "one-api/relay/common" relayconstant "one-api/relay/constant" "one-api/service" @@ -53,7 +54,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 +71,14 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (taskErr *dto. } else { ratio = modelPrice * groupRatio } + 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,11 +147,22 @@ 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) + 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{}) other["model_price"] = modelPrice other["group_ratio"] = groupRatio @@ -362,11 +382,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 { + openAIVideo, err := converter.ConvertToOpenAIVideo(originTask) + if err != nil { + taskResp = service.TaskErrorWrapper(err, "convert_to_openai_video_failed", http.StatusInternalServerError) + return + } + respBody, _ = json.Marshal(openAIVideo) + 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/router/api-router.go b/router/api-router.go index d296159146dd..7eefa936a557 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -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) @@ -73,6 +75,7 @@ func SetApiRouter(router *gin.Engine) { 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) @@ -93,6 +96,8 @@ 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) diff --git a/router/video-router.go b/router/video-router.go index bcc05eae9cd0..e1538e63648e 100644 --- a/router/video-router.go +++ b/router/video-router.go @@ -9,11 +9,18 @@ import ( 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/service/channel.go b/service/channel.go index 6ddc8e9ecca9..5c55855b270d 100644 --- a/service/channel.go +++ b/service/channel.go @@ -75,6 +75,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 b232ca396120..1a39e537a8eb 100644 --- a/service/convert.go +++ b/service/convert.go @@ -636,9 +636,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 +732,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 036c43af8023..09464390ef79 100644 --- a/service/download.go +++ b/service/download.go @@ -45,7 +45,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 +64,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/http_client.go b/service/http_client.go index c1d6880c9f6e..79eca4a985c6 100644 --- a/service/http_client.go +++ b/service/http_client.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "one-api/common" + "one-api/setting/system_setting" "sync" "time" @@ -19,12 +20,27 @@ var ( 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, } } } @@ -69,6 +85,7 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) { Transport: &http.Transport{ Proxy: http.ProxyURL(parsedURL), }, + CheckRedirect: checkRedirect, } client.Timeout = time.Duration(common.RelayTimeout) * time.Second proxyClientLock.Lock() @@ -102,6 +119,7 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) { return dialer.Dial(network, addr) }, }, + CheckRedirect: checkRedirect, } client.Timeout = time.Duration(common.RelayTimeout) * time.Second proxyClientLock.Lock() diff --git a/setting/operation_setting/general_setting.go b/setting/operation_setting/general_setting.go index ae0c436ecefd..c47ff64faadc 100644 --- a/setting/operation_setting/general_setting.go +++ b/setting/operation_setting/general_setting.go @@ -2,17 +2,34 @@ package operation_setting import "one-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/ratio_setting/model_ratio.go b/setting/ratio_setting/model_ratio.go index 5e55576fdc9b..f8ffbaa8cd77 100644 --- a/setting/ratio_setting/model_ratio.go +++ b/setting/ratio_setting/model_ratio.go @@ -290,6 +290,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{ @@ -452,6 +454,10 @@ func GetDefaultModelRatioMap() map[string]float64 { return defaultModelRatio } +func GetDefaultModelPriceMap() map[string]float64 { + return defaultModelPrice +} + func GetDefaultImageRatioMap() map[string]float64 { return defaultImageRatio } diff --git a/setting/system_setting/legal.go b/setting/system_setting/legal.go new file mode 100644 index 000000000000..5758ef71cee5 --- /dev/null +++ b/setting/system_setting/legal.go @@ -0,0 +1,21 @@ +package system_setting + +import "one-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/types/error.go b/types/error.go index a42e8438575c..77a56dd2562f 100644 --- a/types/error.go +++ b/types/error.go @@ -69,6 +69,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 +160,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 +189,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 ec7fcdfe9cd9..dd5dad1aaabd 100644 --- a/types/price_data.go +++ b/types/price_data.go @@ -17,6 +17,7 @@ type PriceData struct { ImageRatio float64 AudioRatio float64 AudioCompletionRatio float64 + OtherRatios map[string]float64 UsePrice bool ShouldPreConsumedQuota int GroupRatioInfo GroupRatioInfo diff --git a/web/bun.lock b/web/bun.lock index 53467aa5ebb9..3d4a2dfa0a44 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/index.html b/web/index.html index df6b0e398900..a9df87f5d335 100644 --- a/web/index.html +++ b/web/index.html @@ -10,7 +10,7 @@ content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用" /> New API - + diff --git a/web/package.json b/web/package.json index f014d84b9e0f..cbb50f7ef664 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", diff --git a/web/src/App.jsx b/web/src/App.jsx index 635742f9161e..06e364897cf6 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}> + + + } + /> { 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(); @@ -98,6 +106,10 @@ const LoginForm = () => { setTurnstileEnabled(true); setTurnstileSiteKey(status.turnstile_site_key); } + + // 从 status 获取用户协议和隐私政策的启用状态 + setHasUserAgreement(status.user_agreement_enabled || false); + setHasPrivacyPolicy(status.privacy_policy_enabled || false); }, [status]); useEffect(() => { @@ -113,6 +125,10 @@ const LoginForm = () => { }, []); const onWeChatLoginClicked = () => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } setWechatLoading(true); setShowWeChatLoginModal(true); setWechatLoading(false); @@ -152,6 +168,10 @@ const LoginForm = () => { } async function handleSubmit(e) { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } if (turnstileEnabled && turnstileToken === '') { showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); return; @@ -203,6 +223,10 @@ const LoginForm = () => { // 添加Telegram登录处理函数 const onTelegramLoginClicked = async (response) => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } const fields = [ 'id', 'first_name', @@ -239,6 +263,10 @@ const LoginForm = () => { // 包装的GitHub登录点击处理 const handleGitHubClick = () => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } setGithubLoading(true); try { onGitHubOAuthClicked(status.github_client_id); @@ -250,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); @@ -261,6 +293,10 @@ const LoginForm = () => { // 包装的LinuxDO登录点击处理 const handleLinuxDOClick = () => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } setLinuxdoLoading(true); try { onLinuxDOOAuthClicked(status.linuxdo_client_id); @@ -278,6 +314,10 @@ const LoginForm = () => { }; const handlePasskeyLogin = async () => { + if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) { + showInfo(t('请先阅读并同意用户协议和隐私政策')); + return; + } if (!passkeySupported) { showInfo('当前环境无法使用 Passkey 登录'); return; @@ -296,15 +336,22 @@ const LoginForm = () => { return; } - const publicKeyOptions = prepareCredentialRequestOptions(data?.options || data?.publicKey || data); - const assertion = await navigator.credentials.get({ publicKey: publicKeyOptions }); + 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 finishRes = await API.post( + '/api/user/passkey/login/finish', + payload, + ); const finish = finishRes.data; if (finish.success) { userDispatch({ type: 'login', payload: finish.data }); @@ -474,6 +521,44 @@ const LoginForm = () => { + {(hasUserAgreement || hasPrivacyPolicy) && ( +
+ setAgreedToTerms(e.target.checked)} + > + + {t('我已阅读并同意')} + {hasUserAgreement && ( + <> + + {t('用户协议')} + + + )} + {hasUserAgreement && hasPrivacyPolicy && t('和')} + {hasPrivacyPolicy && ( + <> + + {t('隐私政策')} + + + )} + + +
+ )} + {!status.self_use_mode_enabled && (
@@ -542,6 +627,44 @@ const LoginForm = () => { 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 9c98bdc3a0e9..436a7b6b8a19 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 000000000000..383afc11d993 --- /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 index cd38775274db..1bb2998b208b 100644 --- a/web/src/components/common/examples/ChannelKeyViewExample.jsx +++ b/web/src/components/common/examples/ChannelKeyViewExample.jsx @@ -58,7 +58,7 @@ const ChannelKeyViewExample = ({ channelId }) => { // 开始查看密钥流程 const handleViewKey = async () => { const apiCall = createApiCalls.viewChannelKey(channelId); - + await startVerification(apiCall, { title: t('查看渠道密钥'), description: t('为了保护账户安全,请验证您的身份。'), @@ -69,11 +69,7 @@ const ChannelKeyViewExample = ({ channelId }) => { return ( <> {/* 查看密钥按钮 */} - @@ -114,4 +110,4 @@ const ChannelKeyViewExample = ({ channelId }) => { ); }; -export default ChannelKeyViewExample; \ No newline at end of file +export default ChannelKeyViewExample; diff --git a/web/src/components/common/modals/SecureVerificationModal.jsx b/web/src/components/common/modals/SecureVerificationModal.jsx index 06f18c7e676e..6c61c291d1d6 100644 --- a/web/src/components/common/modals/SecureVerificationModal.jsx +++ b/web/src/components/common/modals/SecureVerificationModal.jsx @@ -19,7 +19,16 @@ 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'; +import { + Modal, + Button, + Input, + Typography, + Tabs, + TabPane, + Space, + Spin, +} from '@douyinfe/semi-ui'; /** * 通用安全验证模态框组件 @@ -78,9 +87,7 @@ const SecureVerificationModal = ({ title={title || t('安全验证')} visible={visible} onCancel={onCancel} - footer={ - - } + footer={} width={500} style={{ maxWidth: '90vw' }} > @@ -123,21 +130,21 @@ const SecureVerificationModal = ({ width={460} centered style={{ - maxWidth: 'calc(100vw - 32px)' + maxWidth: 'calc(100vw - 32px)', }} bodyStyle={{ - padding: '20px 24px' + padding: '20px 24px', }} >
{/* 描述信息 */} {description && ( {description} @@ -153,10 +160,7 @@ const SecureVerificationModal = ({ style={{ margin: 0 }} > {has2FA && ( - +
- + + } style={{ width: '100%' }} @@ -178,24 +195,26 @@ const SecureVerificationModal = ({
{t('从认证器应用中获取验证码,或使用备用码')} -
+
@@ -214,31 +233,47 @@ const SecureVerificationModal = ({ )} {hasPasskey && passkeySupported && ( - +
-
-
- - +
+
+ +
- + {t('使用 Passkey 验证')} {t('点击验证按钮,使用您的生物特征或安全密钥')}
-
+
@@ -282,4 +319,4 @@ const SecureVerificationModal = ({ ); }; -export default SecureVerificationModal; \ No newline at end of file +export default SecureVerificationModal; diff --git a/web/src/components/settings/OperationSetting.jsx b/web/src/components/settings/OperationSetting.jsx index 0d6e441071e5..51cdb1de6630 100644 --- a/web/src/components/settings/OperationSetting.jsx +++ b/web/src/components/settings/OperationSetting.jsx @@ -42,7 +42,7 @@ const OperationSetting = () => { QuotaPerUnit: 0, USDExchangeRate: 0, RetryTimes: 0, - DisplayInCurrencyEnabled: false, + 'general_setting.quota_display_type': 'USD', DisplayTokenStatEnabled: false, DefaultCollapseSidebar: false, DemoSiteEnabled: false, diff --git a/web/src/components/settings/OtherSetting.jsx b/web/src/components/settings/OtherSetting.jsx index 18119d2427db..646d21e74ce0 100644 --- a/web/src/components/settings/OtherSetting.jsx +++ b/web/src/components/settings/OtherSetting.jsx @@ -34,10 +34,15 @@ import { useTranslation } from 'react-i18next'; import { StatusContext } from '../../context/Status'; import Text from '@douyinfe/semi-ui/lib/es/typography/text'; +const LEGAL_USER_AGREEMENT_KEY = 'legal.user_agreement'; +const LEGAL_PRIVACY_POLICY_KEY = 'legal.privacy_policy'; + const OtherSetting = () => { const { t } = useTranslation(); let [inputs, setInputs] = useState({ Notice: '', + [LEGAL_USER_AGREEMENT_KEY]: '', + [LEGAL_PRIVACY_POLICY_KEY]: '', SystemName: '', Logo: '', Footer: '', @@ -69,6 +74,8 @@ const OtherSetting = () => { const [loadingInput, setLoadingInput] = useState({ Notice: false, + [LEGAL_USER_AGREEMENT_KEY]: false, + [LEGAL_PRIVACY_POLICY_KEY]: false, SystemName: false, Logo: false, HomePageContent: false, @@ -96,6 +103,50 @@ const OtherSetting = () => { setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: false })); } }; + // 通用设置 - UserAgreement + const submitUserAgreement = async () => { + try { + setLoadingInput((loadingInput) => ({ + ...loadingInput, + [LEGAL_USER_AGREEMENT_KEY]: true, + })); + await updateOption( + LEGAL_USER_AGREEMENT_KEY, + inputs[LEGAL_USER_AGREEMENT_KEY], + ); + showSuccess(t('用户协议已更新')); + } catch (error) { + console.error(t('用户协议更新失败'), error); + showError(t('用户协议更新失败')); + } finally { + setLoadingInput((loadingInput) => ({ + ...loadingInput, + [LEGAL_USER_AGREEMENT_KEY]: false, + })); + } + }; + // 通用设置 - PrivacyPolicy + const submitPrivacyPolicy = async () => { + try { + setLoadingInput((loadingInput) => ({ + ...loadingInput, + [LEGAL_PRIVACY_POLICY_KEY]: true, + })); + await updateOption( + LEGAL_PRIVACY_POLICY_KEY, + inputs[LEGAL_PRIVACY_POLICY_KEY], + ); + showSuccess(t('隐私政策已更新')); + } catch (error) { + console.error(t('隐私政策更新失败'), error); + showError(t('隐私政策更新失败')); + } finally { + setLoadingInput((loadingInput) => ({ + ...loadingInput, + [LEGAL_PRIVACY_POLICY_KEY]: false, + })); + } + }; // 个性化设置 const formAPIPersonalization = useRef(); // 个性化设置 - SystemName @@ -324,6 +375,40 @@ const OtherSetting = () => { + + + + diff --git a/web/src/components/settings/PersonalSetting.jsx b/web/src/components/settings/PersonalSetting.jsx index c9934604c211..18d3748019ea 100644 --- a/web/src/components/settings/PersonalSetting.jsx +++ b/web/src/components/settings/PersonalSetting.jsx @@ -155,9 +155,7 @@ const PersonalSetting = () => { gotifyUrl: settings.gotify_url || '', gotifyToken: settings.gotify_token || '', gotifyPriority: - settings.gotify_priority !== undefined - ? settings.gotify_priority - : 5, + settings.gotify_priority !== undefined ? settings.gotify_priority : 5, acceptUnsetModelRatioModel: settings.accept_unset_model_ratio_model || false, recordIpLog: settings.record_ip_log || false, @@ -214,7 +212,9 @@ const PersonalSetting = () => { return; } - const publicKey = prepareCredentialCreationOptions(data?.options || data?.publicKey || data); + const publicKey = prepareCredentialCreationOptions( + data?.options || data?.publicKey || data, + ); const credential = await navigator.credentials.create({ publicKey }); const payload = buildRegistrationResult(credential); if (!payload) { @@ -222,7 +222,10 @@ const PersonalSetting = () => { return; } - const finishRes = await API.post('/api/user/passkey/register/finish', payload); + const finishRes = await API.post( + '/api/user/passkey/register/finish', + payload, + ); if (finishRes.data.success) { showSuccess(t('Passkey 注册成功')); await loadPasskeyStatus(); diff --git a/web/src/components/settings/SystemSetting.jsx b/web/src/components/settings/SystemSetting.jsx index 780e89fb1c35..4a53ecd7b9a7 100644 --- a/web/src/components/settings/SystemSetting.jsx +++ b/web/src/components/settings/SystemSetting.jsx @@ -615,7 +615,10 @@ const SystemSetting = () => { options.push({ key: 'passkey.rp_display_name', - value: formValues['passkey.rp_display_name'] || inputs['passkey.rp_display_name'] || '', + value: + formValues['passkey.rp_display_name'] || + inputs['passkey.rp_display_name'] || + '', }); options.push({ key: 'passkey.rp_id', @@ -623,11 +626,17 @@ const SystemSetting = () => { }); options.push({ key: 'passkey.user_verification', - value: formValues['passkey.user_verification'] || inputs['passkey.user_verification'] || 'preferred', + 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'] || '', + value: + formValues['passkey.attachment_preference'] || + inputs['passkey.attachment_preference'] || + '', }); options.push({ key: 'passkey.origins', @@ -696,8 +705,15 @@ const SystemSetting = () => { + - (支持{' '} + {t('仅支持')}{' '} { > new-api-worker - ) + {' '}{t('或其兼容new-api-worker格式的其他版本')} { {t('用以支持基于 WebAuthn 的无密码登录注册')} { field="['passkey.rp_display_name']" label={t('服务显示名称')} placeholder={t('默认使用系统名称')} - extraText={t('用户注册时看到的网站名称,比如\'我的网站\'')} + extraText={t( + "用户注册时看到的网站名称,比如'我的网站'", + )} /> @@ -1078,7 +1098,9 @@ const SystemSetting = () => { field="['passkey.rp_id']" label={t('网站域名标识')} placeholder={t('例如:example.com')} - extraText={t('留空则默认使用服务器地址,注意不能携带http://或者https://')} + extraText={t( + '留空则默认使用服务器地址,注意不能携带http://或者https://', + )} /> @@ -1092,7 +1114,10 @@ const SystemSetting = () => { label={t('安全验证级别')} placeholder={t('是否要求指纹/面容等生物识别')} optionList={[ - { label: t('推荐使用(用户可选)'), value: 'preferred' }, + { + label: t('推荐使用(用户可选)'), + value: 'preferred', + }, { label: t('强制要求'), value: 'required' }, { label: t('不建议使用'), value: 'discouraged' }, ]} @@ -1109,7 +1134,9 @@ const SystemSetting = () => { { label: t('本设备内置'), value: 'platform' }, { label: t('外接设备'), value: 'cross-platform' }, ]} - extraText={t('本设备:手机指纹/面容,外接:USB安全密钥')} + extraText={t( + '本设备:手机指纹/面容,外接:USB安全密钥', + )} /> @@ -1123,7 +1150,10 @@ const SystemSetting = () => { noLabel extraText={t('仅用于开发环境,生产环境应使用 HTTPS')} onChange={(e) => - handleCheckboxChange('passkey.allow_insecure_origin', e) + handleCheckboxChange( + 'passkey.allow_insecure_origin', + e, + ) } > {t('允许不安全的 Origin(HTTP)')} @@ -1139,11 +1169,16 @@ const SystemSetting = () => { field="['passkey.origins']" label={t('允许的 Origins')} placeholder={t('填写带https的域名,逗号分隔')} - extraText={t('为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https')} + extraText={t( + '为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https', + )} /> - diff --git a/web/src/components/settings/personal/cards/AccountManagement.jsx b/web/src/components/settings/personal/cards/AccountManagement.jsx index 93a2daf892c1..d54edb93a7c8 100644 --- a/web/src/components/settings/personal/cards/AccountManagement.jsx +++ b/web/src/components/settings/personal/cards/AccountManagement.jsx @@ -91,7 +91,8 @@ const AccountManagement = ({ ); }; const isBound = (accountId) => Boolean(accountId); - const [showTelegramBindModal, setShowTelegramBindModal] = React.useState(false); + const [showTelegramBindModal, setShowTelegramBindModal] = + React.useState(false); const passkeyEnabled = passkeyStatus?.enabled; const lastUsedLabel = passkeyStatus?.last_used_at ? new Date(passkeyStatus.last_used_at).toLocaleString() @@ -236,7 +237,8 @@ const AccountManagement = ({ onGitHubOAuthClicked(status.github_client_id) } disabled={ - isBound(userState.user?.github_id) || !status.github_oauth + isBound(userState.user?.github_id) || + !status.github_oauth } > {status.github_oauth ? t('绑定') : t('未启用')} @@ -394,7 +396,8 @@ const AccountManagement = ({ onLinuxDOOAuthClicked(status.linuxdo_client_id) } disabled={ - isBound(userState.user?.linux_do_id) || !status.linuxdo_oauth + isBound(userState.user?.linux_do_id) || + !status.linuxdo_oauth } > {status.linuxdo_oauth ? t('绑定') : t('未启用')} @@ -532,7 +535,9 @@ const AccountManagement = ({ ? () => { Modal.confirm({ title: t('确认解绑 Passkey'), - content: t('解绑后将无法使用 Passkey 登录,确定要继续吗?'), + content: t( + '解绑后将无法使用 Passkey 登录,确定要继续吗?', + ), okText: t('确认解绑'), cancelText: t('取消'), okType: 'danger', @@ -544,7 +549,11 @@ const AccountManagement = ({ className={`w-full sm:w-auto ${passkeyEnabled ? '!bg-slate-500 hover:!bg-slate-600' : ''}`} icon={} disabled={!passkeySupported && !passkeyEnabled} - loading={passkeyEnabled ? passkeyDeleteLoading : passkeyRegisterLoading} + loading={ + passkeyEnabled + ? passkeyDeleteLoading + : passkeyRegisterLoading + } > {passkeyEnabled ? t('解绑 Passkey') : t('注册 Passkey')} diff --git a/web/src/components/settings/personal/cards/NotificationSettings.jsx b/web/src/components/settings/personal/cards/NotificationSettings.jsx index 0c99e28551ab..c19084a51587 100644 --- a/web/src/components/settings/personal/cards/NotificationSettings.jsx +++ b/web/src/components/settings/personal/cards/NotificationSettings.jsx @@ -621,7 +621,9 @@ const NotificationSettings = ({ }, { pattern: /^https?:\/\/.+/, - message: t('Gotify服务器地址必须以http://或https://开头'), + message: t( + 'Gotify服务器地址必须以http://或https://开头', + ), }, ]} /> @@ -678,9 +680,7 @@ const NotificationSettings = ({ '复制应用的令牌(Token)并填写到上方的应用令牌字段', )}
-
- 3. {t('填写Gotify服务器的完整URL地址')} -
+
3. {t('填写Gotify服务器的完整URL地址')}
{t('更多信息请参考')} diff --git a/web/src/components/setup/components/steps/DatabaseStep.jsx b/web/src/components/setup/components/steps/DatabaseStep.jsx index 04dd76a3cf0e..d8d1d4f9f5ad 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( + '您的数据将安全地存储在本地计算机上。所有配置、用户信息和使用记录都会自动保存,关闭应用后不会丢失。', + )} +

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

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

)} -

-

- +

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

+
+ ) : ( +
+

{t( - '建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。', + '您正在使用 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 dfbd75a43898..5fa990c0c9dd 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -91,8 +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, - 43, + 1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 35, 40, 42, 48, 43, ]); function type2secretPrompt(type) { @@ -158,7 +157,7 @@ const EditChannelModal = (props) => { is_enterprise_account: false, // 字段透传控制默认值 allow_service_tier: false, - disable_store: false, // false = 允许透传(默认开启) + disable_store: false, // false = 允许透传(默认开启) allow_safety_identifier: false, }; const [batch, setBatch] = useState(false); @@ -207,7 +206,13 @@ const EditChannelModal = (props) => { channelExtraSettings: null, }); const [currentSectionIndex, setCurrentSectionIndex] = useState(0); - const formSections = ['basicInfo', 'apiConfig', 'modelConfig', 'advancedSettings', 'channelExtraSettings']; + const formSections = [ + 'basicInfo', + 'apiConfig', + 'modelConfig', + 'advancedSettings', + 'channelExtraSettings', + ]; const formContainerRef = useRef(null); // 2FA状态更新辅助函数 @@ -267,13 +272,13 @@ const EditChannelModal = (props) => { sectionElement.scrollIntoView({ behavior: 'smooth', block: 'start', - inline: 'nearest' + inline: 'nearest', }); } }; const navigateToSection = (direction) => { - const availableSections = formSections.filter(section => { + const availableSections = formSections.filter((section) => { if (section === 'apiConfig') { return showApiConfigCard; } @@ -282,9 +287,15 @@ const EditChannelModal = (props) => { let newIndex; if (direction === 'up') { - newIndex = currentSectionIndex > 0 ? currentSectionIndex - 1 : availableSections.length - 1; + newIndex = + currentSectionIndex > 0 + ? currentSectionIndex - 1 + : availableSections.length - 1; } else { - newIndex = currentSectionIndex < availableSections.length - 1 ? currentSectionIndex + 1 : 0; + newIndex = + currentSectionIndex < availableSections.length - 1 + ? currentSectionIndex + 1 + : 0; } setCurrentSectionIndex(newIndex); @@ -408,7 +419,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); @@ -502,11 +516,13 @@ const EditChannelModal = (props) => { // 读取 Vertex 密钥格式 data.vertex_key_type = parsedSettings.vertex_key_type || 'json'; // 读取企业账户设置 - data.is_enterprise_account = parsedSettings.openrouter_enterprise === true; + 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; + data.allow_safety_identifier = + parsedSettings.allow_safety_identifier || false; } catch (error) { console.error('解析其他设置失败:', error); data.azure_responses_version = ''; @@ -683,7 +699,7 @@ const EditChannelModal = (props) => { title: t('查看渠道密钥'), description: t('为了保护账户安全,请验证您的身份。'), preferredMethod: 'passkey', // 优先使用 Passkey - } + }, ); // 如果直接返回了结果(已验证),显示密钥 @@ -929,7 +945,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; } @@ -974,7 +993,8 @@ const EditChannelModal = (props) => { // type === 20: 设置企业账户标识,无论是true还是false都要传到后端 if (localInputs.type === 20) { - settings.openrouter_enterprise = localInputs.is_enterprise_account === true; + settings.openrouter_enterprise = + localInputs.is_enterprise_account === true; } // type === 1 (OpenAI) 或 type === 14 (Claude): 设置字段透传控制(显式保存布尔值) @@ -983,7 +1003,8 @@ const EditChannelModal = (props) => { // 仅 OpenAI 渠道需要 store 和 safety_identifier if (localInputs.type === 1) { settings.disable_store = localInputs.disable_store === true; - settings.allow_safety_identifier = localInputs.allow_safety_identifier === true; + settings.allow_safety_identifier = + localInputs.allow_safety_identifier === true; } } @@ -1332,7 +1353,7 @@ const EditChannelModal = (props) => { padding: 0, display: 'flex', alignItems: 'center', - justifyContent: 'center' + justifyContent: 'center', }} title={t('上一个表单块')} /> @@ -1348,7 +1369,7 @@ const EditChannelModal = (props) => { padding: 0, display: 'flex', alignItems: 'center', - justifyContent: 'center' + justifyContent: 'center', }} title={t('下一个表单块')} /> @@ -1383,334 +1404,149 @@ const EditChannelModal = (props) => { > {() => ( -
-
formSectionRefs.current.basicInfo = el}> +
+
(formSectionRefs.current.basicInfo = el)}> {/* Header: Basic Info */}
- - - -
- - {t('基本信息')} - -
- {t('渠道的基本配置信息')} + + + +
+ + {t('基本信息')} + +
+ {t('渠道的基本配置信息')} +
-
- setChannelSearchValue(value)} - renderOptionItem={renderChannelOption} - onChange={(value) => handleInputChange('type', value)} - /> - - {inputs.type === 20 && ( - { - setIsEnterpriseAccount(value); - handleInputChange('is_enterprise_account', value); - }} - extraText={t('企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选')} - initValue={inputs.is_enterprise_account} - /> - )} - - handleInputChange('name', value)} - autoComplete='new-password' - /> - - {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') - } + filter={selectFilter} + autoClearSearchValue={false} + searchPosition='dropdown' + onSearch={(value) => setChannelSearchValue(value)} + renderOptionItem={renderChannelOption} + onChange={(value) => handleInputChange('type', value)} /> - )} - {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} + + {inputs.type === 20 && ( + { + setIsEnterpriseAccount(value); + handleInputChange('is_enterprise_account', value); + }} + extraText={t( + '企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选', + )} + initValue={inputs.is_enterprise_account} /> - ) : ( - handleInputChange('key', value)} + )} + + handleInputChange('name', value)} + autoComplete='new-password' + /> + + {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={ -
- {isEdit && - isMultiKeyChannel && - keyMode === 'append' && ( - - {t( - '追加模式:新密钥将添加到现有密钥列表的末尾', - )} - - )} - {isEdit && ( - - )} - {batchExtra} -
+ inputs.vertex_key_type === 'api_key' + ? t('API Key 模式下不支持批量创建') + : t('JSON 模式支持手动输入或上传服务账号 JSON') } - showClear /> - ) - ) : ( - <> - {inputs.type === 41 && + )} + {batch ? ( + inputs.type === 41 && (inputs.vertex_key_type || 'json') === 'json' ? ( - <> - {!batch && ( -
- - {t('密钥输入方式')} - - - - - -
- )} - - {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} - /> - )} - - ) : ( - } + dragMainText={t('点击上传文件或拖拽文件到这里')} + dragSubText={t('仅支持 JSON 文件,支持多文件')} + style={{ marginTop: 10 }} + uploadTrigger='custom' + beforeUpload={() => false} + onChange={handleVertexUploadChange} + fileList={vertexFileList} + rules={ isEdit - ? t('密钥(编辑模式下,保存的密钥不会显示)') - : t('密钥') + ? [] + : [ + { + required: true, + message: t('请上传密钥文件'), + }, + ] } - placeholder={t(type2secretPrompt(inputs.type))} + extraText={batchExtra} + /> + ) : ( + handleInputChange('key', value)} extraText={ -
+
{isEdit && isMultiKeyChannel && keyMode === 'append' && ( @@ -1735,200 +1571,501 @@ 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 && ( -
formSectionRefs.current.apiConfig = el}> +
(formSectionRefs.current.apiConfig = el)}> {/* Header: API Config */}
- - - -
- - {t('API 配置')} - -
- {t('API 地址和相关配置')} + + + +
+ + {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) } - > - https://cloud.siliconflow.cn/i/hij0YNTZ - + showClear + />
- } - className='!rounded-lg' - /> - )} +
+ + handleInputChange('other', value) + } + showClear + /> +
+
+ + handleChannelOtherSettingsChange( + 'azure_responses_version', + value, + ) + } + showClear + /> +
+ + )} - {inputs.type === 3 && ( - <> + {inputs.type === 8 && ( + <> + +
+ + handleInputChange('base_url', value) + } + showClear + /> +
+ + )} + + {inputs.type === 37 && ( + )} + + {inputs.type !== 3 && + inputs.type !== 8 && + inputs.type !== 22 && + inputs.type !== 36 && + inputs.type !== 45 && ( +
+ + handleInputChange('base_url', value) + } + showClear + extraText={t( + '对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写', + )} + /> +
+ )} + + {inputs.type === 22 && (
handleInputChange('base_url', value) @@ -1936,53 +2073,17 @@ const EditChannelModal = (props) => { showClear />
-
- - handleInputChange('other', value) - } - showClear - /> -
-
- - handleChannelOtherSettingsChange( - 'azure_responses_version', - value, - ) - } - showClear - /> -
- - )} + )} - {inputs.type === 8 && ( - <> - + {inputs.type === 36 && (
handleInputChange('base_url', value) @@ -1990,488 +2091,392 @@ const EditChannelModal = (props) => { showClear />
- - )} - - {inputs.type === 37 && ( - - )} + )} - {inputs.type !== 3 && - inputs.type !== 8 && - inputs.type !== 22 && - inputs.type !== 36 && - inputs.type !== 45 && ( + {inputs.type === 45 && (
- handleInputChange('base_url', value) } - showClear - extraText={t( - '对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写', - )} + 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' />
)} - - {inputs.type === 22 && ( -
- - handleInputChange('base_url', value) - } - showClear - /> -
- )} - - {inputs.type === 36 && ( -
- - 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 */} -
formSectionRefs.current.modelConfig = el}> +
(formSectionRefs.current.modelConfig = el)}> {/* Header: Model Config */}
- - - -
- - {t('模型配置')} - -
- {t('模型选择和映射设置')} + + + +
+ + {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 { - showError(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 { + showError(t('复制失败')); + } + }} + > + {optionNode.label || modelName} + + ), + }; + }} + extraText={ + + - - {MODEL_FETCHABLE_TYPES.has(inputs.type) && ( + {t('填入相关模型')} + + + {MODEL_FETCHABLE_TYPES.has(inputs.type) && ( + + )} + - )} - + {modelGroups && + modelGroups.length > 0 && + modelGroups.map((group) => ( + + ))} + + } + /> + + setCustomModel(value.trim())} + value={customModel} + suffix={ - {modelGroups && - modelGroups.length > 0 && - modelGroups.map((group) => ( - - ))} - - } - /> - - setCustomModel(value.trim())} - value={customModel} - suffix={ - - } - /> - - handleInputChange('test_model', value)} - showClear - /> - - - handleInputChange('model_mapping', value) - } - template={MODEL_MAPPING_EXAMPLE} - templateLabel={t('填入模板')} - editorType='keyValue' - formApi={formApiRef.current} - extraText={t('键为请求中的模型名称,值为要替换的模型名称')} - /> + } + /> + + + handleInputChange('test_model', value) + } + showClear + /> + + + handleInputChange('model_mapping', value) + } + template={MODEL_MAPPING_EXAMPLE} + templateLabel={t('填入模板')} + editorType='keyValue' + formApi={formApiRef.current} + extraText={t( + '键为请求中的模型名称,值为要替换的模型名称', + )} + />
{/* Advanced Settings Card */} -
formSectionRefs.current.advancedSettings = el}> +
(formSectionRefs.current.advancedSettings = el)} + > {/* Header: Advanced Settings */}
- - - -
- - {t('高级设置')} - -
- {t('渠道的高级配置选项')} + + + +
+ + {t('高级设置')} + +
+ {t('渠道的高级配置选项')} +
-
- handleInputChange('groups', 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('groups', value)} + /> + + handleInputChange('tag', value)} + /> + handleInputChange('remark', value)} + /> + + + + + handleInputChange('priority', value) } - > - {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, - ), - ) + style={{ width: '100%' }} + /> + + + + handleInputChange('weight', value) } - > - {t('新格式模板')} - -
- } - showClear - /> - - - handleInputChange('header_override', value) - } - extraText={ - -
-
+ style={{ width: '100%' }} + /> + + + + setAutoBan(value)} + extraText={t( + '仅当自动禁用开启时有效,关闭后不会自动禁用该渠道', + )} + initValue={autoBan} + /> + + + handleInputChange('param_override', value) + } + extraText={ +
handleInputChange( - 'header_override', + 'param_override', + JSON.stringify({ temperature: 0 }, null, 2), + ) + } + > + {t('旧格式模板')} + + + handleInputChange( + 'param_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', - 'Authorization': 'Bearer{api_key}', + operations: [ + { + path: 'temperature', + mode: 'set', + value: 0.7, + conditions: [ + { + path: 'model', + mode: 'prefix', + value: 'gpt', + }, + ], + logic: 'AND', + }, + ], }, null, 2, @@ -2479,220 +2484,281 @@ const EditChannelModal = (props) => { ) } > - {t('填入模板')} + {t('新格式模板')}
-
- - {t('支持变量:')} - -
-
{t('渠道密钥')}: {'{api_key}'}
+ } + showClear + /> + + + 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', + Authorization: 'Bearer{api_key}', + }, + 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( - '键为原状态码,值为要复写的状态码,仅影响本地判断', - )} - /> + } + showClear + /> - {/* 字段透传控制 - OpenAI 渠道 */} - {inputs.type === 1 && ( - <> -
- {t('字段透传控制')} -
+ + handleInputChange('status_code_mapping', value) + } + template={STATUS_CODE_MAPPING_EXAMPLE} + templateLabel={t('填入模板')} + editorType='keyValue' + formApi={formApiRef.current} + extraText={t( + '键为原状态码,值为要复写的状态码,仅影响本地判断', + )} + /> - - handleChannelOtherSettingsChange('allow_service_tier', value) - } - extraText={t( - 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', - )} - /> + {/* 字段透传控制 - OpenAI 渠道 */} + {inputs.type === 1 && ( + <> +
+ {t('字段透传控制')} +
- - handleChannelOtherSettingsChange('disable_store', value) - } - extraText={t( - 'store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用', - )} - /> + + handleChannelOtherSettingsChange( + 'allow_service_tier', + value, + ) + } + extraText={t( + 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', + )} + /> - - handleChannelOtherSettingsChange('allow_safety_identifier', value) - } - extraText={t( - 'safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私', - )} - /> - - )} - - {/* 字段透传控制 - Claude 渠道 */} - {(inputs.type === 14) && ( - <> -
- {t('字段透传控制')} + + handleChannelOtherSettingsChange( + 'disable_store', + value, + ) + } + extraText={t( + 'store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用', + )} + /> + + + 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 */} +
+ (formSectionRefs.current.channelExtraSettings = el) + } + > + + {/* Header: Channel Extra Settings */} +
+ + + +
+ + {t('渠道额外设置')} +
+
+ {inputs.type === 1 && ( - handleChannelOtherSettingsChange('allow_service_tier', value) + handleChannelSettingsChange('force_format', value) } extraText={t( - 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', + '强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)', )} /> - - )} -
-
+ )} - {/* Channel Extra Settings Card */} -
formSectionRefs.current.channelExtraSettings = el}> - - {/* Header: Channel Extra Settings */} -
- - - -
- - {t('渠道额外设置')} - -
-
+ + handleChannelSettingsChange( + 'thinking_to_content', + value, + ) + } + extraText={t( + '将 reasoning_content 转换为 标签拼接到内容中', + )} + /> - {inputs.type === 1 && ( - handleChannelSettingsChange('force_format', value) + handleChannelSettingsChange( + 'pass_through_body_enabled', + value, + ) } + extraText={t('启用请求体透传功能')} + /> + + + handleChannelSettingsChange('proxy', value) + } + showClear + extraText={t('用于配置网络代理,支持 socks5 协议')} + /> + + + handleChannelSettingsChange('system_prompt', value) + } + autosize + showClear extraText={t( - '强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)', + '用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置', + )} + /> + + handleChannelSettingsChange( + 'system_prompt_override', + value, + ) + } + extraText={t( + '如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面', )} /> - )} - - - handleChannelSettingsChange('thinking_to_content', value) - } - extraText={t( - '将 reasoning_content 转换为 标签拼接到内容中', - )} - /> - - - handleChannelSettingsChange( - 'pass_through_body_enabled', - value, - ) - } - extraText={t('启用请求体透传功能')} - /> - - - handleChannelSettingsChange('proxy', value) - } - showClear - extraText={t('用于配置网络代理,支持 socks5 协议')} - /> - - - handleChannelSettingsChange('system_prompt', value) - } - autosize - showClear - extraText={t( - '用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置', - )} - /> - - handleChannelSettingsChange( - 'system_prompt_override', - value, - ) - } - extraText={t( - '如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面', - )} - />
diff --git a/web/src/components/table/channels/modals/EditTagModal.jsx b/web/src/components/table/channels/modals/EditTagModal.jsx index 752ff3dc5641..0b2f17d90a83 100644 --- a/web/src/components/table/channels/modals/EditTagModal.jsx +++ b/web/src/components/table/channels/modals/EditTagModal.jsx @@ -119,8 +119,19 @@ const EditTagModal = (props) => { 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; + 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 7cc56612d4be..1879cd574ae8 100644 --- a/web/src/components/table/channels/modals/ModelTestModal.jsx +++ b/web/src/components/table/channels/modals/ModelTestModal.jsx @@ -67,9 +67,15 @@ const ModelTestModal = ({ { 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: 'gemini', + label: 'Gemini (/v1beta/models/{model}:generateContent)', + }, { value: 'jina-rerank', label: 'Jina Rerank (/rerank)' }, - { value: 'image-generation', label: t('图像生成') + ' (/v1/images/generations)' }, + { + value: 'image-generation', + label: t('图像生成') + ' (/v1/images/generations)', + }, { value: 'embeddings', label: 'Embeddings (/v1/embeddings)' }, ]; @@ -166,7 +172,13 @@ const ModelTestModal = ({ return ( -
- -
- + {modalContent} @@ -104,22 +120,24 @@ const ContentModal = ({ return (
{isLoading && ( -
- +
+
)} -
@@ -338,7 +356,29 @@ 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 = @@ -351,6 +391,35 @@ const RechargeCard = ({ 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 ( - {formatLargeNumber(preset.value)} + {formatLargeNumber(preset.value)} $ {hasDiscount && ( {t('折').includes('off') @@ -398,10 +467,11 @@ const RechargeCard = ({ margin: '4px 0', }} > - {t('实付')} {actualPay.toFixed(2)}, + {t('实付')} {symbol} + {displayActualPay.toFixed(2)}, {hasDiscount - ? `${t('节省')} ${save.toFixed(2)}` - : `${t('节省')} 0.00`} + ? `${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 558c6705009e..9054da524d51 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(); @@ -77,6 +78,9 @@ 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); @@ -488,6 +492,14 @@ const TopUp = () => { setOpenTransfer(false); }; + const handleOpenHistory = () => { + setOpenHistory(true); + }; + + const handleHistoryCancel = () => { + setOpenHistory(false); + }; + // 选择预设充值额度 const selectPresetAmount = (preset) => { setTopUpCount(preset.value); @@ -544,6 +556,13 @@ const TopUp = () => { discountRate={topupInfo?.discount?.[topUpCount] || 1.0} /> + {/* 充值账单模态框 */} + + {/* 用户信息头部 */}
@@ -580,6 +599,7 @@ const TopUp = () => { renderQuota={renderQuota} statusLoading={statusLoading} topupInfo={topupInfo} + onOpenHistory={handleOpenHistory} />
diff --git a/web/src/components/topup/modals/TopupHistoryModal.jsx b/web/src/components/topup/modals/TopupHistoryModal.jsx new file mode 100644 index 000000000000..12abfbd79e05 --- /dev/null +++ b/web/src/components/topup/modals/TopupHistoryModal.jsx @@ -0,0 +1,273 @@ +/* +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, 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 3b376ed357cd..e79e19f6a15c 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,7 +164,7 @@ export const CHANNEL_OPTIONS = [ color: 'purple', label: 'Vidu', }, - { + { value: 53, color: 'blue', label: 'SubModel', @@ -169,6 +174,11 @@ export const CHANNEL_OPTIONS = [ color: 'blue', label: '豆包视频', }, + { + value: 55, + color: 'green', + label: 'Sora', + }, ]; export const MODEL_TABLE_PAGE_SIZE = 10; diff --git a/web/src/helpers/data.js b/web/src/helpers/data.js index b894a953c318..e45aac3e9941 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/passkey.js b/web/src/helpers/passkey.js index ae62775e892f..c3f3e927d6ef 100644 --- a/web/src/helpers/passkey.js +++ b/web/src/helpers/passkey.js @@ -1,3 +1,21 @@ +/* +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); @@ -26,7 +44,11 @@ export function bufferToBase64Url(buffer) { } export function prepareCredentialCreationOptions(payload) { - const options = payload?.publicKey || payload?.PublicKey || payload?.response || payload?.Response; + const options = + payload?.publicKey || + payload?.PublicKey || + payload?.response || + payload?.Response; if (!options) { throw new Error('无法从服务端响应中解析 Passkey 注册参数'); } @@ -46,7 +68,10 @@ export function prepareCredentialCreationOptions(payload) { })); } - if (Array.isArray(options.attestationFormats) && options.attestationFormats.length === 0) { + if ( + Array.isArray(options.attestationFormats) && + options.attestationFormats.length === 0 + ) { delete publicKey.attestationFormats; } @@ -54,7 +79,11 @@ export function prepareCredentialCreationOptions(payload) { } export function prepareCredentialRequestOptions(payload) { - const options = payload?.publicKey || payload?.PublicKey || payload?.response || payload?.Response; + const options = + payload?.publicKey || + payload?.PublicKey || + payload?.response || + payload?.Response; if (!options) { throw new Error('无法从服务端响应中解析 Passkey 登录参数'); } @@ -77,7 +106,10 @@ export function buildRegistrationResult(credential) { if (!credential) return null; const { response } = credential; - const transports = typeof response.getTransports === 'function' ? response.getTransports() : undefined; + const transports = + typeof response.getTransports === 'function' + ? response.getTransports() + : undefined; return { id: credential.id, @@ -107,7 +139,9 @@ export function buildAssertionResult(assertion) { authenticatorData: bufferToBase64Url(response.authenticatorData), clientDataJSON: bufferToBase64Url(response.clientDataJSON), signature: bufferToBase64Url(response.signature), - userHandle: response.userHandle ? bufferToBase64Url(response.userHandle) : null, + userHandle: response.userHandle + ? bufferToBase64Url(response.userHandle) + : null, }, clientExtensionResults: assertion.getClientExtensionResults?.() ?? {}, }; @@ -117,15 +151,22 @@ export async function isPasskeySupported() { if (typeof window === 'undefined' || !window.PublicKeyCredential) { return false; } - if (typeof window.PublicKeyCredential.isConditionalMediationAvailable === 'function') { + if ( + typeof window.PublicKeyCredential.isConditionalMediationAvailable === + 'function' + ) { try { - const available = await window.PublicKeyCredential.isConditionalMediationAvailable(); + const available = + await window.PublicKeyCredential.isConditionalMediationAvailable(); if (available) return true; } catch (error) { // ignore } } - if (typeof window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === 'function') { + if ( + typeof window.PublicKeyCredential + .isUserVerifyingPlatformAuthenticatorAvailable === 'function' + ) { try { return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); } catch (error) { @@ -134,4 +175,3 @@ export async function isPasskeySupported() { } return true; } - diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx index 78ff8a44dce5..1c3dda854b1c 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 智脑 @@ -832,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) { @@ -889,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) { @@ -1038,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, }, ); @@ -1080,19 +1180,24 @@ export function renderModelPrice( <>

- {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, }, )} @@ -1100,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, }, )} @@ -1112,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, }, )} @@ -1124,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), })}

)} @@ -1149,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, }, @@ -1202,31 +1317,34 @@ export function renderModelPrice( const extraServices = [ webSearch && webSearchCallCount > 0 ? i18next.t( - ' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}', - { - count: webSearchCallCount, - price: webSearchPrice, - ratio: groupRatio, - ratioType: ratioLabel, - }, - ) + ' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', + { + count: webSearchCallCount, + symbol: symbol, + price: (webSearchPrice * rate).toFixed(6), + ratio: groupRatio, + ratioType: ratioLabel, + }, + ) : '', fileSearch && fileSearchCallCount > 0 ? i18next.t( - ' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}', - { - count: fileSearchCallCount, - price: fileSearchPrice, - ratio: groupRatio, - ratioType: ratioLabel, - }, - ) + ' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', + { + count: fileSearchCallCount, + symbol: symbol, + price: (fileSearchPrice * rate).toFixed(6), + ratio: groupRatio, + ratioType: ratioLabel, + }, + ) : '', 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, }, @@ -1235,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), }, ); })()} @@ -1272,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, }); @@ -1367,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, }, ); @@ -1400,25 +1528,27 @@ export function renderAudioModelPrice( let audioPrice = (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio + (audioCompletionTokens / 1000000) * - inputRatioPrice * - audioRatio * - audioCompletionRatio * - groupRatio; + inputRatioPrice * + audioRatio * + audioCompletionRatio * + groupRatio; let price = textPrice + audioPrice; return ( <>

- {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, }, )} @@ -1426,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, }, )} @@ -1437,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, }, @@ -1459,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: inputTokens - cacheTokens, - cacheInput: cacheTokens, - cachePrice: inputRatioPrice * cacheRatio, - price: inputRatioPrice, - completion: completionTokens, - compPrice: completionRatioPrice, - total: textPrice.toFixed(6), - }, - ) + '文字提示 {{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, + symbol: symbol, + cachePrice: (inputRatioPrice * cacheRatio * rate).toFixed( + 6, + ), + price: (inputRatioPrice * rate).toFixed(6), + completion: completionTokens, + compPrice: (completionRatioPrice * rate).toFixed(6), + total: (textPrice * rate).toFixed(6), + }, + ) : i18next.t( - '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', - { - input: inputTokens, - price: inputRatioPrice, - completion: completionTokens, - compPrice: completionRatioPrice, - total: textPrice.toFixed(6), - }, - )} + '文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', + { + input: inputTokens, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + completion: completionTokens, + 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), }, )}

@@ -1512,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 ''; @@ -1539,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 { @@ -1575,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, }, )} @@ -1605,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, }, )} @@ -1619,35 +1776,39 @@ 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: nonCachedTokens, - cacheInput: cacheTokens, - cacheRatio: cacheRatio, - cacheCreationInput: cacheCreationTokens, - cacheCreationRatio: cacheCreationRatio, - cachePrice: cacheRatioPrice, - cacheCreationPrice: cacheCreationRatioPrice, - price: inputRatioPrice, - completion: completionTokens, - compPrice: completionRatioPrice, - ratio: groupRatio, - ratioType: ratioLabel, - total: price.toFixed(6), - }, - ) + '提示 {{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, + symbol: symbol, + cachePrice: (cacheRatioPrice * rate).toFixed(2), + cacheCreationPrice: ( + cacheCreationRatioPrice * rate + ).toFixed(6), + price: (inputRatioPrice * rate).toFixed(6), + completion: completionTokens, + compPrice: (completionRatioPrice * rate).toFixed(6), + ratio: groupRatio, + ratioType: ratioLabel, + total: (price * rate).toFixed(6), + }, + ) : i18next.t( - '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}', - { - input: inputTokens, - price: inputRatioPrice, - completion: completionTokens, - compPrice: completionRatioPrice, - ratio: groupRatio, - ratioType: ratioLabel, - total: price.toFixed(6), - }, - )} + '提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', + { + input: inputTokens, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + completion: completionTokens, + compPrice: (completionRatioPrice * rate).toFixed(6), + ratio: groupRatio, + ratioType: ratioLabel, + total: (price * rate).toFixed(6), + }, + )}

{i18next.t('仅供参考,以实际扣费为准')}

@@ -1671,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 index b82a6ae92454..0054e04aef6f 100644 --- a/web/src/helpers/secureApiCall.js +++ b/web/src/helpers/secureApiCall.js @@ -37,7 +37,7 @@ export function isVerificationRequiredError(error) { const verificationCodes = [ 'VERIFICATION_REQUIRED', 'VERIFICATION_EXPIRED', - 'VERIFICATION_INVALID' + 'VERIFICATION_INVALID', ]; return verificationCodes.includes(data.code); @@ -57,6 +57,6 @@ export function extractVerificationInfo(error) { return { code: data.code, message: data.message || '需要安全验证', - required: true + required: true, }; -} \ No newline at end of file +} diff --git a/web/src/helpers/utils.jsx b/web/src/helpers/utils.jsx index bcd13230e6f6..0cbdebd15497 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 9437f0eff1c0..f3f99f01eb4b 100644 --- a/web/src/hooks/channels/useChannelsData.jsx +++ b/web/src/hooks/channels/useChannelsData.jsx @@ -25,9 +25,13 @@ import { showInfo, showSuccess, loadChannelModels, - copy + copy, } from '../../helpers'; -import { CHANNEL_OPTIONS, ITEMS_PER_PAGE, MODEL_TABLE_PAGE_SIZE } from '../../constants'; +import { + CHANNEL_OPTIONS, + ITEMS_PER_PAGE, + MODEL_TABLE_PAGE_SIZE, +} from '../../constants'; import { useIsMobile } from '../common/useIsMobile'; import { useTableCompactMode } from '../common/useTableCompactMode'; import { Modal } from '@douyinfe/semi-ui'; @@ -64,7 +68,7 @@ export const useChannelsData = () => { // Status filter const [statusFilter, setStatusFilter] = useState( - localStorage.getItem('channel-status-filter') || 'all' + localStorage.getItem('channel-status-filter') || 'all', ); // Type tabs states @@ -81,7 +85,7 @@ export const useChannelsData = () => { const [isBatchTesting, setIsBatchTesting] = useState(false); const [modelTablePage, setModelTablePage] = useState(1); const [selectedEndpointType, setSelectedEndpointType] = useState(''); - + // 使用 ref 来避免闭包问题,类似旧版实现 const shouldStopBatchTestingRef = useRef(false); @@ -117,9 +121,12 @@ export const useChannelsData = () => { // Initialize from localStorage useEffect(() => { const localIdSort = localStorage.getItem('id-sort') === 'true'; - const localPageSize = parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; - const localEnableTagMode = localStorage.getItem('enable-tag-mode') === 'true'; - const localEnableBatchDelete = localStorage.getItem('enable-batch-delete') === 'true'; + const localPageSize = + parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; + const localEnableTagMode = + localStorage.getItem('enable-tag-mode') === 'true'; + const localEnableBatchDelete = + localStorage.getItem('enable-batch-delete') === 'true'; setIdSort(localIdSort); setPageSize(localPageSize); @@ -177,7 +184,10 @@ export const useChannelsData = () => { // Save column preferences useEffect(() => { if (Object.keys(visibleColumns).length > 0) { - localStorage.setItem('channels-table-columns', JSON.stringify(visibleColumns)); + localStorage.setItem( + 'channels-table-columns', + JSON.stringify(visibleColumns), + ); } }, [visibleColumns]); @@ -291,14 +301,21 @@ export const useChannelsData = () => { const { searchKeyword, searchGroup, searchModel } = getFormValues(); if (searchKeyword !== '' || searchGroup !== '' || searchModel !== '') { setLoading(true); - await searchChannels(enableTagMode, typeKey, statusF, page, pageSize, idSort); + await searchChannels( + enableTagMode, + typeKey, + statusF, + page, + pageSize, + idSort, + ); setLoading(false); return; } const reqId = ++requestCounter.current; setLoading(true); - const typeParam = (typeKey !== 'all') ? `&type=${typeKey}` : ''; + const typeParam = typeKey !== 'all' ? `&type=${typeKey}` : ''; const statusParam = statusF !== 'all' ? `&status=${statusF}` : ''; const res = await API.get( `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}${statusParam}`, @@ -312,7 +329,10 @@ export const useChannelsData = () => { if (success) { const { items, total, type_counts } = data; if (type_counts) { - const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0); + const sumAll = Object.values(type_counts).reduce( + (acc, v) => acc + v, + 0, + ); setTypeCounts({ ...type_counts, all: sumAll }); } setChannelFormat(items, enableTagMode); @@ -336,11 +356,18 @@ export const useChannelsData = () => { setSearching(true); try { if (searchKeyword === '' && searchGroup === '' && searchModel === '') { - await loadChannels(page, pageSz, sortFlag, enableTagMode, typeKey, statusF); + await loadChannels( + page, + pageSz, + sortFlag, + enableTagMode, + typeKey, + statusF, + ); return; } - const typeParam = (typeKey !== 'all') ? `&type=${typeKey}` : ''; + const typeParam = typeKey !== 'all' ? `&type=${typeKey}` : ''; const statusParam = statusF !== 'all' ? `&status=${statusF}` : ''; const res = await API.get( `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${sortFlag}&tag_mode=${enableTagMode}&p=${page}&page_size=${pageSz}${typeParam}${statusParam}`, @@ -348,7 +375,10 @@ export const useChannelsData = () => { const { success, message, data } = res.data; if (success) { const { items = [], total = 0, type_counts = {} } = data; - const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0); + const sumAll = Object.values(type_counts).reduce( + (acc, v) => acc + v, + 0, + ); setTypeCounts({ ...type_counts, all: sumAll }); setChannelFormat(items, enableTagMode); setChannelCount(total); @@ -367,7 +397,14 @@ export const useChannelsData = () => { if (searchKeyword === '' && searchGroup === '' && searchModel === '') { await loadChannels(page, pageSize, idSort, enableTagMode); } else { - await searchChannels(enableTagMode, activeTypeKey, statusFilter, page, pageSize, idSort); + await searchChannels( + enableTagMode, + activeTypeKey, + statusFilter, + page, + pageSize, + idSort, + ); } }; @@ -453,9 +490,16 @@ export const useChannelsData = () => { const { searchKeyword, searchGroup, searchModel } = getFormValues(); setActivePage(page); if (searchKeyword === '' && searchGroup === '' && searchModel === '') { - loadChannels(page, pageSize, idSort, enableTagMode).then(() => { }); + loadChannels(page, pageSize, idSort, enableTagMode).then(() => {}); } else { - searchChannels(enableTagMode, activeTypeKey, statusFilter, page, pageSize, idSort); + searchChannels( + enableTagMode, + activeTypeKey, + statusFilter, + page, + pageSize, + idSort, + ); } }; @@ -471,7 +515,14 @@ export const useChannelsData = () => { showError(reason); }); } else { - searchChannels(enableTagMode, activeTypeKey, statusFilter, 1, size, idSort); + searchChannels( + enableTagMode, + activeTypeKey, + statusFilter, + 1, + size, + idSort, + ); } }; @@ -502,7 +553,10 @@ export const useChannelsData = () => { showError(res?.data?.message || t('渠道复制失败')); } } catch (error) { - showError(t('渠道复制失败: ') + (error?.response?.data?.message || error?.message || error)); + showError( + t('渠道复制失败: ') + + (error?.response?.data?.message || error?.message || error), + ); } }; @@ -541,7 +595,11 @@ export const useChannelsData = () => { data.priority = parseInt(data.priority); break; case 'weight': - if (data.weight === undefined || data.weight < 0 || data.weight === '') { + if ( + data.weight === undefined || + data.weight < 0 || + data.weight === '' + ) { showInfo('权重必须是非负整数!'); return; } @@ -684,7 +742,11 @@ export const useChannelsData = () => { const res = await API.post(`/api/channel/fix`); const { success, message, data } = res.data; if (success) { - showSuccess(t('已修复 ${success} 个通道,失败 ${fails} 个通道。').replace('${success}', data.success).replace('${fails}', data.fails)); + showSuccess( + t('已修复 ${success} 个通道,失败 ${fails} 个通道。') + .replace('${success}', data.success) + .replace('${fails}', data.fails), + ); await refresh(); } else { showError(message); @@ -701,7 +763,7 @@ export const useChannelsData = () => { } // 添加到正在测试的模型集合 - setTestingModels(prev => new Set([...prev, model])); + setTestingModels((prev) => new Set([...prev, model])); try { let url = `/api/channel/test/${record.id}?model=${model}`; @@ -718,14 +780,14 @@ export const useChannelsData = () => { const { success, message, time } = res.data; // 更新测试结果 - setModelTestResults(prev => ({ + setModelTestResults((prev) => ({ ...prev, [testKey]: { success, message, time: time || 0, - timestamp: Date.now() - } + timestamp: Date.now(), + }, })); if (success) { @@ -743,7 +805,9 @@ export const useChannelsData = () => { ); } else { showInfo( - t('通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。') + t( + '通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。', + ) .replace('${name}', record.name) .replace('${model}', model) .replace('${time.toFixed(2)}', time.toFixed(2)), @@ -755,19 +819,19 @@ export const useChannelsData = () => { } catch (error) { // 处理网络错误 const testKey = `${record.id}-${model}`; - setModelTestResults(prev => ({ + setModelTestResults((prev) => ({ ...prev, [testKey]: { success: false, message: error.message || t('网络错误'), time: 0, - timestamp: Date.now() - } + timestamp: Date.now(), + }, })); showError(`${t('模型')} ${model}: ${error.message || t('测试失败')}`); } finally { // 从正在测试的模型集合中移除 - setTestingModels(prev => { + setTestingModels((prev) => { const newSet = new Set(prev); newSet.delete(model); return newSet; @@ -782,9 +846,11 @@ export const useChannelsData = () => { return; } - const models = currentTestChannel.models.split(',').filter(model => - model.toLowerCase().includes(modelSearchKeyword.toLowerCase()) - ); + const models = currentTestChannel.models + .split(',') + .filter((model) => + model.toLowerCase().includes(modelSearchKeyword.toLowerCase()), + ); if (models.length === 0) { showError(t('没有找到匹配的模型')); @@ -795,9 +861,9 @@ export const useChannelsData = () => { shouldStopBatchTestingRef.current = false; // 重置停止标志 // 清空该渠道之前的测试结果 - setModelTestResults(prev => { + setModelTestResults((prev) => { const newResults = { ...prev }; - models.forEach(model => { + models.forEach((model) => { const testKey = `${currentTestChannel.id}-${model}`; delete newResults[testKey]; }); @@ -805,7 +871,12 @@ export const useChannelsData = () => { }); try { - showInfo(t('开始批量测试 ${count} 个模型,已清空上次结果...').replace('${count}', models.length)); + showInfo( + t('开始批量测试 ${count} 个模型,已清空上次结果...').replace( + '${count}', + models.length, + ), + ); // 提高并发数量以加快测试速度,参考旧版的并发限制 const concurrencyLimit = 5; @@ -819,13 +890,16 @@ export const useChannelsData = () => { } 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) + 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 batchPromises = batch.map((model) => + testChannel(currentTestChannel, model, selectedEndpointType), + ); const batchResults = await Promise.allSettled(batchPromises); results.push(...batchResults); @@ -837,20 +911,20 @@ export const useChannelsData = () => { // 短暂延迟避免过于频繁的请求 if (i + concurrencyLimit < models.length) { - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); } } if (!shouldStopBatchTestingRef.current) { // 等待一小段时间确保所有结果都已更新 - await new Promise(resolve => setTimeout(resolve, 300)); + await new Promise((resolve) => setTimeout(resolve, 300)); // 使用当前状态重新计算结果统计 - setModelTestResults(currentResults => { + setModelTestResults((currentResults) => { let successCount = 0; let failCount = 0; - models.forEach(model => { + models.forEach((model) => { const testKey = `${currentTestChannel.id}-${model}`; const result = currentResults[testKey]; if (result && result.success) { @@ -862,10 +936,11 @@ export const useChannelsData = () => { // 显示完成消息 setTimeout(() => { - showSuccess(t('批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}') - .replace('${success}', successCount) - .replace('${fail}', failCount) - .replace('${total}', models.length) + showSuccess( + t('批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}') + .replace('${success}', successCount) + .replace('${fail}', failCount) + .replace('${total}', models.length), ); }, 100); @@ -1053,4 +1128,4 @@ export const useChannelsData = () => { setCompactMode, setActivePage, }; -}; \ No newline at end of file +}; diff --git a/web/src/hooks/common/useSecureVerification.jsx b/web/src/hooks/common/useSecureVerification.jsx index e60a104dbc50..9109ec7d7787 100644 --- a/web/src/hooks/common/useSecureVerification.jsx +++ b/web/src/hooks/common/useSecureVerification.jsx @@ -31,11 +31,11 @@ import { isVerificationRequiredError } from '../../helpers/secureApiCall'; * @param {string} options.successMessage - 成功提示消息 * @param {boolean} options.autoReset - 验证完成后是否自动重置状态,默认为 true */ -export const useSecureVerification = ({ - onSuccess, - onError, +export const useSecureVerification = ({ + onSuccess, + onError, successMessage, - autoReset = true + autoReset = true, } = {}) => { const { t } = useTranslation(); @@ -43,7 +43,7 @@ export const useSecureVerification = ({ const [verificationMethods, setVerificationMethods] = useState({ has2FA: false, hasPasskey: false, - passkeySupported: false + passkeySupported: false, }); // 模态框状态 @@ -54,12 +54,13 @@ export const useSecureVerification = ({ method: null, // '2fa' | 'passkey' loading: false, code: '', - apiCall: null + apiCall: null, }); // 检查可用的验证方式 const checkVerificationMethods = useCallback(async () => { - const methods = await SecureVerificationService.checkAvailableVerificationMethods(); + const methods = + await SecureVerificationService.checkAvailableVerificationMethods(); setVerificationMethods(methods); return methods; }, []); @@ -75,94 +76,108 @@ export const useSecureVerification = ({ method: null, loading: false, code: '', - apiCall: null + apiCall: null, }); setIsModalVisible(false); }, []); // 开始验证流程 - const startVerification = useCallback(async (apiCall, options = {}) => { - const { preferredMethod, title, description } = options; + const startVerification = useCallback( + async (apiCall, options = {}) => { + const { preferredMethod, title, description } = options; - // 检查验证方式 - const methods = await checkVerificationMethods(); + // 检查验证方式 + const methods = await checkVerificationMethods(); - if (!methods.has2FA && !methods.hasPasskey) { - const errorMessage = t('您需要先启用两步验证或 Passkey 才能执行此操作'); - showError(errorMessage); - onError?.(new Error(errorMessage)); - return false; - } + 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'; + // 设置默认验证方式 + 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); + setVerificationState((prev) => ({ + ...prev, + method: defaultMethod, + apiCall, + title, + description, + })); + setIsModalVisible(true); - return true; - }, [checkVerificationMethods, onError, t]); + return true; + }, + [checkVerificationMethods, onError, t], + ); // 执行验证 - const executeVerification = useCallback(async (method, code = '') => { - if (!verificationState.apiCall) { - showError(t('验证配置错误')); - return; - } + const executeVerification = useCallback( + async (method, code = '') => { + if (!verificationState.apiCall) { + showError(t('验证配置错误')); + return; + } - setVerificationState(prev => ({ ...prev, loading: true })); + setVerificationState((prev) => ({ ...prev, loading: true })); - try { - // 先调用验证 API,成功后后端会设置 session - await SecureVerificationService.verify(method, code); + try { + // 先调用验证 API,成功后后端会设置 session + await SecureVerificationService.verify(method, code); - // 验证成功,调用业务 API(此时中间件会通过) - const result = await verificationState.apiCall(); + // 验证成功,调用业务 API(此时中间件会通过) + const result = await verificationState.apiCall(); - // 显示成功消息 - if (successMessage) { - showSuccess(successMessage); - } + // 显示成功消息 + if (successMessage) { + showSuccess(successMessage); + } - // 调用成功回调 - onSuccess?.(result, method); + // 调用成功回调 + onSuccess?.(result, method); - // 自动重置状态 - if (autoReset) { - resetState(); - } + // 自动重置状态 + 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]); + 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 })); + setVerificationState((prev) => ({ ...prev, code })); }, []); // 切换验证方式 const switchVerificationMethod = useCallback((method) => { - setVerificationState(prev => ({ ...prev, method, code: '' })); + setVerificationState((prev) => ({ ...prev, method, code: '' })); }, []); // 取消验证 @@ -171,20 +186,29 @@ export const useSecureVerification = ({ }, [resetState]); // 检查是否可以使用某种验证方式 - const canUseMethod = useCallback((method) => { - switch (method) { - case '2fa': - return verificationMethods.has2FA; - case 'passkey': - return verificationMethods.hasPasskey && verificationMethods.passkeySupported; - default: - return false; - } - }, [verificationMethods]); + 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) { + if ( + verificationMethods.hasPasskey && + verificationMethods.passkeySupported + ) { return 'passkey'; } if (verificationMethods.has2FA) { @@ -200,22 +224,25 @@ export const useSecureVerification = ({ * @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; + const withVerification = useCallback( + async (apiCall, options = {}) => { + try { + // 直接尝试调用 API + return await apiCall(); + } catch (error) { + // 检查是否是需要验证的错误 + if (isVerificationRequiredError(error)) { + // 自动触发验证流程 + await startVerification(apiCall, options); + // 不抛出错误,让验证模态框处理 + return null; + } + // 其他错误继续抛出 + throw error; } - // 其他错误继续抛出 - throw error; - } - }, [startVerification]); + }, + [startVerification], + ); return { // 状态 @@ -238,9 +265,10 @@ export const useSecureVerification = ({ withVerification, // 新增:自动处理验证的包装函数 // 便捷属性 - hasAnyVerificationMethod: verificationMethods.has2FA || verificationMethods.hasPasskey, + hasAnyVerificationMethod: + verificationMethods.has2FA || verificationMethods.hasPasskey, isLoading: verificationState.loading, currentMethod: verificationState.method, - code: verificationState.code + code: verificationState.code, }; -}; \ No newline at end of file +}; diff --git a/web/src/hooks/model-pricing/useModelPricingData.jsx b/web/src/hooks/model-pricing/useModelPricingData.jsx index 799cdc1367df..0a407236ddd8 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 d434e7333be7..36684c2c5baf 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 = diff --git a/web/src/hooks/users/useUsersData.jsx b/web/src/hooks/users/useUsersData.jsx index 38579c2fa940..f906be543930 100644 --- a/web/src/hooks/users/useUsersData.jsx +++ b/web/src/hooks/users/useUsersData.jsx @@ -86,7 +86,7 @@ export const useUsersData = () => { }; // Search users with keyword and group -const searchUsers = async ( + const searchUsers = async ( startIdx, pageSize, searchKeyword = null, diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 58b743e13b4c..9bd0ad2b1ad1 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -232,6 +232,7 @@ "邀请新用户奖励额度": "Referral bonus quota", "新用户使用邀请码奖励额度": "New user invitation code bonus quota", "保存额度设置": "Save Quota Settings", + "分组与模型定价设置": "Group and Model Pricing Settings", "倍率设置": "Ratio Settings", "模型倍率": "Model ratio", "为一个 JSON 文本": "Is a JSON text", @@ -244,6 +245,8 @@ "检查更新": "Check for updates", "公告": "Announcement", "在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code", + "在此输入用户协议内容,支持 Markdown & HTML 代码": "Enter user agreement content here, supports Markdown & HTML code", + "在此输入隐私政策内容,支持 Markdown & HTML 代码": "Enter privacy policy content here, supports Markdown & HTML code", "保存公告": "Save Announcement", "个性化设置": "Personalization Settings", "系统名称": "System Name", @@ -1260,6 +1263,8 @@ "仅修改展示粒度,统计精确到小时": "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", + "设置用户协议": "Set user agreement", + "设置隐私政策": "Set privacy policy", "设置 Logo": "Set Logo", "设置首页内容": "Set home page content", "设置关于": "Set about", @@ -1285,7 +1290,6 @@ "可视化倍率设置": "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", @@ -1810,7 +1814,10 @@ "自定义模型名称": "Custom model name", "启用全部密钥": "Enable all keys", "充值价格显示": "Recharge price", - "美元汇率(非充值汇率,仅用于定价页面换算)": "USD exchange rate (not recharge rate, only used for pricing page conversion)", + "自定义货币": "Custom currency", + "自定义货币符号": "Custom currency symbol", + "例如 €, £, Rp, ₩, ₹...": "For example, €, £, Rp, ₩, ₹...", + "站点额度展示类型及汇率": "Site quota display type and exchange rate", "美元汇率": "USD exchange rate", "隐藏操作项": "Hide actions", "显示操作项": "Show actions", @@ -2174,7 +2181,6 @@ "最后使用时间": "Last used time", "备份支持": "Backup support", "支持备份": "Supported", - "不支持": "Not supported", "备份状态": "Backup state", "已备份": "Backed up", "未备份": "Not backed up", @@ -2245,5 +2251,34 @@ "轮询模式必须搭配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", "common": { "changeLanguage": "Change Language" - } + }, + "充值账单": "Recharge Bills", + "订单号": "Order No.", + "支付金额": "Payment Amount", + "待支付": "Pending", + "加载失败": "Load failed", + "加载账单失败": "Failed to load bills", + "暂无充值记录": "No recharge records", + "账单": "Bills", + "补单": "Complete Order", + "补单成功": "Order completed successfully", + "补单失败": "Failed to complete order", + "确认补单": "Confirm Order Completion", + "是否将该订单标记为成功并为用户入账?": "Mark this order as successful and credit the user?", + "用户协议": "User Agreement", + "隐私政策": "Privacy Policy", + "用户协议更新失败": "Failed to update user agreement", + "隐私政策更新失败": "Failed to update privacy policy", + "管理员未设置用户协议内容": "Administrator has not set user agreement content", + "管理员未设置隐私政策内容": "Administrator has not set privacy policy content", + "加载用户协议内容失败...": "Failed to load user agreement content...", + "加载隐私政策内容失败...": "Failed to load privacy policy content...", + "我已阅读并同意": "I have read and agree to", + "和": " and ", + "请先阅读并同意用户协议和隐私政策": "Please read and agree to the user agreement and privacy policy first", + "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "After filling in the user agreement content, users will be required to check that they have read the user agreement when registering", + "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "After filling in the privacy policy content, users will be required to check that they have read the privacy policy when registering", + "管理员设置了外部链接,点击下方按钮访问": "Administrator has set an external link, click the button below to access", + "访问用户协议": "Access User Agreement", + "访问隐私政策": "Access Privacy Policy" } diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 2a160716da76..dcebda245b6f 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -231,6 +231,7 @@ "邀请新用户奖励额度": "Quota de bonus de parrainage", "新用户使用邀请码奖励额度": "Quota de bonus de code d'invitation pour nouvel utilisateur", "保存额度设置": "Enregistrer les paramètres de quota", + "分组与模型定价设置": "Paramètres de groupe et de tarification du modèle", "倍率设置": "Paramètres de ratio", "模型倍率": "Ratio de modèle", "为一个 JSON 文本": "Est un texte JSON", @@ -1806,7 +1807,10 @@ "自定义模型名称": "Nom de modèle personnalisé", "启用全部密钥": "Activer toutes les clés", "充值价格显示": "Prix de recharge", - "美元汇率(非充值汇率,仅用于定价页面换算)": "Taux de change USD (pas de taux de recharge, uniquement utilisé pour la conversion de la page de tarification)", + "站点额度展示类型及汇率": "Type d'affichage du quota du site et taux de change", + "自定义货币": "Devise personnalisée", + "自定义货币符号": "Symbole de devise personnalisé", + "例如 €, £, Rp, ₩, ₹...": "Par exemple, €, £, Rp, ₩, ₹...", "美元汇率": "Taux de change USD", "隐藏操作项": "Masquer les actions", "显示操作项": "Afficher les actions", @@ -2235,5 +2239,40 @@ "配置 Passkey": "Configurer Passkey", "重置 2FA": "Réinitialiser 2FA", "重置 Passkey": "Réinitialiser le Passkey", - "默认使用系统名称": "Le nom du système est utilisé par défaut" -} \ No newline at end of file + "默认使用系统名称": "Le nom du système est utilisé par défaut", + "充值账单": "Factures de recharge", + "订单号": "N° de commande", + "支付金额": "Montant payé", + "待支付": "En attente", + "加载失败": "Échec du chargement", + "加载账单失败": "Échec du chargement des factures", + "暂无充值记录": "Aucune recharge", + "账单": "Factures", + "补单": "Compléter la commande", + "补单成功": "Commande complétée avec succès", + "补单失败": "Échec de la complétion de la commande", + "确认补单": "Confirmer la complétion", + "是否将该订单标记为成功并为用户入账?": "Marquer cette commande comme réussie et créditer l'utilisateur ?", + "用户协议": "Accord utilisateur", + "隐私政策": "Politique de confidentialité", + "在此输入用户协议内容,支持 Markdown & HTML 代码": "Saisissez ici le contenu de l'accord utilisateur, prend en charge le code Markdown et HTML", + "在此输入隐私政策内容,支持 Markdown & HTML 代码": "Saisissez ici le contenu de la politique de confidentialité, prend en charge le code Markdown et HTML", + "设置用户协议": "Définir l'accord utilisateur", + "设置隐私政策": "Définir la politique de confidentialité", + "用户协议已更新": "L'accord utilisateur a été mis à jour", + "隐私政策已更新": "La politique de confidentialité a été mise à jour", + "用户协议更新失败": "Échec de la mise à jour de l'accord utilisateur", + "隐私政策更新失败": "Échec de la mise à jour de la politique de confidentialité", + "管理员未设置用户协议内容": "L'administrateur n'a pas défini le contenu de l'accord utilisateur", + "管理员未设置隐私政策内容": "L'administrateur n'a pas défini le contenu de la politique de confidentialité", + "加载用户协议内容失败...": "Échec du chargement du contenu de l'accord utilisateur...", + "加载隐私政策内容失败...": "Échec du chargement du contenu de la politique de confidentialité...", + "我已阅读并同意": "J'ai lu et j'accepte", + "和": " et ", + "请先阅读并同意用户协议和隐私政策": "Veuillez d'abord lire et accepter l'accord utilisateur et la politique de confidentialité", + "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "Après avoir rempli le contenu de l'accord utilisateur, les utilisateurs devront cocher qu'ils ont lu l'accord utilisateur lors de l'inscription", + "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "Après avoir rempli le contenu de la politique de confidentialité, les utilisateurs devront cocher qu'ils ont lu la politique de confidentialité lors de l'inscription", + "管理员设置了外部链接,点击下方按钮访问": "L'administrateur a défini un lien externe, cliquez sur le bouton ci-dessous pour accéder", + "访问用户协议": "Accéder à l'accord utilisateur", + "访问隐私政策": "Accéder à la politique de confidentialité" +} diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index fb6fbf990dee..8db3e215f6aa 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -94,5 +94,44 @@ "允许通过 Passkey 登录 & 认证": "允许通过 Passkey 登录 & 认证", "确认解绑 Passkey": "确认解绑 Passkey", "解绑后将无法使用 Passkey 登录,确定要继续吗?": "解绑后将无法使用 Passkey 登录,确定要继续吗?", - "确认解绑": "确认解绑" + "确认解绑": "确认解绑", + "充值账单": "充值账单", + "订单号": "订单号", + "支付金额": "支付金额", + "待支付": "待支付", + "加载失败": "加载失败", + "加载账单失败": "加载账单失败", + "暂无充值记录": "暂无充值记录", + "账单": "账单", + "支付方式": "支付方式", + "支付宝": "支付宝", + "微信": "微信", + "补单": "补单", + "补单成功": "补单成功", + "补单失败": "补单失败", + "确认补单": "确认补单", + "是否将该订单标记为成功并为用户入账?": "是否将该订单标记为成功并为用户入账?", + "操作": "操作", + "用户协议": "用户协议", + "隐私政策": "隐私政策", + "在此输入用户协议内容,支持 Markdown & HTML 代码": "在此输入用户协议内容,支持 Markdown & HTML 代码", + "在此输入隐私政策内容,支持 Markdown & HTML 代码": "在此输入隐私政策内容,支持 Markdown & HTML 代码", + "设置用户协议": "设置用户协议", + "设置隐私政策": "设置隐私政策", + "用户协议已更新": "用户协议已更新", + "隐私政策已更新": "隐私政策已更新", + "用户协议更新失败": "用户协议更新失败", + "隐私政策更新失败": "隐私政策更新失败", + "管理员未设置用户协议内容": "管理员未设置用户协议内容", + "管理员未设置隐私政策内容": "管理员未设置隐私政策内容", + "加载用户协议内容失败...": "加载用户协议内容失败...", + "加载隐私政策内容失败...": "加载隐私政策内容失败...", + "我已阅读并同意": "我已阅读并同意", + "和": "和", + "请先阅读并同意用户协议和隐私政策": "请先阅读并同意用户协议和隐私政策", + "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议", + "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策", + "管理员设置了外部链接,点击下方按钮访问": "管理员设置了外部链接,点击下方按钮访问", + "访问用户协议": "访问用户协议", + "访问隐私政策": "访问隐私政策" } diff --git a/web/src/pages/PrivacyPolicy/index.jsx b/web/src/pages/PrivacyPolicy/index.jsx new file mode 100644 index 000000000000..026290b180e8 --- /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 01591c782615..f7f309ac9868 100644 --- a/web/src/pages/Setting/Chat/SettingsChats.jsx +++ b/web/src/pages/Setting/Chat/SettingsChats.jsx @@ -227,7 +227,7 @@ export default function SettingsChats(props) { const isDuplicate = chatConfigs.some( (config) => config.name === values.name && - (!isEdit || config.id !== editingConfig.id) + (!isEdit || config.id !== editingConfig.id), ); if (isDuplicate) { diff --git a/web/src/pages/Setting/Operation/SettingsGeneral.jsx b/web/src/pages/Setting/Operation/SettingsGeneral.jsx index b8b925dcf468..fbfa0ed9995d 100644 --- a/web/src/pages/Setting/Operation/SettingsGeneral.jsx +++ b/web/src/pages/Setting/Operation/SettingsGeneral.jsx @@ -17,8 +17,19 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React, { useEffect, useState, useRef } from 'react'; -import { Banner, Button, Col, Form, Row, Spin, Modal } from '@douyinfe/semi-ui'; +import React, { useEffect, useState, useRef, useMemo } from 'react'; +import { + Banner, + Button, + Col, + Form, + Row, + Spin, + Modal, + Select, + InputGroup, + Input, +} from '@douyinfe/semi-ui'; import { compareObjects, API, @@ -35,10 +46,12 @@ export default function GeneralSettings(props) { const [inputs, setInputs] = useState({ TopUpLink: '', 'general_setting.docs_link': '', + 'general_setting.quota_display_type': 'USD', + 'general_setting.custom_currency_symbol': '¤', + 'general_setting.custom_currency_exchange_rate': '', QuotaPerUnit: '', RetryTimes: '', USDExchangeRate: '', - DisplayInCurrencyEnabled: false, DisplayTokenStatEnabled: false, DefaultCollapseSidebar: false, DemoSiteEnabled: false, @@ -88,6 +101,30 @@ export default function GeneralSettings(props) { }); } + // 计算展示在输入框中的“1 USD = X ”中的 X + const combinedRate = useMemo(() => { + const type = inputs['general_setting.quota_display_type']; + if (type === 'USD') return '1'; + if (type === 'CNY') return String(inputs['USDExchangeRate'] || ''); + if (type === 'TOKENS') return String(inputs['QuotaPerUnit'] || ''); + if (type === 'CUSTOM') + return String( + inputs['general_setting.custom_currency_exchange_rate'] || '', + ); + return ''; + }, [inputs]); + + const onCombinedRateChange = (val) => { + const type = inputs['general_setting.quota_display_type']; + if (type === 'CNY') { + handleFieldChange('USDExchangeRate')(val); + } else if (type === 'TOKENS') { + handleFieldChange('QuotaPerUnit')(val); + } else if (type === 'CUSTOM') { + handleFieldChange('general_setting.custom_currency_exchange_rate')(val); + } + }; + useEffect(() => { const currentInputs = {}; for (let key in props.options) { @@ -95,6 +132,28 @@ export default function GeneralSettings(props) { currentInputs[key] = props.options[key]; } } + // 若旧字段存在且新字段缺失,则做一次兜底映射 + if ( + currentInputs['general_setting.quota_display_type'] === undefined && + props.options?.DisplayInCurrencyEnabled !== undefined + ) { + currentInputs['general_setting.quota_display_type'] = props.options + .DisplayInCurrencyEnabled + ? 'USD' + : 'TOKENS'; + } + // 回填自定义货币相关字段(如果后端已存在) + if (props.options['general_setting.custom_currency_symbol'] !== undefined) { + currentInputs['general_setting.custom_currency_symbol'] = + props.options['general_setting.custom_currency_symbol']; + } + if ( + props.options['general_setting.custom_currency_exchange_rate'] !== + undefined + ) { + currentInputs['general_setting.custom_currency_exchange_rate'] = + props.options['general_setting.custom_currency_exchange_rate']; + } setInputs(currentInputs); setInputsRow(structuredClone(currentInputs)); refForm.current.setValues(currentInputs); @@ -130,30 +189,7 @@ export default function GeneralSettings(props) { showClear /> - {inputs.QuotaPerUnit !== '500000' && - inputs.QuotaPerUnit !== 500000 && ( -
- 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/index.jsx b/web/src/pages/Setting/index.jsx index 255ec683e7e8..1dc4fd8287b3 100644 --- a/web/src/pages/Setting/index.jsx +++ b/web/src/pages/Setting/index.jsx @@ -108,7 +108,7 @@ const Setting = () => { 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 000000000000..965a0dd54214 --- /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 index 93cdd0a4dc2d..51f871a96941 100644 --- a/web/src/services/secureVerification.js +++ b/web/src/services/secureVerification.js @@ -21,7 +21,7 @@ import { API, showError } from '../helpers'; import { prepareCredentialRequestOptions, buildAssertionResult, - isPasskeySupported + isPasskeySupported, } from '../helpers/passkey'; /** @@ -35,46 +35,54 @@ export class SecureVerificationService { */ static async checkAvailableVerificationMethods() { try { - const [twoFAResponse, passkeyResponse, passkeySupported] = await Promise.all([ - API.get('/api/user/2fa/status'), - API.get('/api/user/passkey'), - isPasskeySupported() - ]); + 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( + '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 + result: has2FA, }); - + console.log('hasPasskey calculation:', { success: passkeyResponse.data?.success, dataExists: !!passkeyResponse.data?.data, enabled: passkeyResponse.data?.data?.enabled, - result: hasPasskey + result: hasPasskey, }); const result = { has2FA, hasPasskey, - passkeySupported + passkeySupported, }; - + return result; } catch (error) { console.error('Failed to check verification methods:', error); return { has2FA: false, hasPasskey: false, - passkeySupported: false + passkeySupported: false, }; } } @@ -92,7 +100,7 @@ export class SecureVerificationService { // 调用通用验证 API,验证成功后后端会设置 session const verifyResponse = await API.post('/api/verify', { method: '2fa', - code: code.trim() + code: code.trim(), }); if (!verifyResponse.data?.success) { @@ -115,7 +123,9 @@ export class SecureVerificationService { } // 准备WebAuthn选项 - const publicKey = prepareCredentialRequestOptions(beginResponse.data.data.options); + const publicKey = prepareCredentialRequestOptions( + beginResponse.data.data.options, + ); // 执行WebAuthn验证 const credential = await navigator.credentials.get({ publicKey }); @@ -127,14 +137,17 @@ export class SecureVerificationService { const assertionResult = buildAssertionResult(credential); // 完成验证 - const finishResponse = await API.post('/api/user/passkey/verify/finish', assertionResult); + 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' + method: 'passkey', }); if (!verifyResponse.data?.success) { @@ -191,27 +204,29 @@ export const createApiCalls = { * @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; - } -}; \ No newline at end of file + 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; + }, +};