mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-04 07:43:41 +08:00 
			
		
		
		
	Compare commits
	
		
			18 Commits
		
	
	
		
			v0.6.11-al
			...
			v0.6.11-al
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					93ce6c4cd7 | ||
| 
						 | 
					4fe5ab8d09 | ||
| 
						 | 
					afbbfbbf83 | ||
| 
						 | 
					75d9d9d560 | ||
| 
						 | 
					d1af30ee5a | ||
| 
						 | 
					a3924a2353 | ||
| 
						 | 
					9af5a1d11d | ||
| 
						 | 
					57f9f7dfbb | ||
| 
						 | 
					d9f2df2baf | ||
| 
						 | 
					bdf312e5dc | ||
| 
						 | 
					1521df6551 | ||
| 
						 | 
					c67b167f4f | ||
| 
						 | 
					c351e196e6 | ||
| 
						 | 
					a316ed7abc | ||
| 
						 | 
					0895d8660e | ||
| 
						 | 
					be1ed114f4 | ||
| 
						 | 
					eb6da573a3 | ||
| 
						 | 
					0a6273fc08 | 
							
								
								
									
										64
									
								
								.github/workflows/docker-image-en.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										64
									
								
								.github/workflows/docker-image-en.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,64 +0,0 @@
 | 
			
		||||
name: Publish Docker image (English)
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    tags:
 | 
			
		||||
      - 'v*.*.*'
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
    inputs:
 | 
			
		||||
      name:
 | 
			
		||||
        description: 'reason'
 | 
			
		||||
        required: false
 | 
			
		||||
jobs:
 | 
			
		||||
  push_to_registries:
 | 
			
		||||
    name: Push Docker image to multiple registries
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    permissions:
 | 
			
		||||
      packages: write
 | 
			
		||||
      contents: read
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out the repo
 | 
			
		||||
        uses: actions/checkout@v3
 | 
			
		||||
 | 
			
		||||
      - name: Check repository URL
 | 
			
		||||
        run: |
 | 
			
		||||
          REPO_URL=$(git config --get remote.origin.url)
 | 
			
		||||
          if [[ $REPO_URL == *"pro" ]]; then
 | 
			
		||||
            exit 1
 | 
			
		||||
          fi      
 | 
			
		||||
 | 
			
		||||
      - name: Save version info
 | 
			
		||||
        run: |
 | 
			
		||||
          git describe --tags > VERSION 
 | 
			
		||||
 | 
			
		||||
      - name: Translate
 | 
			
		||||
        run: |
 | 
			
		||||
          python ./i18n/translate.py --repository_path . --json_file_path ./i18n/en.json
 | 
			
		||||
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@v2
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v2
 | 
			
		||||
 | 
			
		||||
      - name: Log in to Docker Hub
 | 
			
		||||
        uses: docker/login-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
 | 
			
		||||
      - name: Extract metadata (tags, labels) for Docker
 | 
			
		||||
        id: meta
 | 
			
		||||
        uses: docker/metadata-action@v4
 | 
			
		||||
        with:
 | 
			
		||||
          images: |
 | 
			
		||||
            justsong/one-api-en
 | 
			
		||||
 | 
			
		||||
      - name: Build and push Docker images
 | 
			
		||||
        uses: docker/build-push-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
							
								
								
									
										3
									
								
								.github/workflows/docker-image.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/docker-image.yml
									
									
									
									
										vendored
									
									
								
							@@ -62,7 +62,8 @@ jobs:
 | 
			
		||||
        uses: docker/build-push-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          #          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          platforms: linux/amd64 # TODO disable arm64 for now, because it cause error
 | 
			
		||||
          push: true
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
							
								
								
									
										45
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										45
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -4,41 +4,48 @@ WORKDIR /web
 | 
			
		||||
COPY ./VERSION .
 | 
			
		||||
COPY ./web .
 | 
			
		||||
 | 
			
		||||
WORKDIR /web/default
 | 
			
		||||
RUN npm install
 | 
			
		||||
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build
 | 
			
		||||
RUN npm install --prefix /web/default & \
 | 
			
		||||
    npm install --prefix /web/berry & \
 | 
			
		||||
    npm install --prefix /web/air & \
 | 
			
		||||
    wait
 | 
			
		||||
 | 
			
		||||
WORKDIR /web/berry
 | 
			
		||||
RUN npm install
 | 
			
		||||
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build
 | 
			
		||||
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat /web/default/VERSION) npm run build --prefix /web/default & \
 | 
			
		||||
    DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat /web/berry/VERSION) npm run build --prefix /web/berry & \
 | 
			
		||||
    DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat /web/air/VERSION) npm run build --prefix /web/air & \
 | 
			
		||||
    wait
 | 
			
		||||
 | 
			
		||||
WORKDIR /web/air
 | 
			
		||||
RUN npm install
 | 
			
		||||
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build
 | 
			
		||||
FROM golang AS builder2
 | 
			
		||||
 | 
			
		||||
FROM golang:alpine AS builder2
 | 
			
		||||
 | 
			
		||||
RUN apk add --no-cache g++
 | 
			
		||||
RUN apt-get update && apt-get install -y --no-install-recommends \
 | 
			
		||||
    build-essential \
 | 
			
		||||
    sqlite3 libsqlite3-dev \
 | 
			
		||||
    && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
ENV GO111MODULE=on \
 | 
			
		||||
    CGO_ENABLED=1 \
 | 
			
		||||
    GOOS=linux
 | 
			
		||||
    GOOS=linux \
 | 
			
		||||
    CGO_CFLAGS="-I/usr/include" \
 | 
			
		||||
    CGO_LDFLAGS="-L/usr/lib"
 | 
			
		||||
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
 | 
			
		||||
ADD go.mod go.sum ./
 | 
			
		||||
RUN go mod download
 | 
			
		||||
 | 
			
		||||
COPY . .
 | 
			
		||||
COPY --from=builder /web/build ./web/build
 | 
			
		||||
RUN go build -trimpath -ldflags "-s -w -X 'github.com/songquanpeng/one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
 | 
			
		||||
 | 
			
		||||
FROM alpine
 | 
			
		||||
RUN go build -trimpath -ldflags "-s -w -X 'github.com/songquanpeng/one-api/common.Version=$(cat VERSION)'" -o one-api
 | 
			
		||||
 | 
			
		||||
RUN apk update \
 | 
			
		||||
    && apk upgrade \
 | 
			
		||||
    && apk add --no-cache ca-certificates tzdata \
 | 
			
		||||
    && update-ca-certificates 2>/dev/null || true
 | 
			
		||||
# Final runtime image
 | 
			
		||||
FROM ubuntu:22.04
 | 
			
		||||
 | 
			
		||||
RUN apt-get update && apt-get install -y --no-install-recommends \
 | 
			
		||||
    ca-certificates tzdata bash \
 | 
			
		||||
    && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
COPY --from=builder2 /build/one-api /
 | 
			
		||||
 | 
			
		||||
EXPOSE 3000
 | 
			
		||||
WORKDIR /data
 | 
			
		||||
ENTRYPOINT ["/one-api"]
 | 
			
		||||
							
								
								
									
										7
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								go.mod
									
									
									
									
									
								
							@@ -1,6 +1,5 @@
 | 
			
		||||
module github.com/songquanpeng/one-api
 | 
			
		||||
 | 
			
		||||
// +heroku goVersion go1.18
 | 
			
		||||
go 1.20
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
@@ -27,10 +26,11 @@ require (
 | 
			
		||||
	github.com/stretchr/testify v1.9.0
 | 
			
		||||
	golang.org/x/crypto v0.31.0
 | 
			
		||||
	golang.org/x/image v0.18.0
 | 
			
		||||
	golang.org/x/sync v0.10.0
 | 
			
		||||
	google.golang.org/api v0.187.0
 | 
			
		||||
	gorm.io/driver/mysql v1.5.6
 | 
			
		||||
	gorm.io/driver/postgres v1.5.7
 | 
			
		||||
	gorm.io/driver/sqlite v1.5.5
 | 
			
		||||
	gorm.io/driver/sqlite v1.5.1
 | 
			
		||||
	gorm.io/gorm v1.25.10
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -82,7 +82,7 @@ require (
 | 
			
		||||
	github.com/kr/text v0.2.0 // indirect
 | 
			
		||||
	github.com/leodido/go-urn v1.4.0 // indirect
 | 
			
		||||
	github.com/mattn/go-isatty v0.0.20 // indirect
 | 
			
		||||
	github.com/mattn/go-sqlite3 v1.14.22 // indirect
 | 
			
		||||
	github.com/mattn/go-sqlite3 v1.14.24 // indirect
 | 
			
		||||
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 | 
			
		||||
	github.com/modern-go/reflect2 v1.0.2 // indirect
 | 
			
		||||
	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
 | 
			
		||||
@@ -99,7 +99,6 @@ require (
 | 
			
		||||
	golang.org/x/arch v0.8.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.26.0 // indirect
 | 
			
		||||
	golang.org/x/oauth2 v0.21.0 // indirect
 | 
			
		||||
	golang.org/x/sync v0.10.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.28.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.21.0 // indirect
 | 
			
		||||
	golang.org/x/time v0.5.0 // indirect
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
									
									
									
									
								
							@@ -163,8 +163,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 | 
			
		||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 | 
			
		||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
 | 
			
		||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 | 
			
		||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
 | 
			
		||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 | 
			
		||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 | 
			
		||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 | 
			
		||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 | 
			
		||||
@@ -306,8 +306,8 @@ gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
 | 
			
		||||
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
 | 
			
		||||
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
 | 
			
		||||
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
 | 
			
		||||
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
 | 
			
		||||
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
 | 
			
		||||
gorm.io/driver/sqlite v1.5.1 h1:hYyrLkAWE71bcarJDPdZNTLWtr8XrSjOWyjUYI6xdL4=
 | 
			
		||||
gorm.io/driver/sqlite v1.5.1/go.mod h1:7MZZ2Z8bqyfSQA1gYEV6MagQWj3cpUkJj9Z+d1HEMEQ=
 | 
			
		||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
 | 
			
		||||
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
 | 
			
		||||
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,14 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "axios": "^0.27.2",
 | 
			
		||||
    "history": "^5.3.0",
 | 
			
		||||
    "i18next": "^24.2.2",
 | 
			
		||||
    "i18next-browser-languagedetector": "^8.0.2",
 | 
			
		||||
    "i18next-http-backend": "^3.0.2",
 | 
			
		||||
    "marked": "^4.1.1",
 | 
			
		||||
    "react": "^18.2.0",
 | 
			
		||||
    "react-dom": "^18.2.0",
 | 
			
		||||
    "react-dropzone": "^14.2.3",
 | 
			
		||||
    "react-i18next": "^15.4.0",
 | 
			
		||||
    "react-router-dom": "^6.3.0",
 | 
			
		||||
    "react-scripts": "5.0.1",
 | 
			
		||||
    "react-toastify": "^9.0.8",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										156
									
								
								web/default/public/locales/en/translation.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								web/default/public/locales/en/translation.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,156 @@
 | 
			
		||||
{
 | 
			
		||||
  "header": {
 | 
			
		||||
    "home": "Home",
 | 
			
		||||
    "channel": "Channel",
 | 
			
		||||
    "token": "Token",
 | 
			
		||||
    "redemption": "Redemption",
 | 
			
		||||
    "topup": "Top Up",
 | 
			
		||||
    "user": "User",
 | 
			
		||||
    "dashboard": "Dashboard",
 | 
			
		||||
    "log": "Log",
 | 
			
		||||
    "setting": "Settings",
 | 
			
		||||
    "about": "About",
 | 
			
		||||
    "chat": "Chat",
 | 
			
		||||
    "login": "Login",
 | 
			
		||||
    "logout": "Logout",
 | 
			
		||||
    "register": "Register"
 | 
			
		||||
  },
 | 
			
		||||
  "topup": {
 | 
			
		||||
    "title": "Top Up Center",
 | 
			
		||||
    "get_code": {
 | 
			
		||||
      "title": "Get Redemption Code",
 | 
			
		||||
      "current_quota": "Current Available Quota",
 | 
			
		||||
      "button": "Get Code Now"
 | 
			
		||||
    },
 | 
			
		||||
    "redeem_code": {
 | 
			
		||||
      "title": "Redeem Code",
 | 
			
		||||
      "placeholder": "Please enter redemption code",
 | 
			
		||||
      "paste": "Paste",
 | 
			
		||||
      "paste_error": "Cannot access clipboard, please paste manually",
 | 
			
		||||
      "submit": "Redeem Now",
 | 
			
		||||
      "submitting": "Redeeming...",
 | 
			
		||||
      "empty_code": "Please enter the redemption code!",
 | 
			
		||||
      "success": "Top up successful!",
 | 
			
		||||
      "request_failed": "Request failed",
 | 
			
		||||
      "no_link": "Admin has not set up the top-up link!"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "channel": {
 | 
			
		||||
    "title": "Channel Management",
 | 
			
		||||
    "search": "Search channels by ID, name and key...",
 | 
			
		||||
    "balance_notice": "OpenAI channels no longer support getting balance via key, so balance shows as 0. For supported channel types, click balance to refresh.",
 | 
			
		||||
    "test_notice": "Channel testing only supports chat models, preferring gpt-3.5-turbo. If unavailable, uses the first model in your configured list.",
 | 
			
		||||
    "detail_notice": "Click the detail button below to show balance and set additional test models.",
 | 
			
		||||
    "table": {
 | 
			
		||||
      "id": "ID",
 | 
			
		||||
      "name": "Name",
 | 
			
		||||
      "group": "Group",
 | 
			
		||||
      "type": "Type",
 | 
			
		||||
      "status": "Status",
 | 
			
		||||
      "response_time": "Response Time",
 | 
			
		||||
      "balance": "Balance",
 | 
			
		||||
      "priority": "Priority",
 | 
			
		||||
      "test_model": "Test Model",
 | 
			
		||||
      "actions": "Actions",
 | 
			
		||||
      "no_name": "None",
 | 
			
		||||
      "status_enabled": "Enabled",
 | 
			
		||||
      "status_disabled": "Disabled",
 | 
			
		||||
      "status_auto_disabled": "Disabled",
 | 
			
		||||
      "status_disabled_tip": "This channel is manually disabled",
 | 
			
		||||
      "status_auto_disabled_tip": "This channel is automatically disabled",
 | 
			
		||||
      "status_unknown": "Unknown Status",
 | 
			
		||||
      "not_tested": "Not Tested",
 | 
			
		||||
      "priority_tip": "Channel selection priority, higher is preferred",
 | 
			
		||||
      "select_test_model": "Please select test model",
 | 
			
		||||
      "click_to_update": "Click to update"
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "test": "Test",
 | 
			
		||||
      "delete": "Delete",
 | 
			
		||||
      "confirm_delete": "Delete Channel",
 | 
			
		||||
      "enable": "Enable",
 | 
			
		||||
      "disable": "Disable",
 | 
			
		||||
      "edit": "Edit",
 | 
			
		||||
      "add": "Add New Channel",
 | 
			
		||||
      "test_all": "Test All Channels",
 | 
			
		||||
      "test_disabled": "Test Disabled Channels",
 | 
			
		||||
      "delete_disabled": "Delete Disabled Channels",
 | 
			
		||||
      "confirm_delete_disabled": "Confirm Delete",
 | 
			
		||||
      "refresh": "Refresh",
 | 
			
		||||
      "show_detail": "Details",
 | 
			
		||||
      "hide_detail": "Hide Details"
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "test_success": "Channel ${name} test successful, model ${model}, time ${time}s, output: ${message}",
 | 
			
		||||
      "test_all_started": "Channel testing started successfully, please refresh page to see results.",
 | 
			
		||||
      "delete_disabled_success": "Deleted all disabled channels, total: ${count}",
 | 
			
		||||
      "balance_update_success": "Channel ${name} balance updated successfully!",
 | 
			
		||||
      "all_balance_updated": "All enabled channel balances have been updated!"
 | 
			
		||||
    },
 | 
			
		||||
    "edit": {
 | 
			
		||||
      "title_edit": "Update Channel Information",
 | 
			
		||||
      "title_create": "Create New Channel",
 | 
			
		||||
      "type": "Type",
 | 
			
		||||
      "name": "Name",
 | 
			
		||||
      "name_placeholder": "Please enter name",
 | 
			
		||||
      "group": "Group",
 | 
			
		||||
      "group_placeholder": "Please select groups that can use this channel",
 | 
			
		||||
      "group_addition": "Please edit group multipliers in system settings to add new group:",
 | 
			
		||||
      "models": "Models",
 | 
			
		||||
      "models_placeholder": "Please select models supported by this channel",
 | 
			
		||||
      "model_mapping": "Model Mapping",
 | 
			
		||||
      "model_mapping_placeholder": "Optional, used to modify model names in request body. A JSON string where keys are request model names and values are target model names",
 | 
			
		||||
      "system_prompt": "System Prompt",
 | 
			
		||||
      "system_prompt_placeholder": "Optional, used to force set system prompt. Use with custom model & model mapping. First create a unique custom model name above, then map it to a natively supported model",
 | 
			
		||||
      "base_url": "Proxy",
 | 
			
		||||
      "base_url_placeholder": "Optional, used for API calls through proxy. Enter proxy address in format: https://domain.com",
 | 
			
		||||
      "key": "Key",
 | 
			
		||||
      "key_placeholder": "Please enter key",
 | 
			
		||||
      "batch": "Batch Create",
 | 
			
		||||
      "batch_placeholder": "Please enter keys, one per line",
 | 
			
		||||
      "buttons": {
 | 
			
		||||
        "cancel": "Cancel",
 | 
			
		||||
        "submit": "Submit",
 | 
			
		||||
        "fill_models": "Fill Related Models",
 | 
			
		||||
        "fill_all": "Fill All Models",
 | 
			
		||||
        "clear": "Clear All Models",
 | 
			
		||||
        "add_custom": "Add",
 | 
			
		||||
        "custom_placeholder": "Enter custom model name"
 | 
			
		||||
      },
 | 
			
		||||
      "messages": {
 | 
			
		||||
        "name_required": "Please enter channel name and key!",
 | 
			
		||||
        "models_required": "Please select at least one model!",
 | 
			
		||||
        "model_mapping_invalid": "Model mapping must be valid JSON format!",
 | 
			
		||||
        "update_success": "Channel updated successfully!",
 | 
			
		||||
        "create_success": "Channel created successfully!"
 | 
			
		||||
      },
 | 
			
		||||
      "spark_version": "Model Version",
 | 
			
		||||
      "spark_version_placeholder": "Please enter Spark model version from API URL, e.g.: v2.1",
 | 
			
		||||
      "knowledge_id": "Knowledge Base ID",
 | 
			
		||||
      "knowledge_id_placeholder": "Please enter knowledge base ID, e.g.: 123456",
 | 
			
		||||
      "plugin_param": "Plugin Parameter",
 | 
			
		||||
      "plugin_param_placeholder": "Please enter plugin parameter (X-DashScope-Plugin header value)",
 | 
			
		||||
      "coze_notice": "For Coze, model name is the Bot ID. You can add prefix `bot-`, e.g.: `bot-123456`.",
 | 
			
		||||
      "douban_notice": "For Douban, you need to go to",
 | 
			
		||||
      "douban_notice_link": "Model Inference Page",
 | 
			
		||||
      "douban_notice_2": "to create an inference endpoint, and use the endpoint name as model name, e.g.: `ep-20240608051426-tkxvl`.",
 | 
			
		||||
      "aws_region_placeholder": "region, e.g.: us-west-2",
 | 
			
		||||
      "aws_ak_placeholder": "AWS IAM Access Key",
 | 
			
		||||
      "aws_sk_placeholder": "AWS IAM Secret Key",
 | 
			
		||||
      "vertex_region_placeholder": "Vertex AI Region, e.g.: us-east5",
 | 
			
		||||
      "vertex_project_id": "Vertex AI Project ID",
 | 
			
		||||
      "vertex_project_id_placeholder": "Vertex AI Project ID",
 | 
			
		||||
      "vertex_credentials": "Google Cloud Application Default Credentials JSON",
 | 
			
		||||
      "vertex_credentials_placeholder": "Google Cloud Application Default Credentials JSON",
 | 
			
		||||
      "user_id": "User ID",
 | 
			
		||||
      "user_id_placeholder": "User ID who generated this key",
 | 
			
		||||
      "key_prompts": {
 | 
			
		||||
        "default": "Please enter the authentication key for this channel",
 | 
			
		||||
        "zhipu": "Enter in format: APIKey|SecretKey",
 | 
			
		||||
        "spark": "Enter in format: APPID|APISecret|APIKey",
 | 
			
		||||
        "fastgpt": "Enter in format: APIKey-AppId, e.g.: fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041",
 | 
			
		||||
        "tencent": "Enter in format: AppId|SecretId|SecretKey"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										156
									
								
								web/default/public/locales/zh/translation.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								web/default/public/locales/zh/translation.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,156 @@
 | 
			
		||||
{
 | 
			
		||||
  "header": {
 | 
			
		||||
    "home": "首页",
 | 
			
		||||
    "channel": "渠道",
 | 
			
		||||
    "token": "令牌",
 | 
			
		||||
    "redemption": "兑换",
 | 
			
		||||
    "topup": "充值",
 | 
			
		||||
    "user": "用户",
 | 
			
		||||
    "dashboard": "总览",
 | 
			
		||||
    "log": "日志",
 | 
			
		||||
    "setting": "设置",
 | 
			
		||||
    "about": "关于",
 | 
			
		||||
    "chat": "聊天",
 | 
			
		||||
    "login": "登录",
 | 
			
		||||
    "logout": "注销",
 | 
			
		||||
    "register": "注册"
 | 
			
		||||
  },
 | 
			
		||||
  "topup": {
 | 
			
		||||
    "title": "充值中心",
 | 
			
		||||
    "get_code": {
 | 
			
		||||
      "title": "获取兑换码",
 | 
			
		||||
      "current_quota": "当前可用额度",
 | 
			
		||||
      "button": "立即获取兑换码"
 | 
			
		||||
    },
 | 
			
		||||
    "redeem_code": {
 | 
			
		||||
      "title": "兑换码充值",
 | 
			
		||||
      "placeholder": "请输入兑换码",
 | 
			
		||||
      "paste": "粘贴",
 | 
			
		||||
      "paste_error": "无法访问剪贴板,请手动粘贴",
 | 
			
		||||
      "submit": "立即兑换",
 | 
			
		||||
      "submitting": "兑换中...",
 | 
			
		||||
      "empty_code": "请输入兑换码!",
 | 
			
		||||
      "success": "充值成功!",
 | 
			
		||||
      "request_failed": "请求失败",
 | 
			
		||||
      "no_link": "超级管理员未设置充值链接!"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "channel": {
 | 
			
		||||
    "title": "管理渠道",
 | 
			
		||||
    "search": "搜索渠道的 ID,名称和密钥 ...",
 | 
			
		||||
    "balance_notice": "OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。",
 | 
			
		||||
    "test_notice": "渠道测试仅支持 chat 模型,优先使用 gpt-3.5-turbo,如果该模型不可用则使用你所配置的模型列表中的第一个模型。",
 | 
			
		||||
    "detail_notice": "点击下方详情按钮可以显示余额以及设置额外的测试模型。",
 | 
			
		||||
    "table": {
 | 
			
		||||
      "id": "ID",
 | 
			
		||||
      "name": "名称",
 | 
			
		||||
      "group": "分组",
 | 
			
		||||
      "type": "类型",
 | 
			
		||||
      "status": "状态",
 | 
			
		||||
      "response_time": "响应时间",
 | 
			
		||||
      "balance": "余额",
 | 
			
		||||
      "priority": "优先级",
 | 
			
		||||
      "test_model": "测试模型",
 | 
			
		||||
      "actions": "操作",
 | 
			
		||||
      "no_name": "无",
 | 
			
		||||
      "status_enabled": "已启用",
 | 
			
		||||
      "status_disabled": "已禁用",
 | 
			
		||||
      "status_auto_disabled": "已禁用",
 | 
			
		||||
      "status_disabled_tip": "本渠道被手动禁用",
 | 
			
		||||
      "status_auto_disabled_tip": "本渠道被程序自动禁用",
 | 
			
		||||
      "status_unknown": "未知状态",
 | 
			
		||||
      "not_tested": "未测试",
 | 
			
		||||
      "priority_tip": "渠道选择优先级,越高越优先",
 | 
			
		||||
      "select_test_model": "请选择测试模型",
 | 
			
		||||
      "click_to_update": "点击更新"
 | 
			
		||||
    },
 | 
			
		||||
    "buttons": {
 | 
			
		||||
      "test": "测试",
 | 
			
		||||
      "delete": "删除",
 | 
			
		||||
      "confirm_delete": "删除渠道",
 | 
			
		||||
      "enable": "启用",
 | 
			
		||||
      "disable": "禁用",
 | 
			
		||||
      "edit": "编辑",
 | 
			
		||||
      "add": "添加新的渠道",
 | 
			
		||||
      "test_all": "测试所有渠道",
 | 
			
		||||
      "test_disabled": "测试禁用渠道",
 | 
			
		||||
      "delete_disabled": "删除禁用渠道",
 | 
			
		||||
      "confirm_delete_disabled": "确认删除",
 | 
			
		||||
      "refresh": "刷新",
 | 
			
		||||
      "show_detail": "详情",
 | 
			
		||||
      "hide_detail": "隐藏详情"
 | 
			
		||||
    },
 | 
			
		||||
    "messages": {
 | 
			
		||||
      "test_success": "渠道 ${name} 测试成功,模型 ${model},耗时 ${time} 秒,模型输出:${message}",
 | 
			
		||||
      "test_all_started": "已成功开始测试渠道,请刷新页面查看结果。",
 | 
			
		||||
      "delete_disabled_success": "已删除所有禁用渠道,共计 ${count} 个",
 | 
			
		||||
      "balance_update_success": "渠道 ${name} 余额更新成功!",
 | 
			
		||||
      "all_balance_updated": "已更新完毕所有已启用渠道余额!"
 | 
			
		||||
    },
 | 
			
		||||
    "edit": {
 | 
			
		||||
      "title_edit": "更新渠道信息",
 | 
			
		||||
      "title_create": "创建新的渠道",
 | 
			
		||||
      "type": "类型",
 | 
			
		||||
      "name": "名称",
 | 
			
		||||
      "name_placeholder": "请输入名称",
 | 
			
		||||
      "group": "分组",
 | 
			
		||||
      "group_placeholder": "请选择可以使用该渠道的分组",
 | 
			
		||||
      "group_addition": "请在系统设置页面编辑分组倍率以添加新的分组:",
 | 
			
		||||
      "models": "模型",
 | 
			
		||||
      "models_placeholder": "请选择该渠道所支持的模型",
 | 
			
		||||
      "model_mapping": "模型重定向",
 | 
			
		||||
      "model_mapping_placeholder": "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称",
 | 
			
		||||
      "system_prompt": "系统提示词",
 | 
			
		||||
      "system_prompt_placeholder": "此项可选,用于强制设置给定的系统提示词,请配合自定义模型 & 模型重定向使用,首先创建一个唯一的自定义模型名称并在上面填入,之后将该自定义模型重定向映射到该渠道一个原生支持的模型",
 | 
			
		||||
      "base_url": "代理",
 | 
			
		||||
      "base_url_placeholder": "此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com",
 | 
			
		||||
      "key": "密钥",
 | 
			
		||||
      "key_placeholder": "请输入密钥",
 | 
			
		||||
      "batch": "批量创建",
 | 
			
		||||
      "batch_placeholder": "请输入密钥,一行一个",
 | 
			
		||||
      "buttons": {
 | 
			
		||||
        "cancel": "取消",
 | 
			
		||||
        "submit": "提交",
 | 
			
		||||
        "fill_models": "填入相关模型",
 | 
			
		||||
        "fill_all": "填入所有模型",
 | 
			
		||||
        "clear": "清除所有模型",
 | 
			
		||||
        "add_custom": "填入",
 | 
			
		||||
        "custom_placeholder": "输入自定义模型名称"
 | 
			
		||||
      },
 | 
			
		||||
      "messages": {
 | 
			
		||||
        "name_required": "请填写渠道名称和渠道密钥!",
 | 
			
		||||
        "models_required": "请至少选择一个模型!",
 | 
			
		||||
        "model_mapping_invalid": "模型映射必须是合法的 JSON 格式!",
 | 
			
		||||
        "update_success": "渠道更新成功!",
 | 
			
		||||
        "create_success": "渠道创建成功!"
 | 
			
		||||
      },
 | 
			
		||||
      "spark_version": "模型版本",
 | 
			
		||||
      "spark_version_placeholder": "请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1",
 | 
			
		||||
      "knowledge_id": "知识库 ID",
 | 
			
		||||
      "knowledge_id_placeholder": "请输入知识库 ID,例如:123456",
 | 
			
		||||
      "plugin_param": "插件参数",
 | 
			
		||||
      "plugin_param_placeholder": "请输入插件参数,即 X-DashScope-Plugin 请求头的取值",
 | 
			
		||||
      "coze_notice": "对于 Coze 而言,模型名称即 Bot ID,你可以添加一个前缀 `bot-`,例如:`bot-123456`。",
 | 
			
		||||
      "douban_notice": "对于豆包而言,需要手动去",
 | 
			
		||||
      "douban_notice_link": "模型推理页面",
 | 
			
		||||
      "douban_notice_2": "创建推理接入点,以接入点名称作为模型名称,例如:`ep-20240608051426-tkxvl`。",
 | 
			
		||||
      "aws_region_placeholder": "region,例如:us-west-2",
 | 
			
		||||
      "aws_ak_placeholder": "AWS IAM Access Key",
 | 
			
		||||
      "aws_sk_placeholder": "AWS IAM Secret Key",
 | 
			
		||||
      "vertex_region_placeholder": "Vertex AI Region,例如:us-east5",
 | 
			
		||||
      "vertex_project_id": "Vertex AI Project ID",
 | 
			
		||||
      "vertex_project_id_placeholder": "Vertex AI Project ID",
 | 
			
		||||
      "vertex_credentials": "Google Cloud Application Default Credentials JSON",
 | 
			
		||||
      "vertex_credentials_placeholder": "Google Cloud Application Default Credentials JSON",
 | 
			
		||||
      "user_id": "User ID",
 | 
			
		||||
      "user_id_placeholder": "生成该密钥的用户 ID",
 | 
			
		||||
      "key_prompts": {
 | 
			
		||||
        "default": "请输入渠道对应的鉴权密钥",
 | 
			
		||||
        "zhipu": "按照如下格式输入:APIKey|SecretKey",
 | 
			
		||||
        "spark": "按照如下格式输入:APPID|APISecret|APIKey",
 | 
			
		||||
        "fastgpt": "按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041",
 | 
			
		||||
        "tencent": "按照如下格式输入:AppId|SecretId|SecretKey"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Dropdown,
 | 
			
		||||
@@ -31,13 +32,17 @@ function renderTimestamp(timestamp) {
 | 
			
		||||
 | 
			
		||||
let type2label = undefined;
 | 
			
		||||
 | 
			
		||||
function renderType(type) {
 | 
			
		||||
function renderType(type, t) {
 | 
			
		||||
  if (!type2label) {
 | 
			
		||||
    type2label = new Map();
 | 
			
		||||
    for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
 | 
			
		||||
      type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i];
 | 
			
		||||
    }
 | 
			
		||||
    type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
 | 
			
		||||
    type2label[0] = {
 | 
			
		||||
      value: 0,
 | 
			
		||||
      text: t('channel.table.status_unknown'),
 | 
			
		||||
      color: 'grey',
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <Label basic color={type2label[type]?.color}>
 | 
			
		||||
@@ -46,7 +51,7 @@ function renderType(type) {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderBalance(type, balance) {
 | 
			
		||||
function renderBalance(type, balance, t) {
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case 1: // OpenAI
 | 
			
		||||
      return <span>${balance.toFixed(2)}</span>;
 | 
			
		||||
@@ -67,7 +72,7 @@ function renderBalance(type, balance) {
 | 
			
		||||
    case 44: // SiliconFlow
 | 
			
		||||
      return <span>¥{balance.toFixed(2)}</span>;
 | 
			
		||||
    default:
 | 
			
		||||
      return <span>不支持</span>;
 | 
			
		||||
      return <span>{t('channel.table.balance_not_supported')}</span>;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -78,6 +83,7 @@ function isShowDetail() {
 | 
			
		||||
const promptID = 'detail';
 | 
			
		||||
 | 
			
		||||
const ChannelsTable = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const [channels, setChannels] = useState([]);
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
  const [activePage, setActivePage] = useState(1);
 | 
			
		||||
@@ -207,12 +213,12 @@ const ChannelsTable = () => {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const renderStatus = (status) => {
 | 
			
		||||
  const renderStatus = (status, t) => {
 | 
			
		||||
    switch (status) {
 | 
			
		||||
      case 1:
 | 
			
		||||
        return (
 | 
			
		||||
          <Label basic color='green'>
 | 
			
		||||
            已启用
 | 
			
		||||
            {t('channel.table.status_enabled')}
 | 
			
		||||
          </Label>
 | 
			
		||||
        );
 | 
			
		||||
      case 2:
 | 
			
		||||
@@ -220,10 +226,10 @@ const ChannelsTable = () => {
 | 
			
		||||
          <Popup
 | 
			
		||||
            trigger={
 | 
			
		||||
              <Label basic color='red'>
 | 
			
		||||
                已禁用
 | 
			
		||||
                {t('channel.table.status_disabled')}
 | 
			
		||||
              </Label>
 | 
			
		||||
            }
 | 
			
		||||
            content='本渠道被手动禁用'
 | 
			
		||||
            content={t('channel.table.status_disabled_tip')}
 | 
			
		||||
            basic
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
@@ -232,29 +238,29 @@ const ChannelsTable = () => {
 | 
			
		||||
          <Popup
 | 
			
		||||
            trigger={
 | 
			
		||||
              <Label basic color='yellow'>
 | 
			
		||||
                已禁用
 | 
			
		||||
                {t('channel.table.status_auto_disabled')}
 | 
			
		||||
              </Label>
 | 
			
		||||
            }
 | 
			
		||||
            content='本渠道被程序自动禁用'
 | 
			
		||||
            content={t('channel.table.status_auto_disabled_tip')}
 | 
			
		||||
            basic
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
      default:
 | 
			
		||||
        return (
 | 
			
		||||
          <Label basic color='grey'>
 | 
			
		||||
            未知状态
 | 
			
		||||
            {t('channel.table.status_unknown')}
 | 
			
		||||
          </Label>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const renderResponseTime = (responseTime) => {
 | 
			
		||||
  const renderResponseTime = (responseTime, t) => {
 | 
			
		||||
    let time = responseTime / 1000;
 | 
			
		||||
    time = time.toFixed(2) + ' 秒';
 | 
			
		||||
    time = time.toFixed(2) + 's';
 | 
			
		||||
    if (responseTime === 0) {
 | 
			
		||||
      return (
 | 
			
		||||
        <Label basic color='grey'>
 | 
			
		||||
          未测试
 | 
			
		||||
          {t('channel.table.not_tested')}
 | 
			
		||||
        </Label>
 | 
			
		||||
      );
 | 
			
		||||
    } else if (responseTime <= 1000) {
 | 
			
		||||
@@ -320,9 +326,12 @@ const ChannelsTable = () => {
 | 
			
		||||
      newChannels[realIdx].test_time = Date.now() / 1000;
 | 
			
		||||
      setChannels(newChannels);
 | 
			
		||||
      showInfo(
 | 
			
		||||
        `渠道 ${name} 测试成功,模型 ${model},耗时 ${time.toFixed(
 | 
			
		||||
          2
 | 
			
		||||
        )} 秒,模型输出:${message}`
 | 
			
		||||
        t('channel.messages.test_success', {
 | 
			
		||||
          name: name,
 | 
			
		||||
          model: model,
 | 
			
		||||
          time: time.toFixed(2),
 | 
			
		||||
          message: message,
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
@@ -338,7 +347,7 @@ const ChannelsTable = () => {
 | 
			
		||||
    const res = await API.get(`/api/channel/test?scope=${scope}`);
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      showInfo('已成功开始测试渠道,请刷新页面查看结果。');
 | 
			
		||||
      showInfo(t('channel.messages.test_all_started'));
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
@@ -348,7 +357,9 @@ const ChannelsTable = () => {
 | 
			
		||||
    const res = await API.delete(`/api/channel/disabled`);
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      showSuccess(`已删除所有禁用渠道,共计 ${data} 个`);
 | 
			
		||||
      showSuccess(
 | 
			
		||||
        t('channel.messages.delete_disabled_success', { count: data })
 | 
			
		||||
      );
 | 
			
		||||
      await refresh();
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
@@ -364,7 +375,7 @@ const ChannelsTable = () => {
 | 
			
		||||
      newChannels[realIdx].balance = balance;
 | 
			
		||||
      newChannels[realIdx].balance_updated_time = Date.now() / 1000;
 | 
			
		||||
      setChannels(newChannels);
 | 
			
		||||
      showInfo(`渠道 ${name} 余额更新成功!`);
 | 
			
		||||
      showInfo(t('channel.messages.balance_update_success', { name: name }));
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
@@ -375,7 +386,7 @@ const ChannelsTable = () => {
 | 
			
		||||
    const res = await API.get(`/api/channel/update_balance`);
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      showInfo('已更新完毕所有已启用渠道余额!');
 | 
			
		||||
      showInfo(t('channel.messages.all_balance_updated'));
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
@@ -413,7 +424,7 @@ const ChannelsTable = () => {
 | 
			
		||||
          icon='search'
 | 
			
		||||
          fluid
 | 
			
		||||
          iconPosition='left'
 | 
			
		||||
          placeholder='搜索渠道的 ID,名称和密钥 ...'
 | 
			
		||||
          placeholder={t('channel.search')}
 | 
			
		||||
          value={searchKeyword}
 | 
			
		||||
          loading={searching}
 | 
			
		||||
          onChange={handleKeywordChange}
 | 
			
		||||
@@ -426,13 +437,11 @@ const ChannelsTable = () => {
 | 
			
		||||
            setPromptShown(promptID);
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为
 | 
			
		||||
          0。对于支持的渠道类型,请点击余额进行刷新。
 | 
			
		||||
          {t('channel.balance_notice')}
 | 
			
		||||
          <br />
 | 
			
		||||
          渠道测试仅支持 chat 模型,优先使用
 | 
			
		||||
          gpt-3.5-turbo,如果该模型不可用则使用你所配置的模型列表中的第一个模型。
 | 
			
		||||
          {t('channel.test_notice')}
 | 
			
		||||
          <br />
 | 
			
		||||
          点击下方详情按钮可以显示余额以及设置额外的测试模型。
 | 
			
		||||
          {t('channel.detail_notice')}
 | 
			
		||||
        </Message>
 | 
			
		||||
      )}
 | 
			
		||||
      <Table basic={'very'} compact size='small'>
 | 
			
		||||
@@ -444,7 +453,7 @@ const ChannelsTable = () => {
 | 
			
		||||
                sortChannel('id');
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              ID
 | 
			
		||||
              {t('channel.table.id')}
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
            <Table.HeaderCell
 | 
			
		||||
              style={{ cursor: 'pointer' }}
 | 
			
		||||
@@ -452,7 +461,7 @@ const ChannelsTable = () => {
 | 
			
		||||
                sortChannel('name');
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              名称
 | 
			
		||||
              {t('channel.table.name')}
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
            <Table.HeaderCell
 | 
			
		||||
              style={{ cursor: 'pointer' }}
 | 
			
		||||
@@ -460,7 +469,7 @@ const ChannelsTable = () => {
 | 
			
		||||
                sortChannel('group');
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              分组
 | 
			
		||||
              {t('channel.table.group')}
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
            <Table.HeaderCell
 | 
			
		||||
              style={{ cursor: 'pointer' }}
 | 
			
		||||
@@ -468,7 +477,7 @@ const ChannelsTable = () => {
 | 
			
		||||
                sortChannel('type');
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              类型
 | 
			
		||||
              {t('channel.table.type')}
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
            <Table.HeaderCell
 | 
			
		||||
              style={{ cursor: 'pointer' }}
 | 
			
		||||
@@ -476,7 +485,7 @@ const ChannelsTable = () => {
 | 
			
		||||
                sortChannel('status');
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              状态
 | 
			
		||||
              {t('channel.table.status')}
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
            <Table.HeaderCell
 | 
			
		||||
              style={{ cursor: 'pointer' }}
 | 
			
		||||
@@ -484,7 +493,7 @@ const ChannelsTable = () => {
 | 
			
		||||
                sortChannel('response_time');
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              响应时间
 | 
			
		||||
              {t('channel.table.response_time')}
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
            <Table.HeaderCell
 | 
			
		||||
              style={{ cursor: 'pointer' }}
 | 
			
		||||
@@ -493,7 +502,7 @@ const ChannelsTable = () => {
 | 
			
		||||
              }}
 | 
			
		||||
              hidden={!showDetail}
 | 
			
		||||
            >
 | 
			
		||||
              余额
 | 
			
		||||
              {t('channel.table.balance')}
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
            <Table.HeaderCell
 | 
			
		||||
              style={{ cursor: 'pointer' }}
 | 
			
		||||
@@ -501,10 +510,12 @@ const ChannelsTable = () => {
 | 
			
		||||
                sortChannel('priority');
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              优先级
 | 
			
		||||
              {t('channel.table.priority')}
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
            <Table.HeaderCell hidden={!showDetail}>测试模型</Table.HeaderCell>
 | 
			
		||||
            <Table.HeaderCell>操作</Table.HeaderCell>
 | 
			
		||||
            <Table.HeaderCell hidden={!showDetail}>
 | 
			
		||||
              {t('channel.table.test_model')}
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
            <Table.HeaderCell>{t('channel.table.actions')}</Table.HeaderCell>
 | 
			
		||||
          </Table.Row>
 | 
			
		||||
        </Table.Header>
 | 
			
		||||
 | 
			
		||||
@@ -519,19 +530,21 @@ const ChannelsTable = () => {
 | 
			
		||||
              return (
 | 
			
		||||
                <Table.Row key={channel.id}>
 | 
			
		||||
                  <Table.Cell>{channel.id}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{channel.name ? channel.name : '无'}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    {channel.name ? channel.name : t('channel.table.no_name')}
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderGroup(channel.group)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderType(channel.type)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderStatus(channel.status)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderType(channel.type, t)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>{renderStatus(channel.status, t)}</Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    <Popup
 | 
			
		||||
                      content={
 | 
			
		||||
                        channel.test_time
 | 
			
		||||
                          ? renderTimestamp(channel.test_time)
 | 
			
		||||
                          : '未测试'
 | 
			
		||||
                          : t('channel.table.not_tested')
 | 
			
		||||
                      }
 | 
			
		||||
                      key={channel.id}
 | 
			
		||||
                      trigger={renderResponseTime(channel.response_time)}
 | 
			
		||||
                      trigger={renderResponseTime(channel.response_time, t)}
 | 
			
		||||
                      basic
 | 
			
		||||
                    />
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
@@ -544,10 +557,10 @@ const ChannelsTable = () => {
 | 
			
		||||
                          }}
 | 
			
		||||
                          style={{ cursor: 'pointer' }}
 | 
			
		||||
                        >
 | 
			
		||||
                          {renderBalance(channel.type, channel.balance)}
 | 
			
		||||
                          {renderBalance(channel.type, channel.balance, t)}
 | 
			
		||||
                        </span>
 | 
			
		||||
                      }
 | 
			
		||||
                      content='点击更新'
 | 
			
		||||
                      content={t('channel.table.click_to_update')}
 | 
			
		||||
                      basic
 | 
			
		||||
                    />
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
@@ -569,13 +582,13 @@ const ChannelsTable = () => {
 | 
			
		||||
                          <input style={{ maxWidth: '60px' }} />
 | 
			
		||||
                        </Input>
 | 
			
		||||
                      }
 | 
			
		||||
                      content='渠道选择优先级,越高越优先'
 | 
			
		||||
                      content={t('channel.table.priority_tip')}
 | 
			
		||||
                      basic
 | 
			
		||||
                    />
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell hidden={!showDetail}>
 | 
			
		||||
                    <Dropdown
 | 
			
		||||
                      placeholder='请选择测试模型'
 | 
			
		||||
                      placeholder={t('channel.table.select_test_model')}
 | 
			
		||||
                      selection
 | 
			
		||||
                      options={channel.model_options}
 | 
			
		||||
                      defaultValue={channel.test_model}
 | 
			
		||||
@@ -598,22 +611,12 @@ const ChannelsTable = () => {
 | 
			
		||||
                          );
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        测试
 | 
			
		||||
                        {t('channel.buttons.test')}
 | 
			
		||||
                      </Button>
 | 
			
		||||
                      {/*<Button*/}
 | 
			
		||||
                      {/*  size={'small'}*/}
 | 
			
		||||
                      {/*  positive*/}
 | 
			
		||||
                      {/*  loading={updatingBalance}*/}
 | 
			
		||||
                      {/*  onClick={() => {*/}
 | 
			
		||||
                      {/*    updateChannelBalance(channel.id, channel.name, idx);*/}
 | 
			
		||||
                      {/*  }}*/}
 | 
			
		||||
                      {/*>*/}
 | 
			
		||||
                      {/*  更新余额*/}
 | 
			
		||||
                      {/*</Button>*/}
 | 
			
		||||
                      <Popup
 | 
			
		||||
                        trigger={
 | 
			
		||||
                          <Button size='small' negative>
 | 
			
		||||
                            删除
 | 
			
		||||
                            {t('channel.buttons.delete')}
 | 
			
		||||
                          </Button>
 | 
			
		||||
                        }
 | 
			
		||||
                        on='click'
 | 
			
		||||
@@ -626,7 +629,7 @@ const ChannelsTable = () => {
 | 
			
		||||
                            manageChannel(channel.id, 'delete', idx);
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          删除渠道 {channel.name}
 | 
			
		||||
                          {t('channel.buttons.confirm_delete')} {channel.name}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                      </Popup>
 | 
			
		||||
                      <Button
 | 
			
		||||
@@ -639,14 +642,16 @@ const ChannelsTable = () => {
 | 
			
		||||
                          );
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        {channel.status === 1 ? '禁用' : '启用'}
 | 
			
		||||
                        {channel.status === 1
 | 
			
		||||
                          ? t('channel.buttons.disable')
 | 
			
		||||
                          : t('channel.buttons.enable')}
 | 
			
		||||
                      </Button>
 | 
			
		||||
                      <Button
 | 
			
		||||
                        size={'small'}
 | 
			
		||||
                        as={Link}
 | 
			
		||||
                        to={'/channel/edit/' + channel.id}
 | 
			
		||||
                      >
 | 
			
		||||
                        编辑
 | 
			
		||||
                        {t('channel.buttons.edit')}
 | 
			
		||||
                      </Button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
@@ -664,7 +669,7 @@ const ChannelsTable = () => {
 | 
			
		||||
                to='/channel/add'
 | 
			
		||||
                loading={loading}
 | 
			
		||||
              >
 | 
			
		||||
                添加新的渠道
 | 
			
		||||
                {t('channel.buttons.add')}
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                size='small'
 | 
			
		||||
@@ -673,7 +678,7 @@ const ChannelsTable = () => {
 | 
			
		||||
                  testChannels('all');
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                测试所有渠道
 | 
			
		||||
                {t('channel.buttons.test_all')}
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                size='small'
 | 
			
		||||
@@ -682,14 +687,12 @@ const ChannelsTable = () => {
 | 
			
		||||
                  testChannels('disabled');
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                测试禁用渠道
 | 
			
		||||
                {t('channel.buttons.test_disabled')}
 | 
			
		||||
              </Button>
 | 
			
		||||
              {/*<Button size='small' onClick={updateAllChannelsBalance}*/}
 | 
			
		||||
              {/*        loading={loading || updatingBalance}>更新已启用渠道余额</Button>*/}
 | 
			
		||||
              <Popup
 | 
			
		||||
                trigger={
 | 
			
		||||
                  <Button size='small' loading={loading}>
 | 
			
		||||
                    删除禁用渠道
 | 
			
		||||
                    {t('channel.buttons.delete_disabled')}
 | 
			
		||||
                  </Button>
 | 
			
		||||
                }
 | 
			
		||||
                on='click'
 | 
			
		||||
@@ -702,7 +705,7 @@ const ChannelsTable = () => {
 | 
			
		||||
                  negative
 | 
			
		||||
                  onClick={deleteAllDisabledChannels}
 | 
			
		||||
                >
 | 
			
		||||
                  确认删除
 | 
			
		||||
                  {t('channel.buttons.confirm_delete_disabled')}
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Popup>
 | 
			
		||||
              <Pagination
 | 
			
		||||
@@ -717,10 +720,12 @@ const ChannelsTable = () => {
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
              <Button size='small' onClick={refresh} loading={loading}>
 | 
			
		||||
                刷新
 | 
			
		||||
                {t('channel.buttons.refresh')}
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button size='small' onClick={toggleShowDetail}>
 | 
			
		||||
                {showDetail ? '隐藏详情' : '详情'}
 | 
			
		||||
                {showDetail
 | 
			
		||||
                  ? t('channel.buttons.hide_detail')
 | 
			
		||||
                  : t('channel.buttons.show_detail')}
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
          </Table.Row>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import React, { useContext, useState } from 'react';
 | 
			
		||||
import { Link, useNavigate } from 'react-router-dom';
 | 
			
		||||
import { UserContext } from '../context/User';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
@@ -23,55 +24,55 @@ import '../index.css';
 | 
			
		||||
// Header Buttons
 | 
			
		||||
let headerButtons = [
 | 
			
		||||
  {
 | 
			
		||||
    name: '首页',
 | 
			
		||||
    name: 'header.home',
 | 
			
		||||
    to: '/',
 | 
			
		||||
    icon: 'home',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '渠道',
 | 
			
		||||
    name: 'header.channel',
 | 
			
		||||
    to: '/channel',
 | 
			
		||||
    icon: 'sitemap',
 | 
			
		||||
    admin: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '令牌',
 | 
			
		||||
    name: 'header.token',
 | 
			
		||||
    to: '/token',
 | 
			
		||||
    icon: 'key',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '兑换',
 | 
			
		||||
    name: 'header.redemption',
 | 
			
		||||
    to: '/redemption',
 | 
			
		||||
    icon: 'dollar sign',
 | 
			
		||||
    admin: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '充值',
 | 
			
		||||
    name: 'header.topup',
 | 
			
		||||
    to: '/topup',
 | 
			
		||||
    icon: 'cart',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '用户',
 | 
			
		||||
    name: 'header.user',
 | 
			
		||||
    to: '/user',
 | 
			
		||||
    icon: 'user',
 | 
			
		||||
    admin: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '总览',
 | 
			
		||||
    name: 'header.dashboard',
 | 
			
		||||
    to: '/dashboard',
 | 
			
		||||
    icon: 'chart bar',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '日志',
 | 
			
		||||
    name: 'header.log',
 | 
			
		||||
    to: '/log',
 | 
			
		||||
    icon: 'book',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '设置',
 | 
			
		||||
    name: 'header.setting',
 | 
			
		||||
    to: '/setting',
 | 
			
		||||
    icon: 'setting',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: '关于',
 | 
			
		||||
    name: 'header.about',
 | 
			
		||||
    to: '/about',
 | 
			
		||||
    icon: 'info circle',
 | 
			
		||||
  },
 | 
			
		||||
@@ -79,13 +80,14 @@ let headerButtons = [
 | 
			
		||||
 | 
			
		||||
if (localStorage.getItem('chat_link')) {
 | 
			
		||||
  headerButtons.splice(1, 0, {
 | 
			
		||||
    name: '聊天',
 | 
			
		||||
    name: 'header.chat',
 | 
			
		||||
    to: '/chat',
 | 
			
		||||
    icon: 'comments',
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Header = () => {
 | 
			
		||||
  const { t, i18n } = useTranslation();
 | 
			
		||||
  const [userState, userDispatch] = useContext(UserContext);
 | 
			
		||||
  let navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
@@ -112,13 +114,14 @@ const Header = () => {
 | 
			
		||||
      if (isMobile) {
 | 
			
		||||
        return (
 | 
			
		||||
          <Menu.Item
 | 
			
		||||
            key={button.name}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              navigate(button.to);
 | 
			
		||||
              setShowSidebar(false);
 | 
			
		||||
            }}
 | 
			
		||||
            style={{ fontSize: '15px' }}
 | 
			
		||||
          >
 | 
			
		||||
            {button.name}
 | 
			
		||||
            {t(button.name)}
 | 
			
		||||
          </Menu.Item>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
@@ -134,12 +137,22 @@ const Header = () => {
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Icon name={button.icon} style={{ marginRight: '4px' }} />
 | 
			
		||||
          {button.name}
 | 
			
		||||
          {t(button.name)}
 | 
			
		||||
        </Menu.Item>
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Add language switcher dropdown
 | 
			
		||||
  const languageOptions = [
 | 
			
		||||
    { key: 'zh', text: '中文', value: 'zh' },
 | 
			
		||||
    { key: 'en', text: 'English', value: 'en' },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const changeLanguage = (language) => {
 | 
			
		||||
    i18n.changeLanguage(language);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (isMobile()) {
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
@@ -175,10 +188,18 @@ const Header = () => {
 | 
			
		||||
          <Segment style={{ marginTop: 0, borderTop: '0' }}>
 | 
			
		||||
            <Menu secondary vertical style={{ width: '100%', margin: 0 }}>
 | 
			
		||||
              {renderButtons(true)}
 | 
			
		||||
              <Menu.Item>
 | 
			
		||||
                <Dropdown
 | 
			
		||||
                  selection
 | 
			
		||||
                  options={languageOptions}
 | 
			
		||||
                  value={i18n.language}
 | 
			
		||||
                  onChange={(_, { value }) => changeLanguage(value)}
 | 
			
		||||
                />
 | 
			
		||||
              </Menu.Item>
 | 
			
		||||
              <Menu.Item>
 | 
			
		||||
                {userState.user ? (
 | 
			
		||||
                  <Button onClick={logout} style={{ color: '#666666' }}>
 | 
			
		||||
                    注销
 | 
			
		||||
                    {t('header.logout')}
 | 
			
		||||
                  </Button>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <>
 | 
			
		||||
@@ -188,7 +209,7 @@ const Header = () => {
 | 
			
		||||
                        navigate('/login');
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      登录
 | 
			
		||||
                      {t('header.login')}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                    <Button
 | 
			
		||||
                      onClick={() => {
 | 
			
		||||
@@ -196,7 +217,7 @@ const Header = () => {
 | 
			
		||||
                        navigate('/register');
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      注册
 | 
			
		||||
                      {t('header.register')}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </>
 | 
			
		||||
                )}
 | 
			
		||||
@@ -235,6 +256,17 @@ const Header = () => {
 | 
			
		||||
          </Menu.Item>
 | 
			
		||||
          {renderButtons(false)}
 | 
			
		||||
          <Menu.Menu position='right'>
 | 
			
		||||
            <Dropdown
 | 
			
		||||
              item
 | 
			
		||||
              options={languageOptions}
 | 
			
		||||
              value={i18n.language}
 | 
			
		||||
              onChange={(_, { value }) => changeLanguage(value)}
 | 
			
		||||
              style={{
 | 
			
		||||
                fontSize: '15px',
 | 
			
		||||
                fontWeight: '400',
 | 
			
		||||
                color: '#666',
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
            {userState.user ? (
 | 
			
		||||
              <Dropdown
 | 
			
		||||
                text={userState.user.username}
 | 
			
		||||
@@ -255,13 +287,13 @@ const Header = () => {
 | 
			
		||||
                      color: '#666',
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    注销
 | 
			
		||||
                    {t('header.logout')}
 | 
			
		||||
                  </Dropdown.Item>
 | 
			
		||||
                </Dropdown.Menu>
 | 
			
		||||
              </Dropdown>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <Menu.Item
 | 
			
		||||
                name='登录'
 | 
			
		||||
                name={t('header.login')}
 | 
			
		||||
                as={Link}
 | 
			
		||||
                to='/login'
 | 
			
		||||
                className='btn btn-link'
 | 
			
		||||
 
 | 
			
		||||
@@ -240,6 +240,44 @@ const LoginForm = () => {
 | 
			
		||||
            )}
 | 
			
		||||
          </Card.Content>
 | 
			
		||||
        </Card>
 | 
			
		||||
        <Modal
 | 
			
		||||
          onClose={() => setShowWeChatLoginModal(false)}
 | 
			
		||||
          onOpen={() => setShowWeChatLoginModal(true)}
 | 
			
		||||
          open={showWeChatLoginModal}
 | 
			
		||||
          size={'mini'}
 | 
			
		||||
        >
 | 
			
		||||
          <Modal.Content>
 | 
			
		||||
            <Modal.Description>
 | 
			
		||||
              <Image src={status.wechat_qrcode} fluid />
 | 
			
		||||
              <div style={{ textAlign: 'center' }}>
 | 
			
		||||
                <p>
 | 
			
		||||
                  微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
 | 
			
		||||
                </p>
 | 
			
		||||
              </div>
 | 
			
		||||
              <Form size='large'>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  fluid
 | 
			
		||||
                  placeholder='验证码'
 | 
			
		||||
                  name='wechat_verification_code'
 | 
			
		||||
                  value={inputs.wechat_verification_code}
 | 
			
		||||
                  onChange={handleChange}
 | 
			
		||||
                />
 | 
			
		||||
                <Button
 | 
			
		||||
                  fluid
 | 
			
		||||
                  size='large'
 | 
			
		||||
                  style={{
 | 
			
		||||
                    background: '#2F73FF', // 使用更现代的蓝色
 | 
			
		||||
                    color: 'white',
 | 
			
		||||
                    marginBottom: '1.5em',
 | 
			
		||||
                  }}
 | 
			
		||||
                  onClick={onSubmitWeChatVerificationCode}
 | 
			
		||||
                >
 | 
			
		||||
                  登录
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Form>
 | 
			
		||||
            </Modal.Description>
 | 
			
		||||
          </Modal.Content>
 | 
			
		||||
        </Modal>
 | 
			
		||||
      </Grid.Column>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -171,7 +171,7 @@ const RegisterForm = () => {
 | 
			
		||||
                      <Button
 | 
			
		||||
                        onClick={sendVerificationCode}
 | 
			
		||||
                        disabled={loading}
 | 
			
		||||
                        style={{ backgroundColor: '#2185d0', color: 'white' }}
 | 
			
		||||
                        // style={{ backgroundColor: '#2F73FF', color: 'white' }}
 | 
			
		||||
                      >
 | 
			
		||||
                        获取验证码
 | 
			
		||||
                      </Button>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								web/default/src/i18n.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								web/default/src/i18n.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import i18n from 'i18next';
 | 
			
		||||
import { initReactI18next } from 'react-i18next';
 | 
			
		||||
import Backend from 'i18next-http-backend';
 | 
			
		||||
import LanguageDetector from 'i18next-browser-languagedetector';
 | 
			
		||||
 | 
			
		||||
i18n
 | 
			
		||||
  .use(Backend)
 | 
			
		||||
  .use(LanguageDetector)
 | 
			
		||||
  .use(initReactI18next)
 | 
			
		||||
  .init({
 | 
			
		||||
    fallbackLng: 'zh',
 | 
			
		||||
    debug: process.env.NODE_ENV === 'development',
 | 
			
		||||
 | 
			
		||||
    interpolation: {
 | 
			
		||||
      escapeValue: false,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    backend: {
 | 
			
		||||
      loadPath: '/locales/{{lng}}/{{ns}}.json',
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export default i18n;
 | 
			
		||||
@@ -11,6 +11,7 @@ import { UserProvider } from './context/User';
 | 
			
		||||
import { ToastContainer } from 'react-toastify';
 | 
			
		||||
import 'react-toastify/dist/ReactToastify.css';
 | 
			
		||||
import { StatusProvider } from './context/Status';
 | 
			
		||||
import './i18n';
 | 
			
		||||
 | 
			
		||||
const root = ReactDOM.createRoot(document.getElementById('root'));
 | 
			
		||||
root.render(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Card } from 'semantic-ui-react';
 | 
			
		||||
import { Card, Header, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { API, showError } from '../../helpers';
 | 
			
		||||
import { marked } from 'marked';
 | 
			
		||||
 | 
			
		||||
@@ -7,39 +7,58 @@ const About = () => {
 | 
			
		||||
  const [about, setAbout] = useState('');
 | 
			
		||||
  const [aboutLoaded, setAboutLoaded] = useState(false);
 | 
			
		||||
 | 
			
		||||
  // ... 其他函数保持不变 ...
 | 
			
		||||
  const displayAbout = async () => {
 | 
			
		||||
    setAbout(localStorage.getItem('about') || '');
 | 
			
		||||
    const res = await API.get('/api/about');
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      let aboutContent = data;
 | 
			
		||||
      if (!data.startsWith('https://')) {
 | 
			
		||||
        aboutContent = marked.parse(data);
 | 
			
		||||
      }
 | 
			
		||||
      setAbout(aboutContent);
 | 
			
		||||
      localStorage.setItem('about', aboutContent);
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
      setAbout('加载关于内容失败...');
 | 
			
		||||
    }
 | 
			
		||||
    setAboutLoaded(true);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    displayAbout().then();
 | 
			
		||||
  }, []);
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='dashboard-container'>
 | 
			
		||||
      <Card fluid className='chart-card'>
 | 
			
		||||
        <Card.Content>
 | 
			
		||||
          <Card.Header className='header'>关于系统</Card.Header>
 | 
			
		||||
          {aboutLoaded && about === '' ? (
 | 
			
		||||
            <>
 | 
			
		||||
    <>
 | 
			
		||||
      {aboutLoaded && about === '' ? (
 | 
			
		||||
        <div className='dashboard-container'>
 | 
			
		||||
          <Card fluid className='chart-card'>
 | 
			
		||||
            <Card.Content>
 | 
			
		||||
              <Card.Header className='header'>关于系统</Card.Header>
 | 
			
		||||
              <p>可在设置页面设置关于内容,支持 HTML & Markdown</p>
 | 
			
		||||
              项目仓库地址:
 | 
			
		||||
              <a href='https://github.com/songquanpeng/one-api'>
 | 
			
		||||
                https://github.com/songquanpeng/one-api
 | 
			
		||||
              </a>
 | 
			
		||||
            </>
 | 
			
		||||
            </Card.Content>
 | 
			
		||||
          </Card>
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          {about.startsWith('https://') ? (
 | 
			
		||||
            <iframe
 | 
			
		||||
              src={about}
 | 
			
		||||
              style={{ width: '100%', height: '100vh', border: 'none' }}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <>
 | 
			
		||||
              {about.startsWith('https://') ? (
 | 
			
		||||
                <iframe
 | 
			
		||||
                  src={about}
 | 
			
		||||
                  style={{ width: '100%', height: '100vh', border: 'none' }}
 | 
			
		||||
                />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <div
 | 
			
		||||
                  style={{ fontSize: 'larger' }}
 | 
			
		||||
                  dangerouslySetInnerHTML={{ __html: about }}
 | 
			
		||||
                ></div>
 | 
			
		||||
              )}
 | 
			
		||||
            </>
 | 
			
		||||
            <div
 | 
			
		||||
              style={{ fontSize: 'larger' }}
 | 
			
		||||
              dangerouslySetInnerHTML={{ __html: about }}
 | 
			
		||||
            ></div>
 | 
			
		||||
          )}
 | 
			
		||||
        </Card.Content>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
@@ -26,23 +27,23 @@ const MODEL_MAPPING_EXAMPLE = {
 | 
			
		||||
  'gpt-4-32k-0314': 'gpt-4-32k',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function type2secretPrompt(type) {
 | 
			
		||||
  // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
 | 
			
		||||
function type2secretPrompt(type, t) {
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case 15:
 | 
			
		||||
      return '按照如下格式输入:APIKey|SecretKey';
 | 
			
		||||
      return t('channel.edit.key_prompts.zhipu');
 | 
			
		||||
    case 18:
 | 
			
		||||
      return '按照如下格式输入:APPID|APISecret|APIKey';
 | 
			
		||||
      return t('channel.edit.key_prompts.spark');
 | 
			
		||||
    case 22:
 | 
			
		||||
      return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
 | 
			
		||||
      return t('channel.edit.key_prompts.fastgpt');
 | 
			
		||||
    case 23:
 | 
			
		||||
      return '按照如下格式输入:AppId|SecretId|SecretKey';
 | 
			
		||||
      return t('channel.edit.key_prompts.tencent');
 | 
			
		||||
    default:
 | 
			
		||||
      return '请输入渠道对应的鉴权密钥';
 | 
			
		||||
      return t('channel.edit.key_prompts.default');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const EditChannel = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const params = useParams();
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const channelId = params.id;
 | 
			
		||||
@@ -194,15 +195,15 @@ const EditChannel = () => {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (!isEdit && (inputs.name === '' || inputs.key === '')) {
 | 
			
		||||
      showInfo('请填写渠道名称和渠道密钥!');
 | 
			
		||||
      showInfo(t('channel.edit.messages.name_required'));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (inputs.type !== 43 && inputs.models.length === 0) {
 | 
			
		||||
      showInfo('请至少选择一个模型!');
 | 
			
		||||
      showInfo(t('channel.edit.messages.models_required'));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
 | 
			
		||||
      showInfo('模型映射必须是合法的 JSON 格式!');
 | 
			
		||||
      showInfo(t('channel.edit.messages.model_mapping_invalid'));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    let localInputs = { ...inputs };
 | 
			
		||||
@@ -230,9 +231,9 @@ const EditChannel = () => {
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      if (isEdit) {
 | 
			
		||||
        showSuccess('渠道更新成功!');
 | 
			
		||||
        showSuccess(t('channel.edit.messages.update_success'));
 | 
			
		||||
      } else {
 | 
			
		||||
        showSuccess('渠道创建成功!');
 | 
			
		||||
        showSuccess(t('channel.edit.messages.create_success'));
 | 
			
		||||
        setInputs(originInputs);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -263,12 +264,14 @@ const EditChannel = () => {
 | 
			
		||||
      <Card fluid className='chart-card'>
 | 
			
		||||
        <Card.Content>
 | 
			
		||||
          <Card.Header className='header'>
 | 
			
		||||
            {isEdit ? '更新渠道信息' : '创建新的渠道'}
 | 
			
		||||
            {isEdit
 | 
			
		||||
              ? t('channel.edit.title_edit')
 | 
			
		||||
              : t('channel.edit.title_create')}
 | 
			
		||||
          </Card.Header>
 | 
			
		||||
          <Form loading={loading} autoComplete='new-password'>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Select
 | 
			
		||||
                label='类型'
 | 
			
		||||
                label={t('channel.edit.type')}
 | 
			
		||||
                name='type'
 | 
			
		||||
                required
 | 
			
		||||
                search
 | 
			
		||||
@@ -277,6 +280,35 @@ const EditChannel = () => {
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label={t('channel.edit.name')}
 | 
			
		||||
                name='name'
 | 
			
		||||
                placeholder={t('channel.edit.name_placeholder')}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.name}
 | 
			
		||||
                required
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Dropdown
 | 
			
		||||
                label={t('channel.edit.group')}
 | 
			
		||||
                placeholder={t('channel.edit.group_placeholder')}
 | 
			
		||||
                name='groups'
 | 
			
		||||
                required
 | 
			
		||||
                fluid
 | 
			
		||||
                multiple
 | 
			
		||||
                selection
 | 
			
		||||
                allowAdditions
 | 
			
		||||
                additionLabel={t('channel.edit.group_addition')}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.groups}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
                options={groupOptions}
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
 | 
			
		||||
            {/* Azure OpenAI specific fields */}
 | 
			
		||||
            {inputs.type === 3 && (
 | 
			
		||||
              <>
 | 
			
		||||
                <Message>
 | 
			
		||||
@@ -295,9 +327,7 @@ const EditChannel = () => {
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    label='AZURE_OPENAI_ENDPOINT'
 | 
			
		||||
                    name='base_url'
 | 
			
		||||
                    placeholder={
 | 
			
		||||
                      '请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
 | 
			
		||||
                    }
 | 
			
		||||
                    placeholder='请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.base_url}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
@@ -307,9 +337,7 @@ const EditChannel = () => {
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    label='默认 API 版本'
 | 
			
		||||
                    name='other'
 | 
			
		||||
                    placeholder={
 | 
			
		||||
                      '请输入默认 API 版本,例如:2024-03-01-preview,该配置可以被实际的请求查询参数所覆盖'
 | 
			
		||||
                    }
 | 
			
		||||
                    placeholder='请输入默认 API 版本,例如:2024-03-01-preview,该配置可以被实际的请求查询参数所覆盖'
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.other}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
@@ -317,55 +345,27 @@ const EditChannel = () => {
 | 
			
		||||
                </Form.Field>
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            {/* Custom base URL field */}
 | 
			
		||||
            {inputs.type === 8 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='Base URL'
 | 
			
		||||
                  label={t('channel.edit.base_url')}
 | 
			
		||||
                  name='base_url'
 | 
			
		||||
                  placeholder={
 | 
			
		||||
                    '请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'
 | 
			
		||||
                  }
 | 
			
		||||
                  placeholder={t('channel.edit.base_url_placeholder')}
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.base_url}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )}
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='名称'
 | 
			
		||||
                name='name'
 | 
			
		||||
                placeholder={'请输入名称'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.name}
 | 
			
		||||
                required
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
            <Form.Field>
 | 
			
		||||
              <Form.Dropdown
 | 
			
		||||
                label='分组'
 | 
			
		||||
                placeholder={'请选择可以使用该渠道的分组'}
 | 
			
		||||
                name='groups'
 | 
			
		||||
                required
 | 
			
		||||
                fluid
 | 
			
		||||
                multiple
 | 
			
		||||
                selection
 | 
			
		||||
                allowAdditions
 | 
			
		||||
                additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                value={inputs.groups}
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
                options={groupOptions}
 | 
			
		||||
              />
 | 
			
		||||
            </Form.Field>
 | 
			
		||||
 | 
			
		||||
            {inputs.type === 18 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='模型版本'
 | 
			
		||||
                  label={t('channel.edit.spark_version')}
 | 
			
		||||
                  name='other'
 | 
			
		||||
                  placeholder={
 | 
			
		||||
                    '请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
 | 
			
		||||
                  }
 | 
			
		||||
                  placeholder={t('channel.edit.spark_version_placeholder')}
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.other}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
@@ -375,9 +375,9 @@ const EditChannel = () => {
 | 
			
		||||
            {inputs.type === 21 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='知识库 ID'
 | 
			
		||||
                  label={t('channel.edit.knowledge_id')}
 | 
			
		||||
                  name='other'
 | 
			
		||||
                  placeholder={'请输入知识库 ID,例如:123456'}
 | 
			
		||||
                  placeholder={t('channel.edit.knowledge_id_placeholder')}
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.other}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
@@ -387,11 +387,9 @@ const EditChannel = () => {
 | 
			
		||||
            {inputs.type === 17 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='插件参数'
 | 
			
		||||
                  label={t('channel.edit.plugin_param')}
 | 
			
		||||
                  name='other'
 | 
			
		||||
                  placeholder={
 | 
			
		||||
                    '请输入插件参数,即 X-DashScope-Plugin 请求头的取值'
 | 
			
		||||
                  }
 | 
			
		||||
                  placeholder={t('channel.edit.plugin_param_placeholder')}
 | 
			
		||||
                  onChange={handleInputChange}
 | 
			
		||||
                  value={inputs.other}
 | 
			
		||||
                  autoComplete='new-password'
 | 
			
		||||
@@ -399,28 +397,25 @@ const EditChannel = () => {
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type === 34 && (
 | 
			
		||||
              <Message>
 | 
			
		||||
                对于 Coze 而言,模型名称即 Bot ID,你可以添加一个前缀
 | 
			
		||||
                `bot-`,例如:`bot-123456`。
 | 
			
		||||
              </Message>
 | 
			
		||||
              <Message>{t('channel.edit.coze_notice')}</Message>
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type === 40 && (
 | 
			
		||||
              <Message>
 | 
			
		||||
                对于豆包而言,需要手动去{' '}
 | 
			
		||||
                {t('channel.edit.douban_notice')}
 | 
			
		||||
                <a
 | 
			
		||||
                  target='_blank'
 | 
			
		||||
                  href='https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint'
 | 
			
		||||
                >
 | 
			
		||||
                  模型推理页面
 | 
			
		||||
                </a>{' '}
 | 
			
		||||
                创建推理接入点,以接入点名称作为模型名称,例如:`ep-20240608051426-tkxvl`。
 | 
			
		||||
                  {t('channel.edit.douban_notice_link')}
 | 
			
		||||
                </a>
 | 
			
		||||
                {t('channel.edit.douban_notice_2')}
 | 
			
		||||
              </Message>
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type !== 43 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Dropdown
 | 
			
		||||
                  label='模型'
 | 
			
		||||
                  placeholder={'请选择该渠道所支持的模型'}
 | 
			
		||||
                  label={t('channel.edit.models')}
 | 
			
		||||
                  placeholder={t('channel.edit.models_placeholder')}
 | 
			
		||||
                  name='models'
 | 
			
		||||
                  required
 | 
			
		||||
                  fluid
 | 
			
		||||
@@ -448,7 +443,7 @@ const EditChannel = () => {
 | 
			
		||||
                    });
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  填入相关模型
 | 
			
		||||
                  {t('channel.edit.buttons.fill_models')}
 | 
			
		||||
                </Button>
 | 
			
		||||
                <Button
 | 
			
		||||
                  type={'button'}
 | 
			
		||||
@@ -459,7 +454,7 @@ const EditChannel = () => {
 | 
			
		||||
                    });
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  填入所有模型
 | 
			
		||||
                  {t('channel.edit.buttons.fill_all')}
 | 
			
		||||
                </Button>
 | 
			
		||||
                <Button
 | 
			
		||||
                  type={'button'}
 | 
			
		||||
@@ -467,15 +462,15 @@ const EditChannel = () => {
 | 
			
		||||
                    handleInputChange(null, { name: 'models', value: [] });
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  清除所有模型
 | 
			
		||||
                  {t('channel.edit.buttons.clear')}
 | 
			
		||||
                </Button>
 | 
			
		||||
                <Input
 | 
			
		||||
                  action={
 | 
			
		||||
                    <Button type={'button'} onClick={addCustomModel}>
 | 
			
		||||
                      填入
 | 
			
		||||
                      {t('channel.edit.buttons.add_custom')}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  }
 | 
			
		||||
                  placeholder='输入自定义模型名称'
 | 
			
		||||
                  placeholder={t('channel.edit.buttons.custom_placeholder')}
 | 
			
		||||
                  value={customModel}
 | 
			
		||||
                  onChange={(e, { value }) => {
 | 
			
		||||
                    setCustomModel(value);
 | 
			
		||||
@@ -493,12 +488,10 @@ const EditChannel = () => {
 | 
			
		||||
              <>
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.TextArea
 | 
			
		||||
                    label='模型重定向'
 | 
			
		||||
                    placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(
 | 
			
		||||
                      MODEL_MAPPING_EXAMPLE,
 | 
			
		||||
                      null,
 | 
			
		||||
                      2
 | 
			
		||||
                    )}`}
 | 
			
		||||
                    label={t('channel.edit.model_mapping')}
 | 
			
		||||
                    placeholder={`${t(
 | 
			
		||||
                      'channel.edit.model_mapping_placeholder'
 | 
			
		||||
                    )}\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
 | 
			
		||||
                    name='model_mapping'
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.model_mapping}
 | 
			
		||||
@@ -511,8 +504,8 @@ const EditChannel = () => {
 | 
			
		||||
                </Form.Field>
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.TextArea
 | 
			
		||||
                    label='系统提示词'
 | 
			
		||||
                    placeholder={`此项可选,用于强制设置给定的系统提示词,请配合自定义模型 & 模型重定向使用,首先创建一个唯一的自定义模型名称并在上面填入,之后将该自定义模型重定向映射到该渠道一个原生支持的模型`}
 | 
			
		||||
                    label={t('channel.edit.system_prompt')}
 | 
			
		||||
                    placeholder={t('channel.edit.system_prompt_placeholder')}
 | 
			
		||||
                    name='system_prompt'
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.system_prompt}
 | 
			
		||||
@@ -531,7 +524,7 @@ const EditChannel = () => {
 | 
			
		||||
                  label='Region'
 | 
			
		||||
                  name='region'
 | 
			
		||||
                  required
 | 
			
		||||
                  placeholder={'region,e.g. us-west-2'}
 | 
			
		||||
                  placeholder={t('channel.edit.aws_region_placeholder')}
 | 
			
		||||
                  onChange={handleConfigChange}
 | 
			
		||||
                  value={config.region}
 | 
			
		||||
                  autoComplete=''
 | 
			
		||||
@@ -540,7 +533,7 @@ const EditChannel = () => {
 | 
			
		||||
                  label='AK'
 | 
			
		||||
                  name='ak'
 | 
			
		||||
                  required
 | 
			
		||||
                  placeholder={'AWS IAM Access Key'}
 | 
			
		||||
                  placeholder={t('channel.edit.aws_ak_placeholder')}
 | 
			
		||||
                  onChange={handleConfigChange}
 | 
			
		||||
                  value={config.ak}
 | 
			
		||||
                  autoComplete=''
 | 
			
		||||
@@ -549,7 +542,7 @@ const EditChannel = () => {
 | 
			
		||||
                  label='SK'
 | 
			
		||||
                  name='sk'
 | 
			
		||||
                  required
 | 
			
		||||
                  placeholder={'AWS IAM Secret Key'}
 | 
			
		||||
                  placeholder={t('channel.edit.aws_sk_placeholder')}
 | 
			
		||||
                  onChange={handleConfigChange}
 | 
			
		||||
                  value={config.sk}
 | 
			
		||||
                  autoComplete=''
 | 
			
		||||
@@ -562,27 +555,25 @@ const EditChannel = () => {
 | 
			
		||||
                  label='Region'
 | 
			
		||||
                  name='region'
 | 
			
		||||
                  required
 | 
			
		||||
                  placeholder={'Vertex AI Region.g. us-east5'}
 | 
			
		||||
                  placeholder={t('channel.edit.vertex_region_placeholder')}
 | 
			
		||||
                  onChange={handleConfigChange}
 | 
			
		||||
                  value={config.region}
 | 
			
		||||
                  autoComplete=''
 | 
			
		||||
                />
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='Vertex AI Project ID'
 | 
			
		||||
                  label={t('channel.edit.vertex_project_id')}
 | 
			
		||||
                  name='vertex_ai_project_id'
 | 
			
		||||
                  required
 | 
			
		||||
                  placeholder={'Vertex AI Project ID'}
 | 
			
		||||
                  placeholder={t('channel.edit.vertex_project_id_placeholder')}
 | 
			
		||||
                  onChange={handleConfigChange}
 | 
			
		||||
                  value={config.vertex_ai_project_id}
 | 
			
		||||
                  autoComplete=''
 | 
			
		||||
                />
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='Google Cloud Application Default Credentials JSON'
 | 
			
		||||
                  label={t('channel.edit.vertex_credentials')}
 | 
			
		||||
                  name='vertex_ai_adc'
 | 
			
		||||
                  required
 | 
			
		||||
                  placeholder={
 | 
			
		||||
                    'Google Cloud Application Default Credentials JSON'
 | 
			
		||||
                  }
 | 
			
		||||
                  placeholder={t('channel.edit.vertex_credentials_placeholder')}
 | 
			
		||||
                  onChange={handleConfigChange}
 | 
			
		||||
                  value={config.vertex_ai_adc}
 | 
			
		||||
                  autoComplete=''
 | 
			
		||||
@@ -591,10 +582,10 @@ const EditChannel = () => {
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type === 34 && (
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                label='User ID'
 | 
			
		||||
                label={t('channel.edit.user_id')}
 | 
			
		||||
                name='user_id'
 | 
			
		||||
                required
 | 
			
		||||
                placeholder={'生成该密钥的用户 ID'}
 | 
			
		||||
                placeholder={t('channel.edit.user_id_placeholder')}
 | 
			
		||||
                onChange={handleConfigChange}
 | 
			
		||||
                value={config.user_id}
 | 
			
		||||
                autoComplete=''
 | 
			
		||||
@@ -605,10 +596,10 @@ const EditChannel = () => {
 | 
			
		||||
              (batch ? (
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.TextArea
 | 
			
		||||
                    label='密钥'
 | 
			
		||||
                    label={t('channel.edit.key')}
 | 
			
		||||
                    name='key'
 | 
			
		||||
                    required
 | 
			
		||||
                    placeholder={'请输入密钥,一行一个'}
 | 
			
		||||
                    placeholder={t('channel.edit.batch_placeholder')}
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.key}
 | 
			
		||||
                    style={{
 | 
			
		||||
@@ -621,35 +612,20 @@ const EditChannel = () => {
 | 
			
		||||
              ) : (
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    label='密钥'
 | 
			
		||||
                    label={t('channel.edit.key')}
 | 
			
		||||
                    name='key'
 | 
			
		||||
                    required
 | 
			
		||||
                    placeholder={type2secretPrompt(inputs.type)}
 | 
			
		||||
                    placeholder={type2secretPrompt(inputs.type, t)}
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.key}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
                  />
 | 
			
		||||
                </Form.Field>
 | 
			
		||||
              ))}
 | 
			
		||||
            {inputs.type === 37 && (
 | 
			
		||||
              <Form.Field>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  label='Account ID'
 | 
			
		||||
                  name='user_id'
 | 
			
		||||
                  required
 | 
			
		||||
                  placeholder={
 | 
			
		||||
                    '请输入 Account ID,例如:d8d7c61dbc334c32d3ced580e4bf42b4'
 | 
			
		||||
                  }
 | 
			
		||||
                  onChange={handleConfigChange}
 | 
			
		||||
                  value={config.user_id}
 | 
			
		||||
                  autoComplete=''
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )}
 | 
			
		||||
            {inputs.type !== 33 && !isEdit && (
 | 
			
		||||
              <Form.Checkbox
 | 
			
		||||
                checked={batch}
 | 
			
		||||
                label='批量创建'
 | 
			
		||||
                label={t('channel.edit.batch')}
 | 
			
		||||
                name='batch'
 | 
			
		||||
                onChange={() => setBatch(!batch)}
 | 
			
		||||
              />
 | 
			
		||||
@@ -660,11 +636,9 @@ const EditChannel = () => {
 | 
			
		||||
              inputs.type !== 22 && (
 | 
			
		||||
                <Form.Field>
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    label='代理'
 | 
			
		||||
                    label={t('channel.edit.base_url')}
 | 
			
		||||
                    name='base_url'
 | 
			
		||||
                    placeholder={
 | 
			
		||||
                      '此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com'
 | 
			
		||||
                    }
 | 
			
		||||
                    placeholder={t('channel.edit.base_url_placeholder')}
 | 
			
		||||
                    onChange={handleInputChange}
 | 
			
		||||
                    value={inputs.base_url}
 | 
			
		||||
                    autoComplete='new-password'
 | 
			
		||||
@@ -685,13 +659,15 @@ const EditChannel = () => {
 | 
			
		||||
                />
 | 
			
		||||
              </Form.Field>
 | 
			
		||||
            )}
 | 
			
		||||
            <Button onClick={handleCancel}>取消</Button>
 | 
			
		||||
            <Button onClick={handleCancel}>
 | 
			
		||||
              {t('channel.edit.buttons.cancel')}
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              type={isEdit ? 'button' : 'submit'}
 | 
			
		||||
              positive
 | 
			
		||||
              onClick={submit}
 | 
			
		||||
            >
 | 
			
		||||
              提交
 | 
			
		||||
              {t('channel.edit.buttons.submit')}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Form>
 | 
			
		||||
        </Card.Content>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,21 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Card } from 'semantic-ui-react';
 | 
			
		||||
import ChannelsTable from '../../components/ChannelsTable';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
 | 
			
		||||
const Channel = () => (
 | 
			
		||||
  <div className='dashboard-container'>
 | 
			
		||||
    <Card fluid className='chart-card'>
 | 
			
		||||
      <Card.Content>
 | 
			
		||||
        <Card.Header className='header'>管理渠道</Card.Header>
 | 
			
		||||
        <ChannelsTable />
 | 
			
		||||
      </Card.Content>
 | 
			
		||||
    </Card>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
const Channel = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='dashboard-container'>
 | 
			
		||||
      <Card fluid className='chart-card'>
 | 
			
		||||
        <Card.Content>
 | 
			
		||||
          <Card.Header className='header'>{t('channel.title')}</Card.Header>
 | 
			
		||||
          <ChannelsTable />
 | 
			
		||||
        </Card.Content>
 | 
			
		||||
      </Card>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Channel;
 | 
			
		||||
 
 | 
			
		||||
@@ -68,16 +68,27 @@ const Dashboard = () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await axios.get('/api/user/dashboard');
 | 
			
		||||
      if (response.data.success) {
 | 
			
		||||
        const dashboardData = response.data.data;
 | 
			
		||||
        const dashboardData = response.data.data || [];
 | 
			
		||||
        setData(dashboardData);
 | 
			
		||||
        calculateSummary(dashboardData);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Failed to fetch dashboard data:', error);
 | 
			
		||||
      setData([]);
 | 
			
		||||
      calculateSummary([]);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const calculateSummary = (dashboardData) => {
 | 
			
		||||
    if (!Array.isArray(dashboardData) || dashboardData.length === 0) {
 | 
			
		||||
      setSummaryData({
 | 
			
		||||
        todayRequests: 0,
 | 
			
		||||
        todayQuota: 0,
 | 
			
		||||
        todayTokens: 0
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const today = new Date().toISOString().split('T')[0];
 | 
			
		||||
    const todayData = dashboardData.filter((item) => item.Day === today);
 | 
			
		||||
 | 
			
		||||
@@ -87,7 +98,7 @@ const Dashboard = () => {
 | 
			
		||||
        0
 | 
			
		||||
      ),
 | 
			
		||||
      todayQuota:
 | 
			
		||||
        todayData.reduce((sum, item) => sum + item.Quota, 0) / 1000000, // 转换为美元
 | 
			
		||||
        todayData.reduce((sum, item) => sum + item.Quota, 0) / 1000000,
 | 
			
		||||
      todayTokens: todayData.reduce(
 | 
			
		||||
        (sum, item) => sum + item.PromptTokens + item.CompletionTokens,
 | 
			
		||||
        0
 | 
			
		||||
 
 | 
			
		||||
@@ -56,218 +56,222 @@ const Home = () => {
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='dashboard-container'>
 | 
			
		||||
      <Card fluid className='chart-card'>
 | 
			
		||||
        <Card.Content>
 | 
			
		||||
          <Card.Header className='header'>欢迎使用 One API</Card.Header>
 | 
			
		||||
          <Card.Description style={{ lineHeight: '1.6' }}>
 | 
			
		||||
            <p>
 | 
			
		||||
              One API 是一个 LLM API
 | 
			
		||||
              接口管理和分发系统,可以帮助您更好地管理和使用各大厂商的 LLM API。
 | 
			
		||||
            </p>
 | 
			
		||||
            {!userState.user && (
 | 
			
		||||
              <p>
 | 
			
		||||
                如需使用,请先<Link to='/login'>登录</Link>或
 | 
			
		||||
                <Link to='/register'>注册</Link>。
 | 
			
		||||
              </p>
 | 
			
		||||
            )}
 | 
			
		||||
          </Card.Description>
 | 
			
		||||
        </Card.Content>
 | 
			
		||||
      </Card>
 | 
			
		||||
 | 
			
		||||
    <>
 | 
			
		||||
      {homePageContentLoaded && homePageContent === '' ? (
 | 
			
		||||
        <Card fluid className='chart-card'>
 | 
			
		||||
          <Card.Content>
 | 
			
		||||
            <Card.Header>
 | 
			
		||||
              <Header as='h3'>系统状况</Header>
 | 
			
		||||
            </Card.Header>
 | 
			
		||||
            <Grid columns={2} stackable>
 | 
			
		||||
              <Grid.Column>
 | 
			
		||||
                <Card
 | 
			
		||||
                  fluid
 | 
			
		||||
                  className='chart-card'
 | 
			
		||||
                  style={{ boxShadow: '0 1px 3px rgba(0,0,0,0.12)' }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Card.Content>
 | 
			
		||||
                    <Card.Header>
 | 
			
		||||
                      <Header as='h3' style={{ color: '#444' }}>
 | 
			
		||||
                        系统信息
 | 
			
		||||
                      </Header>
 | 
			
		||||
                    </Card.Header>
 | 
			
		||||
                    <Card.Description
 | 
			
		||||
                      style={{ lineHeight: '2', marginTop: '1em' }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <p
 | 
			
		||||
                        style={{
 | 
			
		||||
                          display: 'flex',
 | 
			
		||||
                          alignItems: 'center',
 | 
			
		||||
                          gap: '0.5em',
 | 
			
		||||
                        }}
 | 
			
		||||
        <div className='dashboard-container'>
 | 
			
		||||
          <Card fluid className='chart-card'>
 | 
			
		||||
            <Card.Content>
 | 
			
		||||
              <Card.Header className='header'>欢迎使用 One API</Card.Header>
 | 
			
		||||
              <Card.Description style={{ lineHeight: '1.6' }}>
 | 
			
		||||
                <p>
 | 
			
		||||
                  One API 是一个 LLM API
 | 
			
		||||
                  接口管理和分发系统,可以帮助您更好地管理和使用各大厂商的 LLM
 | 
			
		||||
                  API。
 | 
			
		||||
                </p>
 | 
			
		||||
                {!userState.user && (
 | 
			
		||||
                  <p>
 | 
			
		||||
                    如需使用,请先<Link to='/login'>登录</Link>或
 | 
			
		||||
                    <Link to='/register'>注册</Link>。
 | 
			
		||||
                  </p>
 | 
			
		||||
                )}
 | 
			
		||||
              </Card.Description>
 | 
			
		||||
            </Card.Content>
 | 
			
		||||
          </Card>
 | 
			
		||||
          <Card fluid className='chart-card'>
 | 
			
		||||
            <Card.Content>
 | 
			
		||||
              <Card.Header>
 | 
			
		||||
                <Header as='h3'>系统状况</Header>
 | 
			
		||||
              </Card.Header>
 | 
			
		||||
              <Grid columns={2} stackable>
 | 
			
		||||
                <Grid.Column>
 | 
			
		||||
                  <Card
 | 
			
		||||
                    fluid
 | 
			
		||||
                    className='chart-card'
 | 
			
		||||
                    style={{ boxShadow: '0 1px 3px rgba(0,0,0,0.12)' }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Card.Content>
 | 
			
		||||
                      <Card.Header>
 | 
			
		||||
                        <Header as='h3' style={{ color: '#444' }}>
 | 
			
		||||
                          系统信息
 | 
			
		||||
                        </Header>
 | 
			
		||||
                      </Card.Header>
 | 
			
		||||
                      <Card.Description
 | 
			
		||||
                        style={{ lineHeight: '2', marginTop: '1em' }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <i className='info circle icon'></i>
 | 
			
		||||
                        <span style={{ fontWeight: 'bold' }}>名称:</span>
 | 
			
		||||
                        <span>{statusState?.status?.system_name}</span>
 | 
			
		||||
                      </p>
 | 
			
		||||
                      <p
 | 
			
		||||
                        style={{
 | 
			
		||||
                          display: 'flex',
 | 
			
		||||
                          alignItems: 'center',
 | 
			
		||||
                          gap: '0.5em',
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <i className='code branch icon'></i>
 | 
			
		||||
                        <span style={{ fontWeight: 'bold' }}>版本:</span>
 | 
			
		||||
                        <span>{statusState?.status?.version || 'unknown'}</span>
 | 
			
		||||
                      </p>
 | 
			
		||||
                      <p
 | 
			
		||||
                        style={{
 | 
			
		||||
                          display: 'flex',
 | 
			
		||||
                          alignItems: 'center',
 | 
			
		||||
                          gap: '0.5em',
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <i className='github icon'></i>
 | 
			
		||||
                        <span style={{ fontWeight: 'bold' }}>源码:</span>
 | 
			
		||||
                        <a
 | 
			
		||||
                          href='https://github.com/songquanpeng/one-api'
 | 
			
		||||
                          target='_blank'
 | 
			
		||||
                          style={{ color: '#2185d0' }}
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          GitHub 仓库
 | 
			
		||||
                        </a>
 | 
			
		||||
                      </p>
 | 
			
		||||
                      <p
 | 
			
		||||
                        style={{
 | 
			
		||||
                          display: 'flex',
 | 
			
		||||
                          alignItems: 'center',
 | 
			
		||||
                          gap: '0.5em',
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <i className='clock outline icon'></i>
 | 
			
		||||
                        <span style={{ fontWeight: 'bold' }}>启动时间:</span>
 | 
			
		||||
                        <span>{getStartTimeString()}</span>
 | 
			
		||||
                      </p>
 | 
			
		||||
                    </Card.Description>
 | 
			
		||||
                  </Card.Content>
 | 
			
		||||
                </Card>
 | 
			
		||||
              </Grid.Column>
 | 
			
		||||
                          <i className='info circle icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>名称:</span>
 | 
			
		||||
                          <span>{statusState?.status?.system_name}</span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <i className='code branch icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>版本:</span>
 | 
			
		||||
                          <span>
 | 
			
		||||
                            {statusState?.status?.version || 'unknown'}
 | 
			
		||||
                          </span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <i className='github icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>源码:</span>
 | 
			
		||||
                          <a
 | 
			
		||||
                            href='https://github.com/songquanpeng/one-api'
 | 
			
		||||
                            target='_blank'
 | 
			
		||||
                            style={{ color: '#2185d0' }}
 | 
			
		||||
                          >
 | 
			
		||||
                            GitHub 仓库
 | 
			
		||||
                          </a>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <i className='clock outline icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>启动时间:</span>
 | 
			
		||||
                          <span>{getStartTimeString()}</span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                      </Card.Description>
 | 
			
		||||
                    </Card.Content>
 | 
			
		||||
                  </Card>
 | 
			
		||||
                </Grid.Column>
 | 
			
		||||
 | 
			
		||||
              <Grid.Column>
 | 
			
		||||
                <Card
 | 
			
		||||
                  fluid
 | 
			
		||||
                  className='chart-card'
 | 
			
		||||
                  style={{ boxShadow: '0 1px 3px rgba(0,0,0,0.12)' }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Card.Content>
 | 
			
		||||
                    <Card.Header>
 | 
			
		||||
                      <Header as='h3' style={{ color: '#444' }}>
 | 
			
		||||
                        系统配置
 | 
			
		||||
                      </Header>
 | 
			
		||||
                    </Card.Header>
 | 
			
		||||
                    <Card.Description
 | 
			
		||||
                      style={{ lineHeight: '2', marginTop: '1em' }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <p
 | 
			
		||||
                        style={{
 | 
			
		||||
                          display: 'flex',
 | 
			
		||||
                          alignItems: 'center',
 | 
			
		||||
                          gap: '0.5em',
 | 
			
		||||
                        }}
 | 
			
		||||
                <Grid.Column>
 | 
			
		||||
                  <Card
 | 
			
		||||
                    fluid
 | 
			
		||||
                    className='chart-card'
 | 
			
		||||
                    style={{ boxShadow: '0 1px 3px rgba(0,0,0,0.12)' }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Card.Content>
 | 
			
		||||
                      <Card.Header>
 | 
			
		||||
                        <Header as='h3' style={{ color: '#444' }}>
 | 
			
		||||
                          系统配置
 | 
			
		||||
                        </Header>
 | 
			
		||||
                      </Card.Header>
 | 
			
		||||
                      <Card.Description
 | 
			
		||||
                        style={{ lineHeight: '2', marginTop: '1em' }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <i className='envelope icon'></i>
 | 
			
		||||
                        <span style={{ fontWeight: 'bold' }}>邮箱验证:</span>
 | 
			
		||||
                        <span
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            color: statusState?.status?.email_verification
 | 
			
		||||
                              ? '#21ba45'
 | 
			
		||||
                              : '#db2828',
 | 
			
		||||
                            fontWeight: '500',
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          {statusState?.status?.email_verification
 | 
			
		||||
                            ? '已启用'
 | 
			
		||||
                            : '未启用'}
 | 
			
		||||
                        </span>
 | 
			
		||||
                      </p>
 | 
			
		||||
                      <p
 | 
			
		||||
                        style={{
 | 
			
		||||
                          display: 'flex',
 | 
			
		||||
                          alignItems: 'center',
 | 
			
		||||
                          gap: '0.5em',
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <i className='github icon'></i>
 | 
			
		||||
                        <span style={{ fontWeight: 'bold' }}>
 | 
			
		||||
                          GitHub 身份验证:
 | 
			
		||||
                        </span>
 | 
			
		||||
                        <span
 | 
			
		||||
                          <i className='envelope icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>邮箱验证:</span>
 | 
			
		||||
                          <span
 | 
			
		||||
                            style={{
 | 
			
		||||
                              color: statusState?.status?.email_verification
 | 
			
		||||
                                ? '#21ba45'
 | 
			
		||||
                                : '#db2828',
 | 
			
		||||
                              fontWeight: '500',
 | 
			
		||||
                            }}
 | 
			
		||||
                          >
 | 
			
		||||
                            {statusState?.status?.email_verification
 | 
			
		||||
                              ? '已启用'
 | 
			
		||||
                              : '未启用'}
 | 
			
		||||
                          </span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            color: statusState?.status?.github_oauth
 | 
			
		||||
                              ? '#21ba45'
 | 
			
		||||
                              : '#db2828',
 | 
			
		||||
                            fontWeight: '500',
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          {statusState?.status?.github_oauth
 | 
			
		||||
                            ? '已启用'
 | 
			
		||||
                            : '未启用'}
 | 
			
		||||
                        </span>
 | 
			
		||||
                      </p>
 | 
			
		||||
                      <p
 | 
			
		||||
                        style={{
 | 
			
		||||
                          display: 'flex',
 | 
			
		||||
                          alignItems: 'center',
 | 
			
		||||
                          gap: '0.5em',
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <i className='wechat icon'></i>
 | 
			
		||||
                        <span style={{ fontWeight: 'bold' }}>
 | 
			
		||||
                          微信身份验证:
 | 
			
		||||
                        </span>
 | 
			
		||||
                        <span
 | 
			
		||||
                          <i className='github icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>
 | 
			
		||||
                            GitHub 身份验证:
 | 
			
		||||
                          </span>
 | 
			
		||||
                          <span
 | 
			
		||||
                            style={{
 | 
			
		||||
                              color: statusState?.status?.github_oauth
 | 
			
		||||
                                ? '#21ba45'
 | 
			
		||||
                                : '#db2828',
 | 
			
		||||
                              fontWeight: '500',
 | 
			
		||||
                            }}
 | 
			
		||||
                          >
 | 
			
		||||
                            {statusState?.status?.github_oauth
 | 
			
		||||
                              ? '已启用'
 | 
			
		||||
                              : '未启用'}
 | 
			
		||||
                          </span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            color: statusState?.status?.wechat_login
 | 
			
		||||
                              ? '#21ba45'
 | 
			
		||||
                              : '#db2828',
 | 
			
		||||
                            fontWeight: '500',
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          {statusState?.status?.wechat_login
 | 
			
		||||
                            ? '已启用'
 | 
			
		||||
                            : '未启用'}
 | 
			
		||||
                        </span>
 | 
			
		||||
                      </p>
 | 
			
		||||
                      <p
 | 
			
		||||
                        style={{
 | 
			
		||||
                          display: 'flex',
 | 
			
		||||
                          alignItems: 'center',
 | 
			
		||||
                          gap: '0.5em',
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <i className='shield alternate icon'></i>
 | 
			
		||||
                        <span style={{ fontWeight: 'bold' }}>
 | 
			
		||||
                          Turnstile 校验:
 | 
			
		||||
                        </span>
 | 
			
		||||
                        <span
 | 
			
		||||
                          <i className='wechat icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>
 | 
			
		||||
                            微信身份验证:
 | 
			
		||||
                          </span>
 | 
			
		||||
                          <span
 | 
			
		||||
                            style={{
 | 
			
		||||
                              color: statusState?.status?.wechat_login
 | 
			
		||||
                                ? '#21ba45'
 | 
			
		||||
                                : '#db2828',
 | 
			
		||||
                              fontWeight: '500',
 | 
			
		||||
                            }}
 | 
			
		||||
                          >
 | 
			
		||||
                            {statusState?.status?.wechat_login
 | 
			
		||||
                              ? '已启用'
 | 
			
		||||
                              : '未启用'}
 | 
			
		||||
                          </span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p
 | 
			
		||||
                          style={{
 | 
			
		||||
                            color: statusState?.status?.turnstile_check
 | 
			
		||||
                              ? '#21ba45'
 | 
			
		||||
                              : '#db2828',
 | 
			
		||||
                            fontWeight: '500',
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            alignItems: 'center',
 | 
			
		||||
                            gap: '0.5em',
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          {statusState?.status?.turnstile_check
 | 
			
		||||
                            ? '已启用'
 | 
			
		||||
                            : '未启用'}
 | 
			
		||||
                        </span>
 | 
			
		||||
                      </p>
 | 
			
		||||
                    </Card.Description>
 | 
			
		||||
                  </Card.Content>
 | 
			
		||||
                </Card>
 | 
			
		||||
              </Grid.Column>
 | 
			
		||||
            </Grid>
 | 
			
		||||
          </Card.Content>
 | 
			
		||||
        </Card>
 | 
			
		||||
                          <i className='shield alternate icon'></i>
 | 
			
		||||
                          <span style={{ fontWeight: 'bold' }}>
 | 
			
		||||
                            Turnstile 校验:
 | 
			
		||||
                          </span>
 | 
			
		||||
                          <span
 | 
			
		||||
                            style={{
 | 
			
		||||
                              color: statusState?.status?.turnstile_check
 | 
			
		||||
                                ? '#21ba45'
 | 
			
		||||
                                : '#db2828',
 | 
			
		||||
                              fontWeight: '500',
 | 
			
		||||
                            }}
 | 
			
		||||
                          >
 | 
			
		||||
                            {statusState?.status?.turnstile_check
 | 
			
		||||
                              ? '已启用'
 | 
			
		||||
                              : '未启用'}
 | 
			
		||||
                          </span>
 | 
			
		||||
                        </p>
 | 
			
		||||
                      </Card.Description>
 | 
			
		||||
                    </Card.Content>
 | 
			
		||||
                  </Card>
 | 
			
		||||
                </Grid.Column>
 | 
			
		||||
              </Grid>
 | 
			
		||||
            </Card.Content>
 | 
			
		||||
          </Card>{' '}
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          {homePageContent.startsWith('https://') ? (
 | 
			
		||||
@@ -283,7 +287,7 @@ const Home = () => {
 | 
			
		||||
          )}
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,10 @@ import {
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { API, showError, showInfo, showSuccess } from '../../helpers';
 | 
			
		||||
import { renderQuota } from '../../helpers/render';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
 | 
			
		||||
const TopUp = () => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
  const [redemptionCode, setRedemptionCode] = useState('');
 | 
			
		||||
  const [topUpLink, setTopUpLink] = useState('');
 | 
			
		||||
  const [userQuota, setUserQuota] = useState(0);
 | 
			
		||||
@@ -20,7 +22,7 @@ const TopUp = () => {
 | 
			
		||||
 | 
			
		||||
  const topUp = async () => {
 | 
			
		||||
    if (redemptionCode === '') {
 | 
			
		||||
      showInfo('请输入兑换码!');
 | 
			
		||||
      showInfo(t('topup.redeem_code.empty_code'));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setIsSubmitting(true);
 | 
			
		||||
@@ -30,7 +32,7 @@ const TopUp = () => {
 | 
			
		||||
      });
 | 
			
		||||
      const { success, message, data } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
        showSuccess('充值成功!');
 | 
			
		||||
        showSuccess(t('topup.redeem_code.success'));
 | 
			
		||||
        setUserQuota((quota) => {
 | 
			
		||||
          return quota + data;
 | 
			
		||||
        });
 | 
			
		||||
@@ -39,7 +41,7 @@ const TopUp = () => {
 | 
			
		||||
        showError(message);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      showError('请求失败');
 | 
			
		||||
      showError(t('topup.redeem_code.request_failed'));
 | 
			
		||||
    } finally {
 | 
			
		||||
      setIsSubmitting(false);
 | 
			
		||||
    }
 | 
			
		||||
@@ -47,13 +49,12 @@ const TopUp = () => {
 | 
			
		||||
 | 
			
		||||
  const openTopUpLink = () => {
 | 
			
		||||
    if (!topUpLink) {
 | 
			
		||||
      showError('超级管理员未设置充值链接!');
 | 
			
		||||
      showError(t('topup.redeem_code.no_link'));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    let url = new URL(topUpLink);
 | 
			
		||||
    let username = user.username;
 | 
			
		||||
    let user_id = user.id;
 | 
			
		||||
    // add username and user_id to the topup link
 | 
			
		||||
    url.searchParams.append('username', username);
 | 
			
		||||
    url.searchParams.append('user_id', user_id);
 | 
			
		||||
    url.searchParams.append('transaction_id', crypto.randomUUID());
 | 
			
		||||
@@ -87,7 +88,7 @@ const TopUp = () => {
 | 
			
		||||
      <Card fluid className='chart-card'>
 | 
			
		||||
        <Card.Content>
 | 
			
		||||
          <Card.Header>
 | 
			
		||||
            <Header as='h2'>充值中心</Header>
 | 
			
		||||
            <Header as='h2'>{t('topup.title')}</Header>
 | 
			
		||||
          </Card.Header>
 | 
			
		||||
 | 
			
		||||
          <Grid columns={2} stackable>
 | 
			
		||||
@@ -109,7 +110,7 @@ const TopUp = () => {
 | 
			
		||||
                  <Card.Header>
 | 
			
		||||
                    <Header as='h3' style={{ color: '#2185d0', margin: '1em' }}>
 | 
			
		||||
                      <i className='credit card icon'></i>
 | 
			
		||||
                      获取兑换码
 | 
			
		||||
                      {t('topup.get_code.title')}
 | 
			
		||||
                    </Header>
 | 
			
		||||
                  </Card.Header>
 | 
			
		||||
                  <Card.Description
 | 
			
		||||
@@ -132,7 +133,9 @@ const TopUp = () => {
 | 
			
		||||
                          <Statistic.Value style={{ color: '#2185d0' }}>
 | 
			
		||||
                            {renderQuota(userQuota)}
 | 
			
		||||
                          </Statistic.Value>
 | 
			
		||||
                          <Statistic.Label>当前可用额度</Statistic.Label>
 | 
			
		||||
                          <Statistic.Label>
 | 
			
		||||
                            {t('topup.get_code.current_quota')}
 | 
			
		||||
                          </Statistic.Label>
 | 
			
		||||
                        </Statistic>
 | 
			
		||||
                      </div>
 | 
			
		||||
 | 
			
		||||
@@ -145,7 +148,7 @@ const TopUp = () => {
 | 
			
		||||
                          onClick={openTopUpLink}
 | 
			
		||||
                          style={{ width: '80%' }}
 | 
			
		||||
                        >
 | 
			
		||||
                          立即获取兑换码
 | 
			
		||||
                          {t('topup.get_code.button')}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
@@ -172,7 +175,7 @@ const TopUp = () => {
 | 
			
		||||
                  <Card.Header>
 | 
			
		||||
                    <Header as='h3' style={{ color: '#21ba45', margin: '1em' }}>
 | 
			
		||||
                      <i className='ticket alternate icon'></i>
 | 
			
		||||
                      兑换码充值
 | 
			
		||||
                      {t('topup.redeem_code.title')}
 | 
			
		||||
                    </Header>
 | 
			
		||||
                  </Card.Header>
 | 
			
		||||
                  <Card.Description
 | 
			
		||||
@@ -194,7 +197,7 @@ const TopUp = () => {
 | 
			
		||||
                        fluid
 | 
			
		||||
                        icon='key'
 | 
			
		||||
                        iconPosition='left'
 | 
			
		||||
                        placeholder='请输入兑换码'
 | 
			
		||||
                        placeholder={t('topup.redeem_code.placeholder')}
 | 
			
		||||
                        value={redemptionCode}
 | 
			
		||||
                        onChange={(e) => {
 | 
			
		||||
                          setRedemptionCode(e.target.value);
 | 
			
		||||
@@ -207,14 +210,14 @@ const TopUp = () => {
 | 
			
		||||
                        action={
 | 
			
		||||
                          <Button
 | 
			
		||||
                            icon='paste'
 | 
			
		||||
                            content='粘贴'
 | 
			
		||||
                            content={t('topup.redeem_code.paste')}
 | 
			
		||||
                            onClick={async () => {
 | 
			
		||||
                              try {
 | 
			
		||||
                                const text =
 | 
			
		||||
                                  await navigator.clipboard.readText();
 | 
			
		||||
                                setRedemptionCode(text.trim());
 | 
			
		||||
                              } catch (err) {
 | 
			
		||||
                                showError('无法访问剪贴板,请手动粘贴');
 | 
			
		||||
                                showError(t('topup.redeem_code.paste_error'));
 | 
			
		||||
                              }
 | 
			
		||||
                            }}
 | 
			
		||||
                          />
 | 
			
		||||
@@ -230,7 +233,9 @@ const TopUp = () => {
 | 
			
		||||
                          loading={isSubmitting}
 | 
			
		||||
                          disabled={isSubmitting}
 | 
			
		||||
                        >
 | 
			
		||||
                          {isSubmitting ? '兑换中...' : '立即兑换'}
 | 
			
		||||
                          {isSubmitting
 | 
			
		||||
                            ? t('topup.redeem_code.submitting')
 | 
			
		||||
                            : t('topup.redeem_code.submit')}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user