mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-09-17 09:16:36 +08:00
Compare commits
120 Commits
v0.6.11-al
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
8df4a2670b | ||
|
7ac553541b | ||
|
a5c517c27a | ||
|
3f421c4f04 | ||
|
1ce6a226f6 | ||
|
cafd0a0327 | ||
|
8b8cd03e85 | ||
|
54c38de813 | ||
|
d6284bf6b0 | ||
|
df5d2ca93d | ||
|
fef7ae048b | ||
|
6916debf66 | ||
|
53da209134 | ||
|
517f6ad211 | ||
|
10aba11f18 | ||
|
4d011c5f98 | ||
|
eb96aa635e | ||
|
c715f2bc1d | ||
|
aed090dd55 | ||
|
696265774e | ||
|
974729426d | ||
|
57c1367ec8 | ||
|
44233d5c04 | ||
|
bf45a955c3 | ||
|
20435fcbfc | ||
|
6e7a1c2323 | ||
|
dd65b997dd | ||
|
0b6d03d6c6 | ||
|
4375246e24 | ||
|
3e3b8230ac | ||
|
07808122a6 | ||
|
c96895e35b | ||
|
2552c68249 | ||
|
5c81e40612 | ||
|
0d5318b1b7 | ||
|
db65db2807 | ||
|
e0b7e6a9e2 | ||
|
27c2abe80f | ||
|
2c867251b5 | ||
|
108111ebd3 | ||
|
293ba93ad6 | ||
|
faced40d5b | ||
|
2ae9997f29 | ||
|
e146b14d46 | ||
|
e19045f925 | ||
|
d2903b673d | ||
|
33edcf604c | ||
|
78fd6ef161 | ||
|
9b6d5f76e0 | ||
|
2250f311e1 | ||
|
e43f758623 | ||
|
6ded638f70 | ||
|
ea3331b79a | ||
|
7a97ddc03c | ||
|
d7028b55fd | ||
|
36a03f465b | ||
|
ae16647047 | ||
|
4dbc2ad86d | ||
|
94479c2800 | ||
|
614903b524 | ||
|
1dc255c219 | ||
|
15c27e4b12 | ||
|
a910d3ba25 | ||
|
34f889437d | ||
|
a56cb4a2d5 | ||
|
95009b254a | ||
|
20c30a060b | ||
|
ca334a5502 | ||
|
c81ee68dd0 | ||
|
ececfe5ba0 | ||
|
9e1f06df5c | ||
|
8fa9df5b3c | ||
|
19998ebb8a | ||
|
5659fde3fb | ||
|
c64b7c891f | ||
|
9b83d63896 | ||
|
0bf3fd4038 | ||
|
99b6200156 | ||
|
8f40e11c97 | ||
|
562964238c | ||
|
46b1d35d83 | ||
|
6a197ceb69 | ||
|
bd7d0d1e96 | ||
|
f325f58edf | ||
|
aef2100be1 | ||
|
4685c52a5d | ||
|
174dea7763 | ||
|
9b81c88250 | ||
|
72d911986c | ||
|
a01d769a83 | ||
|
d0965050a9 | ||
|
e7ea7c866f | ||
|
b1fe81a84f | ||
|
e183e3b9b0 | ||
|
b7f008cd72 | ||
|
33102c4586 | ||
|
ee3ed65356 | ||
|
958f2f4ea8 | ||
|
4a5f872dce | ||
|
6ca6a3ea74 | ||
|
2c8c29bfc7 | ||
|
ae20aea555 | ||
|
60f2776795 | ||
|
93ce6c4cd7 | ||
|
4fe5ab8d09 | ||
|
afbbfbbf83 | ||
|
75d9d9d560 | ||
|
d1af30ee5a | ||
|
a3924a2353 | ||
|
9af5a1d11d | ||
|
57f9f7dfbb | ||
|
d9f2df2baf | ||
|
bdf312e5dc | ||
|
1521df6551 | ||
|
c67b167f4f | ||
|
c351e196e6 | ||
|
a316ed7abc | ||
|
0895d8660e | ||
|
be1ed114f4 | ||
|
eb6da573a3 |
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 }}
|
10
.github/workflows/docker-image.yml
vendored
10
.github/workflows/docker-image.yml
vendored
@ -32,10 +32,10 @@ jobs:
|
||||
git describe --tags > VERSION
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
@ -55,14 +55,14 @@ jobs:
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
justsong/one-api
|
||||
ghcr.io/${{ github.repository }}
|
||||
${{ contains(github.ref, 'alpha') && 'justsong/one-api-alpha' || 'justsong/one-api' }}
|
||||
${{ contains(github.ref, 'alpha') && format('ghcr.io/{0}-alpha', github.repository) || format('ghcr.io/{0}', github.repository) }}
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: ${{ contains(github.ref, 'alpha') && 'linux/amd64' || 'linux/amd64' }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
1
.github/workflows/linux-release.yml
vendored
1
.github/workflows/linux-release.yml
vendored
@ -7,6 +7,7 @@ on:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
- '!*-alpha*'
|
||||
- '!*-preview*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
name:
|
||||
|
1
.github/workflows/macos-release.yml
vendored
1
.github/workflows/macos-release.yml
vendored
@ -7,6 +7,7 @@ on:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
- '!*-alpha*'
|
||||
- '!*-preview*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
name:
|
||||
|
1
.github/workflows/windows-release.yml
vendored
1
.github/workflows/windows-release.yml
vendored
@ -7,6 +7,7 @@ on:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
- '!*-alpha*'
|
||||
- '!*-preview*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
name:
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,3 +11,5 @@ data
|
||||
cmd.md
|
||||
.env
|
||||
/one-api
|
||||
temp
|
||||
.DS_Store
|
37
Dockerfile
37
Dockerfile
@ -4,41 +4,44 @@ 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
|
||||
|
||||
WORKDIR /web/air
|
||||
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 ./VERSION) npm run build --prefix /web/default & \
|
||||
DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat ./VERSION) npm run build --prefix /web/berry & \
|
||||
DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat ./VERSION) npm run build --prefix /web/air & \
|
||||
wait
|
||||
|
||||
FROM golang:alpine AS builder2
|
||||
|
||||
RUN apk add --no-cache g++
|
||||
RUN apk add --no-cache \
|
||||
gcc \
|
||||
musl-dev \
|
||||
sqlite-dev \
|
||||
build-base
|
||||
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=1 \
|
||||
GOOS=linux
|
||||
|
||||
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)' -linkmode external -extldflags '-static'" -o one-api
|
||||
|
||||
RUN apk update \
|
||||
&& apk upgrade \
|
||||
&& apk add --no-cache ca-certificates tzdata \
|
||||
&& update-ca-certificates 2>/dev/null || true
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
COPY --from=builder2 /build/one-api /
|
||||
|
||||
EXPOSE 3000
|
||||
WORKDIR /data
|
||||
ENTRYPOINT ["/one-api"]
|
@ -52,8 +52,6 @@ _✨ Access all LLM through the standard OpenAI API format, easy to deploy & use
|
||||
|
||||
> **Warning**: This README is translated by ChatGPT. Please feel free to submit a PR if you find any translation errors.
|
||||
|
||||
> **Warning**: The Docker image for English version is `justsong/one-api-en`.
|
||||
|
||||
> **Note**: The latest image pulled from Docker may be an `alpha` release. Specify the version manually if you require stability.
|
||||
|
||||
## Features
|
||||
@ -89,7 +87,9 @@ _✨ Access all LLM through the standard OpenAI API format, easy to deploy & use
|
||||
|
||||
## Deployment
|
||||
### Docker Deployment
|
||||
Deployment command: `docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api-en`
|
||||
|
||||
Deployment command:
|
||||
`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api`
|
||||
|
||||
Update command: `docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR`
|
||||
|
||||
@ -315,6 +315,7 @@ If the channel ID is not provided, load balancing will be used to distribute the
|
||||
* [FastGPT](https://github.com/labring/FastGPT): Knowledge question answering system based on the LLM
|
||||
* [VChart](https://github.com/VisActor/VChart): More than just a cross-platform charting library, but also an expressive data storyteller.
|
||||
* [VMind](https://github.com/VisActor/VMind): Not just automatic, but also fantastic. Open-source solution for intelligent visualization.
|
||||
* * [CherryStudio](https://github.com/CherryHQ/cherry-studio): A cross-platform AI client that integrates multiple service providers and supports local knowledge base management.
|
||||
|
||||
## Note
|
||||
This project is an open-source project. Please use it in compliance with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**. It must not be used for illegal purposes.
|
||||
|
10
README.ja.md
10
README.ja.md
@ -52,8 +52,6 @@ _✨ 標準的な OpenAI API フォーマットを通じてすべての LLM に
|
||||
|
||||
> **警告**: この README は ChatGPT によって翻訳されています。翻訳ミスを発見した場合は遠慮なく PR を投稿してください。
|
||||
|
||||
> **警告**: 英語版の Docker イメージは `justsong/one-api-en` です。
|
||||
|
||||
> **注**: Docker からプルされた最新のイメージは、`alpha` リリースかもしれません。安定性が必要な場合は、手動でバージョンを指定してください。
|
||||
|
||||
## 特徴
|
||||
@ -89,7 +87,9 @@ _✨ 標準的な OpenAI API フォーマットを通じてすべての LLM に
|
||||
|
||||
## デプロイメント
|
||||
### Docker デプロイメント
|
||||
デプロイコマンド: `docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api-en`。
|
||||
|
||||
デプロイコマンド:
|
||||
`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api`。
|
||||
|
||||
コマンドを更新する: `docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrr/watchtower -cR`。
|
||||
|
||||
@ -287,8 +287,8 @@ graph LR
|
||||
+ インターフェイスアドレスと API Key が正しいか再確認してください。
|
||||
|
||||
## 関連プロジェクト
|
||||
[FastGPT](https://github.com/labring/FastGPT): LLM に基づく知識質問応答システム
|
||||
|
||||
* [FastGPT](https://github.com/labring/FastGPT): LLM に基づく知識質問応答システム
|
||||
* [CherryStudio](https://github.com/CherryHQ/cherry-studio): マルチプラットフォーム対応のAIクライアント。複数のサービスプロバイダーを統合管理し、ローカル知識ベースをサポートします。
|
||||
## 注
|
||||
本プロジェクトはオープンソースプロジェクトです。OpenAI の[利用規約](https://openai.com/policies/terms-of-use)および**適用される法令**を遵守してご利用ください。違法な目的での利用はご遠慮ください。
|
||||
|
||||
|
15
README.md
15
README.md
@ -56,8 +56,12 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
|
||||
>
|
||||
> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
|
||||
|
||||
> [!WARNING]
|
||||
> 使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。
|
||||
> [!NOTE]
|
||||
> 稳定版 / 预览版镜像地址:[justsong/one-api](https://hub.docker.com/repository/docker/justsong/one-api)
|
||||
> 或者 [ghcr.io/songquanpeng/one-api](https://github.com/songquanpeng/one-api/pkgs/container/one-api)
|
||||
>
|
||||
> alpha 版镜像地址:[justsong/one-api-alpha](https://hub.docker.com/repository/docker/justsong/one-api-alpha)
|
||||
> 或者 [ghcr.io/songquanpeng/one-api-alpha](https://github.com/songquanpeng/one-api/pkgs/container/one-api-alpha)
|
||||
|
||||
> [!WARNING]
|
||||
> 使用 root 用户初次登录系统后,务必修改默认密码 `123456`!
|
||||
@ -68,7 +72,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
|
||||
+ [x] [Anthropic Claude 系列模型](https://anthropic.com) (支持 AWS Claude)
|
||||
+ [x] [Google PaLM2/Gemini 系列模型](https://developers.generativeai.google)
|
||||
+ [x] [Mistral 系列模型](https://mistral.ai/)
|
||||
+ [x] [字节跳动豆包大模型](https://console.volcengine.com/ark/region:ark+cn-beijing/model)
|
||||
+ [x] [字节跳动豆包大模型(火山引擎)](https://www.volcengine.com/experience/ark?utm_term=202502dsinvite&ac=DSASUQY5&rc=2QXCA1VI)
|
||||
+ [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html)
|
||||
+ [x] [阿里通义千问系列模型](https://help.aliyun.com/document_detail/2400395.html)
|
||||
+ [x] [讯飞星火认知大模型](https://www.xfyun.cn/doc/spark/Web.html)
|
||||
@ -89,7 +93,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
|
||||
+ [x] [DeepL](https://www.deepl.com/)
|
||||
+ [x] [together.ai](https://www.together.ai/)
|
||||
+ [x] [novita.ai](https://www.novita.ai/)
|
||||
+ [x] [硅基流动 SiliconCloud](https://siliconflow.cn/siliconcloud)
|
||||
+ [x] [硅基流动 SiliconCloud](https://cloud.siliconflow.cn/i/rKXmRobW)
|
||||
+ [x] [xAI](https://x.ai/)
|
||||
2. 支持配置镜像以及众多[第三方代理服务](https://iamazing.cn/page/openai-api-third-party-services)。
|
||||
3. 支持通过**负载均衡**的方式访问多个渠道。
|
||||
@ -111,7 +115,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
|
||||
19. 支持丰富的**自定义**设置,
|
||||
1. 支持自定义系统名称,logo 以及页脚。
|
||||
2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
|
||||
20. 支持通过系统访问令牌调用管理 API,进而**在无需二开的情况下扩展和自定义** One API 的功能,详情请参考此处 [API 文档](./docs/API.md)。。
|
||||
20. 支持通过系统访问令牌调用管理 API,进而**在无需二开的情况下扩展和自定义** One API 的功能,详情请参考此处 [API 文档](./docs/API.md)。
|
||||
21. 支持 Cloudflare Turnstile 用户校验。
|
||||
22. 支持用户管理,支持**多种用户登录注册方式**:
|
||||
+ 邮箱登录注册(支持注册邮箱白名单)以及通过邮箱进行密码重置。
|
||||
@ -465,6 +469,7 @@ https://openai.justsong.cn
|
||||
* [ChatGPT Next Web](https://github.com/Yidadaa/ChatGPT-Next-Web): 一键拥有你自己的跨平台 ChatGPT 应用
|
||||
* [VChart](https://github.com/VisActor/VChart): 不只是开箱即用的多端图表库,更是生动灵活的数据故事讲述者。
|
||||
* [VMind](https://github.com/VisActor/VMind): 不仅自动,还很智能。开源智能可视化解决方案。
|
||||
* [CherryStudio](https://github.com/CherryHQ/cherry-studio): 全平台支持的AI客户端, 多服务商集成管理、本地知识库支持。
|
||||
|
||||
## 注意
|
||||
|
||||
|
@ -126,10 +126,10 @@ var ValidThemes = map[string]bool{
|
||||
// All duration's unit is seconds
|
||||
// Shouldn't larger then RateLimitKeyExpirationDuration
|
||||
var (
|
||||
GlobalApiRateLimitNum = env.Int("GLOBAL_API_RATE_LIMIT", 240)
|
||||
GlobalApiRateLimitNum = env.Int("GLOBAL_API_RATE_LIMIT", 480)
|
||||
GlobalApiRateLimitDuration int64 = 3 * 60
|
||||
|
||||
GlobalWebRateLimitNum = env.Int("GLOBAL_WEB_RATE_LIMIT", 120)
|
||||
GlobalWebRateLimitNum = env.Int("GLOBAL_WEB_RATE_LIMIT", 240)
|
||||
GlobalWebRateLimitDuration int64 = 3 * 60
|
||||
|
||||
UploadRateLimitNum = 10
|
||||
@ -163,4 +163,4 @@ var UserContentRequestProxy = env.String("USER_CONTENT_REQUEST_PROXY", "")
|
||||
var UserContentRequestTimeout = env.Int("USER_CONTENT_REQUEST_TIMEOUT", 30)
|
||||
|
||||
var EnforceIncludeUsage = env.Bool("ENFORCE_INCLUDE_USAGE", false)
|
||||
var TestPrompt = env.String("TEST_PROMPT", "Print your model name exactly and do not output without any other text.")
|
||||
var TestPrompt = env.String("TEST_PROMPT", "Output only your specific model name with no additional text.")
|
||||
|
@ -3,10 +3,11 @@ package common
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/songquanpeng/one-api/common/ctxkey"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/songquanpeng/one-api/common/ctxkey"
|
||||
)
|
||||
|
||||
func GetRequestBody(c *gin.Context) ([]byte, error) {
|
||||
@ -31,7 +32,6 @@ func UnmarshalBodyReusable(c *gin.Context, v any) error {
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "application/json") {
|
||||
err = json.Unmarshal(requestBody, &v)
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||
} else {
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||
err = c.ShouldBind(&v)
|
||||
@ -40,6 +40,7 @@ func UnmarshalBodyReusable(c *gin.Context, v any) error {
|
||||
return err
|
||||
}
|
||||
// Reset request body
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
72
common/i18n/i18n.go
Normal file
72
common/i18n/i18n.go
Normal file
@ -0,0 +1,72 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
//go:embed locales/*.json
|
||||
var localesFS embed.FS
|
||||
|
||||
var (
|
||||
translations = make(map[string]map[string]string)
|
||||
defaultLang = "en"
|
||||
ContextKey = "i18n"
|
||||
)
|
||||
|
||||
// Init loads all translation files from embedded filesystem
|
||||
func Init() error {
|
||||
entries, err := localesFS.ReadDir("locales")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
|
||||
langCode := strings.TrimSuffix(entry.Name(), ".json")
|
||||
content, err := localesFS.ReadFile("locales/" + entry.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var translation map[string]string
|
||||
if err := json.Unmarshal(content, &translation); err != nil {
|
||||
return err
|
||||
}
|
||||
translations[langCode] = translation
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetLang(c *gin.Context) string {
|
||||
rawLang, ok := c.Get(ContextKey)
|
||||
if !ok {
|
||||
return defaultLang
|
||||
}
|
||||
lang, _ := rawLang.(string)
|
||||
if lang != "" {
|
||||
return lang
|
||||
}
|
||||
return defaultLang
|
||||
}
|
||||
|
||||
func Translate(c *gin.Context, message string) string {
|
||||
lang := GetLang(c)
|
||||
return translateHelper(lang, message)
|
||||
}
|
||||
|
||||
func translateHelper(lang, message string) string {
|
||||
if trans, ok := translations[lang]; ok {
|
||||
if translated, exists := trans[message]; exists {
|
||||
return translated
|
||||
}
|
||||
}
|
||||
return message
|
||||
}
|
5
common/i18n/locales/en.json
Normal file
5
common/i18n/locales/en.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"invalid_input": "Invalid input, please check your input",
|
||||
"send_email_failed": "failed to send email: ",
|
||||
"invalid_parameter": "invalid parameter"
|
||||
}
|
5
common/i18n/locales/zh-CN.json
Normal file
5
common/i18n/locales/zh-CN.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"invalid_input": "无效的输入,请检查您的输入",
|
||||
"send_email_failed": "发送邮件失败:",
|
||||
"invalid_parameter": "无效的参数"
|
||||
}
|
@ -57,6 +57,14 @@ func SysLogf(format string, a ...any) {
|
||||
logHelper(nil, loggerINFO, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func SysWarn(s string) {
|
||||
logHelper(nil, loggerWarn, s)
|
||||
}
|
||||
|
||||
func SysWarnf(format string, a ...any) {
|
||||
logHelper(nil, loggerWarn, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func SysError(s string) {
|
||||
logHelper(nil, loggerError, s)
|
||||
}
|
||||
@ -85,6 +93,9 @@ func Error(ctx context.Context, msg string) {
|
||||
}
|
||||
|
||||
func Debugf(ctx context.Context, format string, a ...any) {
|
||||
if !config.DebugEnabled {
|
||||
return
|
||||
}
|
||||
logHelper(ctx, loggerDEBUG, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
|
@ -5,11 +5,13 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
)
|
||||
|
||||
func shouldAuth() bool {
|
||||
@ -98,8 +100,12 @@ func SendEmail(subject string, receiver string, content string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err = smtp.SendMail(addr, auth, config.SMTPAccount, to, mail)
|
||||
return nil
|
||||
}
|
||||
err = smtp.SendMail(addr, auth, config.SMTPAccount, to, mail)
|
||||
if err != nil && strings.Contains(err.Error(), "short response") { // 部分提供商返回该错误,但实际上邮件已经发送成功
|
||||
logger.SysWarnf("short response from SMTP server, return nil instead of error: %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
34
common/message/template.go
Normal file
34
common/message/template.go
Normal file
@ -0,0 +1,34 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
)
|
||||
|
||||
// EmailTemplate 生成美观的 HTML 邮件内容
|
||||
func EmailTemplate(title, content string) string {
|
||||
return fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="margin: 0; padding: 20px; font-family: Arial, sans-serif; line-height: 1.6; background-color: #f4f4f4;">
|
||||
<div style="max-width: 600px; margin: 20px auto; padding: 30px; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);">
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h2 style="color: #333; margin: 0; font-size: 24px;">%s</h2>
|
||||
</div>
|
||||
<div style="color: #555; font-size: 16px;">
|
||||
%s
|
||||
</div>
|
||||
<div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee; color: #888; font-size: 14px; text-align: center;">
|
||||
<p style="margin: 5px 0;">此邮件由系统自动发送,请勿直接回复</p>
|
||||
<p style="margin: 5px 0;">%s</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, title, content, config.SystemName)
|
||||
}
|
13
common/utils/array.go
Normal file
13
common/utils/array.go
Normal file
@ -0,0 +1,13 @@
|
||||
package utils
|
||||
|
||||
func DeDuplication(slice []string) []string {
|
||||
m := make(map[string]bool)
|
||||
for _, v := range slice {
|
||||
m[v] = true
|
||||
}
|
||||
result := make([]string, 0, len(m))
|
||||
for v := range m {
|
||||
result = append(result, v)
|
||||
}
|
||||
return result
|
||||
}
|
@ -112,6 +112,13 @@ type DeepSeekUsageResponse struct {
|
||||
} `json:"balance_infos"`
|
||||
}
|
||||
|
||||
type OpenRouterResponse struct {
|
||||
Data struct {
|
||||
TotalCredits float64 `json:"total_credits"`
|
||||
TotalUsage float64 `json:"total_usage"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// GetAuthHeader get auth header
|
||||
func GetAuthHeader(token string) http.Header {
|
||||
h := http.Header{}
|
||||
@ -285,6 +292,22 @@ func updateChannelDeepSeekBalance(channel *model.Channel) (float64, error) {
|
||||
return balance, nil
|
||||
}
|
||||
|
||||
func updateChannelOpenRouterBalance(channel *model.Channel) (float64, error) {
|
||||
url := "https://openrouter.ai/api/v1/credits"
|
||||
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
response := OpenRouterResponse{}
|
||||
err = json.Unmarshal(body, &response)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
balance := response.Data.TotalCredits - response.Data.TotalUsage
|
||||
channel.UpdateBalance(balance)
|
||||
return balance, nil
|
||||
}
|
||||
|
||||
func updateChannelBalance(channel *model.Channel) (float64, error) {
|
||||
baseURL := channeltype.ChannelBaseURLs[channel.Type]
|
||||
if channel.GetBaseURL() == "" {
|
||||
@ -313,6 +336,8 @@ func updateChannelBalance(channel *model.Channel) (float64, error) {
|
||||
return updateChannelSiliconFlowBalance(channel)
|
||||
case channeltype.DeepSeek:
|
||||
return updateChannelDeepSeekBalance(channel)
|
||||
case channeltype.OpenRouter:
|
||||
return updateChannelOpenRouterBalance(channel)
|
||||
default:
|
||||
return 0, errors.New("尚未实现")
|
||||
}
|
||||
|
@ -153,6 +153,7 @@ func testChannel(ctx context.Context, channel *model.Channel, request *relaymode
|
||||
rawResponse := w.Body.String()
|
||||
_, responseMessage, err = parseTestResponse(rawResponse)
|
||||
if err != nil {
|
||||
logger.SysError(fmt.Sprintf("failed to parse error: %s, \nresponse: %s", err.Error(), rawResponse))
|
||||
return "", err, nil
|
||||
}
|
||||
result := w.Result()
|
||||
|
@ -3,13 +3,15 @@ package controller
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/message"
|
||||
"github.com/songquanpeng/one-api/model"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/i18n"
|
||||
"github.com/songquanpeng/one-api/common/message"
|
||||
"github.com/songquanpeng/one-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@ -85,7 +87,7 @@ func SendEmailVerification(c *gin.Context) {
|
||||
if err := common.Validate.Var(email, "required,email"); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
"message": i18n.Translate(c, "invalid_parameter"),
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -114,10 +116,17 @@ func SendEmailVerification(c *gin.Context) {
|
||||
}
|
||||
code := common.GenerateVerificationCode(6)
|
||||
common.RegisterVerificationCodeWithKey(email, code, common.EmailVerificationPurpose)
|
||||
subject := fmt.Sprintf("%s邮箱验证邮件", config.SystemName)
|
||||
content := fmt.Sprintf("<p>您好,你正在进行%s邮箱验证。</p>"+
|
||||
"<p>您的验证码为: <strong>%s</strong></p>"+
|
||||
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", config.SystemName, code, common.VerificationValidMinutes)
|
||||
subject := fmt.Sprintf("%s 邮箱验证邮件", config.SystemName)
|
||||
content := message.EmailTemplate(
|
||||
subject,
|
||||
fmt.Sprintf(`
|
||||
<p>您好!</p>
|
||||
<p>您正在进行 %s 邮箱验证。</p>
|
||||
<p>您的验证码为:</p>
|
||||
<p style="font-size: 24px; font-weight: bold; color: #333; background-color: #f8f8f8; padding: 10px; text-align: center; border-radius: 4px;">%s</p>
|
||||
<p style="color: #666;">验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>
|
||||
`, config.SystemName, code, common.VerificationValidMinutes),
|
||||
)
|
||||
err := message.SendEmail(subject, email, content)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@ -138,7 +147,7 @@ func SendPasswordResetEmail(c *gin.Context) {
|
||||
if err := common.Validate.Var(email, "required,email"); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
"message": i18n.Translate(c, "invalid_parameter"),
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -152,16 +161,26 @@ func SendPasswordResetEmail(c *gin.Context) {
|
||||
code := common.GenerateVerificationCode(0)
|
||||
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
|
||||
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", config.ServerAddress, email, code)
|
||||
subject := fmt.Sprintf("%s密码重置", config.SystemName)
|
||||
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
|
||||
"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
|
||||
"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
|
||||
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", config.SystemName, link, link, common.VerificationValidMinutes)
|
||||
subject := fmt.Sprintf("%s 密码重置", config.SystemName)
|
||||
content := message.EmailTemplate(
|
||||
subject,
|
||||
fmt.Sprintf(`
|
||||
<p>您好!</p>
|
||||
<p>您正在进行 %s 密码重置。</p>
|
||||
<p>请点击下面的按钮进行密码重置:</p>
|
||||
<p style="text-align: center; margin: 30px 0;">
|
||||
<a href="%s" style="background-color: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">重置密码</a>
|
||||
</p>
|
||||
<p style="color: #666;">如果按钮无法点击,请复制以下链接到浏览器中打开:</p>
|
||||
<p style="background-color: #f8f8f8; padding: 10px; border-radius: 4px; word-break: break-all;">%s</p>
|
||||
<p style="color: #666;">重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>
|
||||
`, config.SystemName, link, link, common.VerificationValidMinutes),
|
||||
)
|
||||
err := message.SendEmail(subject, email, content)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
"message": fmt.Sprintf("%s%s", i18n.Translate(c, "send_email_failed"), err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -183,7 +202,7 @@ func ResetPassword(c *gin.Context) {
|
||||
if req.Email == "" || req.Token == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
"message": i18n.Translate(c, "invalid_parameter"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -2,12 +2,14 @@ package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/helper"
|
||||
"github.com/songquanpeng/one-api/model"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/helper"
|
||||
"github.com/songquanpeng/one-api/common/i18n"
|
||||
"github.com/songquanpeng/one-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@ -38,7 +40,7 @@ func UpdateOption(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
"message": i18n.Translate(c, "invalid_parameter"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -3,17 +3,19 @@ package controller
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/ctxkey"
|
||||
"github.com/songquanpeng/one-api/common/random"
|
||||
"github.com/songquanpeng/one-api/model"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/ctxkey"
|
||||
"github.com/songquanpeng/one-api/common/i18n"
|
||||
"github.com/songquanpeng/one-api/common/random"
|
||||
"github.com/songquanpeng/one-api/model"
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
@ -33,7 +35,7 @@ func Login(c *gin.Context) {
|
||||
err := json.NewDecoder(c.Request.Body).Decode(&loginRequest)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "无效的参数",
|
||||
"message": i18n.Translate(c, "invalid_parameter"),
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
@ -42,7 +44,7 @@ func Login(c *gin.Context) {
|
||||
password := loginRequest.Password
|
||||
if username == "" || password == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "无效的参数",
|
||||
"message": i18n.Translate(c, "invalid_parameter"),
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
@ -129,14 +131,14 @@ func Register(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
"message": i18n.Translate(c, "invalid_parameter"),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err := common.Validate.Struct(&user); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "输入不合法 " + err.Error(),
|
||||
"message": i18n.Translate(c, "invalid_input"),
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -369,7 +371,7 @@ func UpdateUser(c *gin.Context) {
|
||||
if err != nil || updatedUser.Id == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
"message": i18n.Translate(c, "invalid_parameter"),
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -379,7 +381,7 @@ func UpdateUser(c *gin.Context) {
|
||||
if err := common.Validate.Struct(&updatedUser); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "输入不合法 " + err.Error(),
|
||||
"message": i18n.Translate(c, "invalid_input"),
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -433,7 +435,7 @@ func UpdateSelf(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
"message": i18n.Translate(c, "invalid_parameter"),
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -543,14 +545,14 @@ func CreateUser(c *gin.Context) {
|
||||
if err != nil || user.Username == "" || user.Password == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
"message": i18n.Translate(c, "invalid_parameter"),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err := common.Validate.Struct(&user); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "输入不合法 " + err.Error(),
|
||||
"message": i18n.Translate(c, "invalid_input"),
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -599,7 +601,7 @@ func ManageUser(c *gin.Context) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的参数",
|
||||
"message": i18n.Translate(c, "invalid_parameter"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
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=
|
||||
|
778
i18n/en.json
778
i18n/en.json
@ -1,778 +0,0 @@
|
||||
{
|
||||
"$%.6f 额度": "$%.6f quota",
|
||||
"%d 点额度": "%d point quota",
|
||||
"尚未实现": "Not yet implemented",
|
||||
"余额不足": "Insufficient balance",
|
||||
"危险操作": "Hazardous operations",
|
||||
"输入你的账户名": "Enter your account name",
|
||||
"确认删除": "Confirm Delete",
|
||||
"确认绑定": "Confirm Binding",
|
||||
"您正在删除自己的帐户,将清空所有数据且不可恢复": "You are deleting your account, all data will be cleared and unrecoverable.",
|
||||
"\"渠道「%s」(#%d)已被禁用\"": "\"Channel %s (#%d) has been disabled\"",
|
||||
"渠道「%s」(#%d)已被禁用,原因:%s": "Channel %s (#%d) has been disabled, reason: %s",
|
||||
"测试已在运行中": "Test is already running",
|
||||
"响应时间 %.2fs 超过阈值 %.2fs": "Response time %.2fs exceeds threshold %.2fs",
|
||||
"渠道测试完成": "Channel test completed",
|
||||
"渠道测试完成,如果没有收到禁用通知,说明所有渠道都正常": "Channel test completed, if you have not received the disable notification, it means that all channels are normal",
|
||||
"无法连接至 GitHub 服务器,请稍后重试!": "Unable to connect to GitHub server, please try again later!",
|
||||
"返回值非法,用户字段为空,请稍后重试!": "The return value is illegal, the user field is empty, please try again later!",
|
||||
"管理员未开启通过 GitHub 登录以及注册": "The administrator did not turn on login and registration via GitHub",
|
||||
"管理员关闭了新用户注册": "The administrator has turned off new user registration",
|
||||
"用户已被封禁": "User has been banned",
|
||||
"该 GitHub 账户已被绑定": "The GitHub account has been bound",
|
||||
"邮箱地址已被占用": "Email address is occupied",
|
||||
"%s邮箱验证邮件": "%s Email verification email",
|
||||
"<p>您好,你正在进行%s邮箱验证。</p>": "<p>Hello, you are verifying %s email.</p>",
|
||||
"<p>您的验证码为: <strong>%s</strong></p>": "<p>Your verification code is: <strong>%s</strong></p>",
|
||||
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>": "<p>The verification code is valid within %d minutes. If it is not your operation, please ignore it.</p>",
|
||||
"无效的参数": "Invalid parameter",
|
||||
"该邮箱地址未注册": "The email address is not registered",
|
||||
"%s密码重置": "%s Password reset",
|
||||
"<p>您好,你正在进行%s密码重置。</p>": "<p>Hello, you are resetting %s password.</p>",
|
||||
"<p>点击<a href='%s'>此处</a>进行密码重置。</p>": "<p>Click <a href='%s'>here</a> to reset your password.</p>",
|
||||
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>": "<p>The reset link is valid within %d minutes. If it is not your operation, please ignore it.</p>",
|
||||
"重置链接非法或已过期": "Reset link is illegal or expired",
|
||||
"无法启用 GitHub OAuth,请先填入 GitHub Client ID 以及 GitHub Client Secret!": "Unable to enable GitHub OAuth, please fill in GitHub Client ID and GitHub Client Secret first!",
|
||||
"无法启用微信登录,请先填入微信登录相关配置信息!": "Unable to enable WeChat login, please fill in the relevant configuration information for WeChat login first!",
|
||||
"无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!": "Unable to enable Turnstile verification, please fill in the relevant configuration information for Turnstile verification first!",
|
||||
"兑换码名称长度必须在1-20之间": "The length of the redemption code name must be between 1-20",
|
||||
"兑换码个数必须大于0": "The number of redemption codes must be greater than 0",
|
||||
"一次兑换码批量生成的个数不能大于 100": "The number of redemption codes generated in a batch cannot be greater than 100",
|
||||
"通过令牌「%s」使用模型 %s 消耗 %s(模型倍率 %.2f,分组倍率 %.2f)": "Using model %s with token %s consumes %s (model rate %.2f, group rate %.2f)",
|
||||
"当前分组上游负载已饱和,请稍后再试": "The current group load is saturated, please try again later",
|
||||
"令牌名称过长": "Token name is too long",
|
||||
"令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "The token has expired and cannot be enabled. Please modify the expiration time of the token, or set it to never expire.",
|
||||
"令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "The available quota of the token has been used up and cannot be enabled. Please modify the remaining quota of the token, or set it to unlimited quota",
|
||||
"管理员关闭了密码登录": "The administrator has turned off password login",
|
||||
"无法保存会话信息,请重试": "Unable to save session information, please try again",
|
||||
"管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册": "The administrator has turned off registration via password. Please use the form of third-party account verification to register",
|
||||
"输入不合法 ": "Input is illegal ",
|
||||
"管理员开启了邮箱验证,请输入邮箱地址和验证码": "The administrator has turned on email verification, please enter the email address and verification code",
|
||||
"验证码错误或已过期": "Verification code error or expired",
|
||||
"无权获取同级或更高等级用户的信息": "No permission to get information of users at the same level or higher",
|
||||
"请重试,系统生成的 UUID 竟然重复了!": "Please try again, the system-generated UUID is actually duplicated!",
|
||||
"输入不合法": "Input is illegal",
|
||||
"无权更新同权限等级或更高权限等级的用户信息": "No permission to update user information with the same permission level or higher permission level",
|
||||
"管理员将用户额度从 %s修改为 %s": "The administrator changed the user quota from %s to %s",
|
||||
"无权删除同权限等级或更高权限等级的用户": "No permission to delete users with the same permission level or higher permission level",
|
||||
"无法创建权限大于等于自己的用户": "Unable to create users with permissions greater than or equal to your own",
|
||||
"用户不存在": "User does not exist",
|
||||
"无法禁用超级管理员用户": "Unable to disable super administrator user",
|
||||
"无法删除超级管理员用户": "Unable to delete super administrator user",
|
||||
"普通管理员用户无法提升其他用户为管理员": "Ordinary administrator users cannot promote other users to administrators",
|
||||
"该用户已经是管理员": "The user is already an administrator",
|
||||
"无法降级超级管理员用户": "Unable to downgrade super administrator user",
|
||||
"该用户已经是普通用户": "The user is already an ordinary user",
|
||||
"管理员未开启通过微信登录以及注册": "The administrator has not enabled login and registration via WeChat",
|
||||
"该微信账号已被绑定": "The WeChat account has been bound",
|
||||
"无权进行此操作,未登录且未提供 access token": "No permission to perform this operation, not logged in and no access token provided",
|
||||
"无权进行此操作,access token 无效": "No permission to perform this operation, access token is invalid",
|
||||
"无权进行此操作,权限不足": "No permission to perform this operation, insufficient permissions",
|
||||
"普通用户不支持指定渠道": "Ordinary users do not support specifying channels",
|
||||
"无效的渠道 ID": "Invalid channel ID",
|
||||
"该渠道已被禁用": "The channel has been disabled",
|
||||
"无效的请求": "Invalid request",
|
||||
"无可用渠道": "No available channels",
|
||||
"Turnstile token 为空": "Turnstile token is empty",
|
||||
"Turnstile 校验失败,请刷新重试!": "Turnstile verification failed, please refresh and try again!",
|
||||
"id 为空!": "id is empty!",
|
||||
"未提供兑换码": "No redemption code provided",
|
||||
"无效的 user id": "Invalid user id",
|
||||
"无效的兑换码": "Invalid redemption code",
|
||||
"该兑换码已被使用": "The redemption code has been used",
|
||||
"通过兑换码充值 %s": "Recharge %s through redemption code",
|
||||
"未提供令牌": "No token provided",
|
||||
"该令牌状态不可用": "The token status is not available",
|
||||
"该令牌已过期": "The token has expired",
|
||||
"该令牌额度已用尽": "The token quota has been used up",
|
||||
"无效的令牌": "Invalid token",
|
||||
"令牌验证失败": "Token verification failed",
|
||||
"id 或 userId 为空!": "id or userId is empty!",
|
||||
"quota 不能为负数!": "quota cannot be negative!",
|
||||
"令牌额度不足": "Insufficient token quota",
|
||||
"用户额度不足": "Insufficient user quota",
|
||||
"您的额度即将用尽": "Your quota is about to run out",
|
||||
"您的额度已用尽": "Your quota has been used up",
|
||||
"%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。<br/>充值链接:<a href='%s'>%s</a>": "%s, the current remaining quota is %d, in order not to affect your use, please recharge in time. <br/> Recharge link: <a href='%s'>%s</a>",
|
||||
"affCode 为空!": "affCode is empty!",
|
||||
"新用户注册赠送 %s": "New user registration gives %s",
|
||||
"使用邀请码赠送 %s": "Use invitation code to give %s",
|
||||
"邀请用户赠送 %s": "Invite users to give %s",
|
||||
"用户名或密码为空": "Username or password is empty",
|
||||
"用户名或密码错误,或用户已被封禁": "Username or password is wrong, or user has been banned",
|
||||
"email 为空!": "email is empty!",
|
||||
"GitHub id 为空!": "GitHub id is empty!",
|
||||
"WeChat id 为空!": "WeChat id is empty!",
|
||||
"username 为空!": "username is empty!",
|
||||
"邮箱地址或密码为空!": "Email address or password is empty!",
|
||||
"OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用": "OpenAI interface aggregation management, supports multiple channels including Azure, can be used for secondary distribution management key, only single executable file, Docker image has been packaged, one-click deployment, out of the box",
|
||||
"未知类型": "Unknown type",
|
||||
"不支持": "Not supported",
|
||||
"操作成功完成!": "Operation completed successfully!",
|
||||
"已启用": "Enabled",
|
||||
"已禁用": "Disabled",
|
||||
"未知状态": "Unknown status",
|
||||
" 秒": "s",
|
||||
" 分钟 ": " m ",
|
||||
" 小时 ": " h ",
|
||||
" 天 ": " d ",
|
||||
" 个月 ": " M ",
|
||||
" 年 ": " y ",
|
||||
"未测试": "Not tested",
|
||||
"渠道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test succeeded, time consumed ${time.toFixed(2)} s.",
|
||||
"已成功开始测试所有渠道,请刷新页面查看结果。": "All channels have been successfully tested, please refresh the page to view the results.",
|
||||
"已成功开始测试所有已启用渠道,请刷新页面查看结果。": "All enabled channels have been successfully tested, please refresh the page to view the results.",
|
||||
"渠道 ${name} 余额更新成功!": "Channel ${name} balance updated successfully!",
|
||||
"已更新完毕所有已启用渠道余额!": "The balance of all enabled channels has been updated!",
|
||||
"搜索渠道的 ID,名称和密钥 ...": "Search for channel ID, name and key ...",
|
||||
"名称": "Name",
|
||||
"分组": "Group",
|
||||
"类型": "Type",
|
||||
"状态": "Status",
|
||||
"响应时间": "Response time",
|
||||
"余额": "Balance",
|
||||
"操作": "Operation",
|
||||
"未更新": "Not updated",
|
||||
"测试": "Test",
|
||||
"更新余额": "Update balance",
|
||||
"删除": "Delete",
|
||||
"删除渠道 {channel.name}": "Delete channel {channel.name}",
|
||||
"禁用": "Disable",
|
||||
"启用": "Enable",
|
||||
"编辑": "Edit",
|
||||
"添加新的渠道": "Add a new channel",
|
||||
"测试所有渠道": "Test all channels",
|
||||
"测试所有已启用渠道": "Test all enabled channels",
|
||||
"更新所有已启用渠道余额": "Update the balance of all enabled channels",
|
||||
"刷新": "Refresh",
|
||||
"处理中...": "Processing...",
|
||||
"绑定成功!": "Binding succeeded!",
|
||||
"登录成功!": "Login succeeded!",
|
||||
"操作失败,重定向至登录界面中...": "Operation failed, redirecting to the login page...",
|
||||
"出现错误,第 ${count} 次重试中...": "An error occurred, retrying for the ${count} time...",
|
||||
"首页": "Home",
|
||||
"渠道": "Channel",
|
||||
"令牌": "Token",
|
||||
"兑换": "Redeem",
|
||||
"充值": "Recharge",
|
||||
"用户": "User",
|
||||
"日志": "Log",
|
||||
"设置": "Settings",
|
||||
"关于": "About",
|
||||
"聊天": "Chat",
|
||||
"注销成功!": "Logout succeeded!",
|
||||
"注销": "Logout",
|
||||
"登录": "Login",
|
||||
"注册": "Register",
|
||||
"加载{name}中...": "Loading {name}...",
|
||||
"未登录或登录已过期,请重新登录!": "Not logged in or login has expired, please log in again!",
|
||||
"用户登录": "User login",
|
||||
"\"用户名\"": "\"Username\"",
|
||||
"\"密码\"": "\"Password\"",
|
||||
"忘记密码?": "Forget password?",
|
||||
"点击重置": "Click to reset",
|
||||
"; 没有账户?": "; No account?",
|
||||
"点击注册": "Click to register",
|
||||
"微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "Scan the QR code of WeChat to follow the official account, enter \"verification code\" to get the verification code (valid within three minutes)",
|
||||
"\"验证码\"": "\"Verification code\"",
|
||||
"全部用户": "All users",
|
||||
"当前用户": "Current user",
|
||||
"'全部'": "'All'",
|
||||
"'充值'": "'Recharge'",
|
||||
"'消费'": "'Consumption'",
|
||||
"'管理'": "'Management'",
|
||||
"'系统'": "'System'",
|
||||
" 充值 ": " Recharge ",
|
||||
" 消费 ": " Consumption ",
|
||||
" 管理 ": " Management ",
|
||||
" 系统 ": " System ",
|
||||
" 未知 ": " Unknown ",
|
||||
"时间": "Time",
|
||||
"详情": "Details",
|
||||
"选择模式": "Select mode",
|
||||
"选择明细分类": "Select details category",
|
||||
"模型倍率不是合法的 JSON 字符串": "Model rate is not a valid JSON string",
|
||||
"分组倍率不是合法的 JSON 字符串": "Group rate is not a valid JSON string",
|
||||
"通用设置": "General Settings",
|
||||
"充值链接": "Recharge Link",
|
||||
"例如发卡网站的购买链接": "For example, the purchase link of the card issuing website",
|
||||
"聊天页面链接": "Chat Page Link",
|
||||
"例如 ChatGPT Next Web 的部署地址": "For example, the deployment address of ChatGPT Next Web",
|
||||
"单位美元额度": "Unit Dollar Quota",
|
||||
"一单位货币能兑换的额度": "Quota that can be exchanged for one unit of currency",
|
||||
"启用额度消费日志记录": "Enable quota consumption log recording",
|
||||
"以货币形式显示额度": "Display quota in the form of currency",
|
||||
"相关 API 显示令牌额度而非用户额度": "Related API displays token quota instead of user quota",
|
||||
"保存通用设置": "Save General Settings",
|
||||
"监控设置": "Monitoring Settings",
|
||||
"最长响应时间": "Longest Response Time",
|
||||
"单位秒": "Unit in seconds",
|
||||
"当运行渠道全部测试时": "When all operating channels are tested",
|
||||
"超过此时间将自动禁用渠道": "Channels will be automatically disabled if this time is exceeded",
|
||||
"额度提醒阈值": "Quota reminder threshold",
|
||||
"低于此额度时将发送邮件提醒用户": "Email will be sent to remind users when the quota is below this",
|
||||
"失败时自动禁用渠道": "Automatically disable the channel when it fails",
|
||||
"保存监控设置": "Save Monitoring Settings",
|
||||
"额度设置": "Quota Settings",
|
||||
"新用户初始额度": "Initial quota for new users",
|
||||
"例如": "For example",
|
||||
"请求预扣费额度": "Request for pre-deducted quota",
|
||||
"请求结束后多退少补": "Refund more or less after the request ends",
|
||||
"邀请新用户奖励额度": "Invite new users to reward quota",
|
||||
"新用户使用邀请码奖励额度": "New user rewards quota using invitation code",
|
||||
"保存额度设置": "Save Quota Settings",
|
||||
"倍率设置": "Rate Settings",
|
||||
"模型倍率": "Model rate",
|
||||
"为一个 JSON 文本": "Is a JSON text",
|
||||
"键为模型名称": "Key is model name",
|
||||
"值为倍率": "Value is the rate",
|
||||
"分组倍率": "Group rate",
|
||||
"键为分组名称": "Key is group name",
|
||||
"保存倍率设置": "Save Rate Settings",
|
||||
"已是最新版本": "Is the latest version",
|
||||
"检查更新": "Check for updates",
|
||||
"公告": "Announcement",
|
||||
"在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code",
|
||||
"保存公告": "Save Announcement",
|
||||
"个性化设置": "Personalization Settings",
|
||||
"系统名称": "System Name",
|
||||
"在此输入系统名称": "Enter the system name here",
|
||||
"设置系统名称": "Set system name",
|
||||
"图片地址": "Image URL",
|
||||
"在此输入 Logo 图片地址": "Enter the Logo image URL here",
|
||||
"首页内容": "Home Page Content",
|
||||
"在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "Enter the homepage content here, supports Markdown & HTML code. Once set, the status information of the homepage will not be displayed. If a link is entered, it will be used as the src attribute of the iframe, allowing you to set any webpage as the homepage.",
|
||||
"保存首页内容": "Save Home Page Content",
|
||||
"在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面": "Enter new about content here, supports Markdown & HTML code. If a link is entered, it will be used as the src attribute of the iframe, allowing you to set any webpage as the about page.",
|
||||
"保存关于": "Save About",
|
||||
"移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "Removal of One API copyright mark must first be authorized. Project maintenance requires a lot of effort. If this project is meaningful to you, please actively support it.",
|
||||
"页脚": "Footer",
|
||||
"在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码": "Enter the new footer here, leave blank to use the default footer, supports HTML code.",
|
||||
"设置页脚": "Set Footer",
|
||||
"新版本": "New Version",
|
||||
"关闭": "Close",
|
||||
"密码已重置并已复制到剪贴板": "Password has been reset and copied to clipboard",
|
||||
"密码重置确认": "Password Reset Confirmation",
|
||||
"邮箱地址": "Email Address",
|
||||
"提交": "Submit",
|
||||
"请稍后几秒重试": "Please retry in a few seconds",
|
||||
"正在检查用户环境": "Checking user environment",
|
||||
"重置邮件发送成功": "Reset mail sent successfully",
|
||||
"请检查邮箱": "Please check your email",
|
||||
"密码重置": "Password Reset",
|
||||
"令牌已重置并已复制到剪贴板": "Token has been reset and copied to clipboard",
|
||||
"邀请链接已复制到剪切板": "Invitation link has been copied to clipboard",
|
||||
"微信账户绑定成功": "WeChat account binding succeeded",
|
||||
"验证码发送成功": "Verification code sent successfully",
|
||||
"邮箱账户绑定成功": "Email account binding succeeded",
|
||||
"注意": "Note",
|
||||
"此处生成的令牌用于系统管理": "The token generated here is used for system management",
|
||||
"而非用于请求 OpenAI 相关的服务": "Not for requesting OpenAI related services",
|
||||
"请知悉": "Please be aware",
|
||||
"更新个人信息": "Update Personal Information",
|
||||
"生成系统访问令牌": "Generate System Access Token",
|
||||
"复制邀请链接": "Copy Invitation Link",
|
||||
"账号绑定": "Account Binding",
|
||||
"绑定微信账号": "Bind WeChat Account",
|
||||
"微信扫码关注公众号": "Scan the QR code with WeChat to follow the official account",
|
||||
"输入": "Enter",
|
||||
"验证码": "Verification Code",
|
||||
"获取验证码": "Get Verification Code",
|
||||
"三分钟内有效": "Valid for three minutes",
|
||||
"绑定": "Bind",
|
||||
"绑定 GitHub 账号": "Bind GitHub Account",
|
||||
"绑定邮箱地址": "Bind Email Address",
|
||||
"输入邮箱地址": "Enter Email Address",
|
||||
"未使用": "Unused",
|
||||
"已使用": "Used",
|
||||
"操作成功完成": "Operation successfully completed",
|
||||
"搜索兑换码的 ID 和名称": "Search for ID and name",
|
||||
"额度": "Quota",
|
||||
"创建时间": "Creation Time",
|
||||
"兑换时间": "Redemption Time",
|
||||
"尚未兑换": "Not yet redeemed",
|
||||
"已复制到剪贴板": "Copied to clipboard",
|
||||
"无法复制到剪贴板": "Unable to copy to clipboard",
|
||||
"请手动复制": "Please copy manually",
|
||||
"已将兑换码填入搜索框": "The voucher code has been filled into the search box",
|
||||
"复制": "Copy",
|
||||
"添加新的兑换码": "Add a new voucher",
|
||||
"密码长度不得小于 8 位": "Password length must not be less than 8 characters",
|
||||
"两次输入的密码不一致": "The two passwords entered do not match",
|
||||
"注册成功": "Registration succeeded",
|
||||
"请稍后几秒重试,Turnstile 正在检查用户环境": "Please retry in a few seconds, Turnstile is checking user environment",
|
||||
"验证码发送成功,请检查你的邮箱": "Verification code sent successfully, please check your email",
|
||||
"新用户注册": "New User Registration",
|
||||
"输入用户名,最长 12 位": "Enter username, up to 12 characters",
|
||||
"输入密码,最短 8 位,最长 20 位": "Enter password, at least 8 characters and up to 20 characters",
|
||||
"输入验证码": "Enter Verification Code",
|
||||
"已有账户": "Already have an account",
|
||||
"点击登录": "Click to log in",
|
||||
"服务器地址": "Server Address",
|
||||
"更新服务器地址": "Update Server Address",
|
||||
"配置登录注册": "Configure Login/Registration",
|
||||
"允许通过密码进行登录": "Allow login via password",
|
||||
"允许通过密码进行注册": "Allow registration via password",
|
||||
"通过密码注册时需要进行邮箱验证": "Email verification is required when registering via password",
|
||||
"允许通过 GitHub 账户登录 & 注册": "Allow login & registration via GitHub account",
|
||||
"允许通过微信登录 & 注册": "Allow login & registration via WeChat",
|
||||
"允许新用户注册(此项为否时,新用户将无法以任何方式进行注册": "Allow new user registration (if this option is off, new users will not be able to register in any way",
|
||||
"启用 Turnstile 用户校验": "Enable Turnstile user verification",
|
||||
"配置 SMTP": "Configure SMTP",
|
||||
"用以支持系统的邮件发送": "To support the system email sending",
|
||||
"SMTP 服务器地址": "SMTP Server Address",
|
||||
"例如:smtp.qq.com": "For example: smtp.qq.com",
|
||||
"SMTP 端口": "SMTP Port",
|
||||
"默认: 587": "Default: 587",
|
||||
"SMTP 账户": "SMTP Account",
|
||||
"通常是邮箱地址": "Usually an email address",
|
||||
"发送者邮箱": "Sender email",
|
||||
"通常和邮箱地址保持一致": "Usually consistent with the email address",
|
||||
"SMTP 访问凭证": "SMTP Access Credential",
|
||||
"敏感信息不会发送到前端显示": "Sensitive information will not be displayed in the frontend",
|
||||
"保存 SMTP 设置": "Save SMTP Settings",
|
||||
"配置 GitHub OAuth App": "Configure GitHub OAuth App",
|
||||
"用以支持通过 GitHub 进行登录注册": "To support login & registration via GitHub",
|
||||
"点击此处": "Click here",
|
||||
"管理你的 GitHub OAuth App": "Manage your GitHub OAuth App",
|
||||
"输入你注册的 GitHub OAuth APP 的 ID": "Enter your registered GitHub OAuth APP ID",
|
||||
"保存 GitHub OAuth 设置": "Save GitHub OAuth Settings",
|
||||
"配置 WeChat Server": "Configure WeChat Server",
|
||||
"用以支持通过微信进行登录注册": "To support login & registration via WeChat",
|
||||
"了解 WeChat Server": "Learn about WeChat Server",
|
||||
"WeChat Server 访问凭证": "WeChat Server Access Credential",
|
||||
"微信公众号二维码图片链接": "WeChat Public Account QR Code Image Link",
|
||||
"输入一个图片链接": "Enter an image link",
|
||||
"保存 WeChat Server 设置": "Save WeChat Server Settings",
|
||||
"配置 Turnstile": "Configure Turnstile",
|
||||
"用以支持用户校验": "To support user verification",
|
||||
"管理你的 Turnstile Sites,推荐选择 Invisible Widget Type": "Manage your Turnstile Sites, recommend selecting Invisible Widget Type",
|
||||
"输入你注册的 Turnstile Site Key": "Enter your registered Turnstile Site Key",
|
||||
"保存 Turnstile 设置": "Save Turnstile Settings",
|
||||
"已过期": "Expired",
|
||||
"已耗尽": "Exhausted",
|
||||
"搜索令牌的名称 ...": "Search for the name of the token...",
|
||||
"已用额度": "Quota used",
|
||||
"剩余额度": "Remaining quota",
|
||||
"过期时间": "Expiration time",
|
||||
"无": "None",
|
||||
"无限制": "Unlimited",
|
||||
"永不过期": "Never expires",
|
||||
"无法复制到剪贴板,请手动复制,已将令牌填入搜索框": "Unable to copy to clipboard, please copy manually, the token has been entered into the search box",
|
||||
"删除令牌": "Delete Token",
|
||||
"添加新的令牌": "Add New Token",
|
||||
"普通用户": "Regular User",
|
||||
"管理员": "Admin",
|
||||
"超级管理员": "Super Admin",
|
||||
"未知身份": "Unknown Identity",
|
||||
"已激活": "Activated",
|
||||
"已封禁": "Banned",
|
||||
"搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...": "Search user ID, username, display name, and email address...",
|
||||
"用户名": "Username",
|
||||
"统计信息": "Statistics",
|
||||
"用户角色": "User Role",
|
||||
"未绑定邮箱地址": "Email not bound",
|
||||
"请求次数": "Number of Requests",
|
||||
"提升": "Promote",
|
||||
"降级": "Demote",
|
||||
"删除用户": "Delete User",
|
||||
"添加新的用户": "Add New User",
|
||||
"自定义": "Custom",
|
||||
"等价金额": "Equivalent Amount",
|
||||
"未登录或登录已过期,请重新登录": "Not logged in or login has expired, please log in again",
|
||||
"请求次数过多,请稍后再试": "Too many requests, please try again later",
|
||||
"服务器内部错误,请联系管理员": "Server internal error, please contact the administrator",
|
||||
"本站仅作演示之用,无服务端": "This site is for demonstration purposes only, no server-side",
|
||||
"超级管理员未设置充值链接!": "Super administrator has not set the recharge link!",
|
||||
"错误:": "Error: ",
|
||||
"新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面": "New version available: ${data.version}, please refresh the page using shortcut Shift + F5",
|
||||
"无法正常连接至服务器": "Unable to connect to the server normally",
|
||||
"管理渠道": "Manage Channels",
|
||||
"系统状况": "System Status",
|
||||
"系统信息": "System Information",
|
||||
"系统信息总览": "System Information Overview",
|
||||
"版本": "Version",
|
||||
"源码": "Source Code",
|
||||
"启动时间": "Startup Time",
|
||||
"系统配置": "System Configuration",
|
||||
"系统配置总览": "System Configuration Overview",
|
||||
"邮箱验证": "Email Verification",
|
||||
"未启用": "Not Enabled",
|
||||
"GitHub 身份验证": "GitHub Authentication",
|
||||
"微信身份验证": "WeChat Authentication",
|
||||
"Turnstile 用户校验": "Turnstile User Verification",
|
||||
"创建新的渠道": "Create New Channel",
|
||||
"镜像": "Mirror",
|
||||
"请输入镜像站地址,格式为:https://domain.com,可不填,不填则使用渠道默认值": "Please enter the mirror site address, the format is: https://domain.com, it can be left blank, if left blank, the default value of the channel will be used",
|
||||
"模型": "Model",
|
||||
"请选择该渠道所支持的模型": "Please select the model supported by the channel",
|
||||
"填入基础模型": "Fill in the basic model",
|
||||
"填入所有模型": "Fill in all models",
|
||||
"清除所有模型": "Clear all models",
|
||||
"密钥": "Key",
|
||||
"请输入密钥": "Please enter the key",
|
||||
"批量创建": "Batch Create",
|
||||
"更新渠道信息": "Update Channel Information",
|
||||
"我的令牌": "My Tokens",
|
||||
"管理兑换码": "Manage Redeem Codes",
|
||||
"兑换码": "Redeem Code",
|
||||
"管理用户": "Manage Users",
|
||||
"额度明细": "Quota Details",
|
||||
"个人设置": "Personal Settings",
|
||||
"运营设置": "Operation Settings",
|
||||
"系统设置": "System Settings",
|
||||
"其他设置": "Other Settings",
|
||||
"项目仓库地址": "Project Repository Address",
|
||||
"可在设置页面设置关于内容,支持 HTML & Markdown": "You can set the content about in the settings page, support HTML & Markdown",
|
||||
"由{' '}": "built by{' '}",
|
||||
"构建,源代码遵循{' '}": ", the source code licensed under{' '}",
|
||||
"MIT 协议": "MIT License",
|
||||
"充值额度": "Recharge Quota",
|
||||
"获取兑换码": "Get Redeem Code",
|
||||
"一个月后过期": "Expires after one month",
|
||||
"一天后过期": "Expires after one day",
|
||||
"一小时后过期": "Expires after one hour",
|
||||
"一分钟后过期": "Expires after one minute",
|
||||
"创建新的令牌": "Create New Token",
|
||||
"注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。": "Note that the quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account.",
|
||||
"设为无限额度": "Set to unlimited quota",
|
||||
"更新令牌信息": "Update Token Information",
|
||||
"请输入充值码!": "Please enter the recharge code!",
|
||||
"请输入名称": "Please enter a name",
|
||||
"请输入密钥,一行一个": "Please enter the key, one per line",
|
||||
"请输入额度": "Please enter the quota",
|
||||
"令牌创建成功": "Token created successfully",
|
||||
"令牌更新成功": "Token updated successfully",
|
||||
"充值成功!": "Recharge successful!",
|
||||
"更新用户信息": "Update User Information",
|
||||
"请输入新的用户名": "Please enter a new username",
|
||||
"密码": "Password",
|
||||
"请输入新的密码": "Please enter a new password",
|
||||
"显示名称": "Display Name",
|
||||
"请输入新的显示名称": "Please enter a new display name",
|
||||
"已绑定的 GitHub 账户": "GitHub Account Bound",
|
||||
"此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "This item is read-only. Users need to bind through the relevant binding button on the personal settings page, and cannot be modified directly",
|
||||
"已绑定的微信账户": "WeChat Account Bound",
|
||||
"已绑定的邮箱账户": "Email Account Bound",
|
||||
"用户信息更新成功!": "User information updated successfully!",
|
||||
"模型倍率 %.2f,分组倍率 %.2f": "model rate %.2f, group rate %.2f",
|
||||
"模型倍率 %.2f,分组倍率 %.2f,补全倍率 %.2f": "model rate %.2f, group rate %.2f, completion rate %.2f",
|
||||
"使用明细(总消耗额度:{renderQuota(stat.quota)})": "Usage Details (Total Consumption Quota: {renderQuota(stat.quota)})",
|
||||
"用户名称": "User Name",
|
||||
"令牌名称": "Token Name",
|
||||
"默认令牌": "Default Token",
|
||||
"留空则查询全部用户": "Leave blank to query all users",
|
||||
"留空则查询全部令牌": "Leave blank to query all tokens",
|
||||
"模型名称": "Model Name",
|
||||
"留空则查询全部模型": "Leave blank to query all models",
|
||||
"起始时间": "Start Time",
|
||||
"结束时间": "End Time",
|
||||
"查询": "Query",
|
||||
"提示": "Prompt",
|
||||
"补全": "Completion",
|
||||
"消耗额度": "Used Quota",
|
||||
"可选值": "Optional Values",
|
||||
"渠道不存在:%d": "Channel does not exist: %d",
|
||||
"数据库一致性已被破坏,请联系管理员": "Database consistency has been broken, please contact the administrator",
|
||||
"使用近似的方式估算 token 数以减少计算量": "Estimate the number of tokens in an approximate way to reduce computational load",
|
||||
"请填写ChannelName和ChannelKey!": "Please fill in the ChannelName and ChannelKey!",
|
||||
"请至少选择一个Model!": "Please select at least one Model!",
|
||||
"加载首页内容失败": "Failed to load the homepage content",
|
||||
"加载关于内容失败": "Failed to load the About content",
|
||||
"兑换码更新成功!": "Redemption code updated successfully!",
|
||||
"兑换码创建成功!": "Redemption code created successfully!",
|
||||
"用户账户创建成功!": "User account created successfully!",
|
||||
"生成数量": "Generate quantity",
|
||||
"请输入生成数量": "Please enter the quantity to generate",
|
||||
"创建新用户账户": "Create new user account",
|
||||
"渠道更新成功!": "Channel updated successfully!",
|
||||
"渠道创建成功!": "Channel created successfully!",
|
||||
"请选择分组": "Please select a group",
|
||||
"更新兑换码信息": "Update redemption code information",
|
||||
"创建新的兑换码": "Create a new redemption code",
|
||||
"请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit the group ratio in the system settings page to add a new group:",
|
||||
"未找到所请求的页面": "The requested page was not found",
|
||||
"过期时间格式错误!": "Expiration time format error!",
|
||||
"请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制": "Please enter the expiration time, the format is yyyy-MM-dd HH:mm:ss, -1 means no limit",
|
||||
"此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:": "This is optional, it's a JSON text, the key is the model name requested by the user, and the value is the model name to be replaced, for example:",
|
||||
"此项可选,输入镜像站地址,格式为:": "This is optional, enter the mirror site address, the format is:",
|
||||
"模型映射": "Model mapping",
|
||||
"请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖": "Please enter the default API version, for example: 2023-03-15-preview, this configuration can be overridden by the actual request query parameters",
|
||||
"默认": "Default",
|
||||
"图片演示": "Image demo",
|
||||
"参数替换为你的部署名称(模型名称中的点会被剔除)": "Replace the parameter with your deployment name (dots in the model name will be removed)",
|
||||
"模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
|
||||
"取消无限额度": "Cancel unlimited quota",
|
||||
"取消": "Cancel",
|
||||
"请输入新的剩余额度": "Please enter the new remaining quota",
|
||||
"请输入单个兑换码中包含的额度": "Please enter the quota included in a single redemption code",
|
||||
"请输入用户名": "Please enter username",
|
||||
"请输入显示名称": "Please enter display name",
|
||||
"请输入密码": "Please enter password",
|
||||
"模型部署名称必须和模型名称保持一致": "The model deployment name must be consistent with the model name",
|
||||
",因为 One API 会把请求体中的 model": ", because One API will take the model in the request body",
|
||||
"请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT",
|
||||
"请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel",
|
||||
"Homepage URL 填": "Fill in the Homepage URL",
|
||||
"Authorization callback URL 填": "Fill in the Authorization callback URL",
|
||||
"请为渠道命名": "Please name the channel",
|
||||
"此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:": "This is optional, used to modify the model name in the request body, it's a JSON string, the key is the model name in the request, and the value is the model name to be replaced, for example:",
|
||||
"模型重定向": "Model redirection",
|
||||
"请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel",
|
||||
"注意,": "Note that, ",
|
||||
",图片演示。": "related image demo.",
|
||||
"令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!",
|
||||
"代理": "Proxy",
|
||||
"此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com",
|
||||
"取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?",
|
||||
"按照如下格式输入:": "Enter in the following format:",
|
||||
"模型版本": "Model version",
|
||||
"请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1": "Please enter the version of the Starfire model, note that it is the version number in the interface address, for example: v2.1",
|
||||
"点击查看": "click to view",
|
||||
"请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!": "Please make sure that the gpt-35-turbo model has been created on Azure, and the apiVersion has been filled in correctly!",
|
||||
"处理中...": "Processing...",
|
||||
"绑定成功!": "Binding successful!",
|
||||
"登录成功!": "Login successful!",
|
||||
"操作失败,重定向至登录界面中...": "Operation failed, redirecting to login screen...",
|
||||
"出现错误,第 ${count} 次重试中...": "An error occurred, retrying ${count}...",
|
||||
"首页": "Home",
|
||||
"渠道": "Channel",
|
||||
"令牌": "API Keys",
|
||||
"兑换": "Redeem",
|
||||
"充值": "Recharge",
|
||||
"用户": "Users",
|
||||
"日志": "Logs",
|
||||
"设置": "Settings",
|
||||
"关于": "About",
|
||||
"聊天": "Chat",
|
||||
"注销成功!": "Logout successful!",
|
||||
"注销": "Log out",
|
||||
"登录": "Log in",
|
||||
"注册": "Sign up",
|
||||
"加载{name}中...": "Loading {name}...",
|
||||
"未登录或登录已过期,请重新登录!": "Not logged in or login has expired, please log in again!",
|
||||
"请立刻修改默认密码!": "Please change the default password immediately!",
|
||||
"欢迎回来": "Welcome back",
|
||||
"没有账户?": "No account?",
|
||||
"立刻注册": "Sign up now",
|
||||
"用户名": "Username",
|
||||
"密码": "Password",
|
||||
"正在登录……": "Logging in...",
|
||||
"忘记密码": "Forgot password",
|
||||
"其他方式": "Other methods",
|
||||
"微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "Scan the QR code with WeChat, follow the official account and enter 'verification code' to get the verification code (valid within three minutes)",
|
||||
"验证码": "Verification code",
|
||||
"全部用户": "All users",
|
||||
"当前用户": "Current user",
|
||||
"全部": "All",
|
||||
"消费": "Consumption",
|
||||
"管理": "Management",
|
||||
"系统": "System",
|
||||
"未知": "Unknown",
|
||||
"其他模型": "Other models",
|
||||
"复制成功": "Copy successful",
|
||||
"使用明细": "Usages",
|
||||
"刷新": "Refresh",
|
||||
"收起面板": "Collapse panel",
|
||||
"展开面板": "Expand panel",
|
||||
"显示查询选项": "Show search options",
|
||||
"隐藏查询选项": "Hide search options",
|
||||
"用户名称": "User name",
|
||||
"可选值": "Optional values",
|
||||
"渠道 ID": "Channel ID",
|
||||
"令牌名称": "Key name",
|
||||
"模型名称": "Model name",
|
||||
"起始时间": "Start time",
|
||||
"结束时间": "End time",
|
||||
"查询": "Query",
|
||||
"隐藏条形图": "Hide bar chart",
|
||||
"显示条形图": "Show bar chart",
|
||||
"折线条形图只展示最新50条数据": "Line and bar charts only show the latest 50 pieces of data",
|
||||
"总消耗": "Total consumption",
|
||||
"总共调用了 {payload[0].value} 次": "A total of {payload[0].value} calls were made",
|
||||
"{model.name}: {model.value} 次": "{model.name}: {model.value} times",
|
||||
"总共调用了 {payload[0].value} 次 {payload[0].name}": "A total of {payload[0].value} {payload[0].name} calls were made",
|
||||
"总消耗额度": "Total consumption limit",
|
||||
"暂无数据": "No data available",
|
||||
"更多数据统计图形即将到来,敬请期待!": "More data statistics graphics are coming soon, stay tuned!",
|
||||
"复制用户名": "Copy username",
|
||||
"{`共 ${counts} 条数据`}": "{`A total of ${counts} pieces of data`}",
|
||||
"共 0 条数据": "A total of 0 pieces of data",
|
||||
"选择明细分类": "Select detail category",
|
||||
"模型倍率": "model rate",
|
||||
"分组倍率": "group rate",
|
||||
"新密码已复制到剪贴板:": "New password has been copied to the clipboard:",
|
||||
"密码重置确认": "Password reset confirmation",
|
||||
"邮箱地址": "Email address",
|
||||
"新密码": "New password",
|
||||
"密码已复制到剪贴板:": "Password has been copied to the clipboard:",
|
||||
"密码重置完成": "Password reset complete",
|
||||
"提交": "Submit",
|
||||
"返回登录": "Return to login",
|
||||
"请稍后重试,浏览器环境检查未通过": "Please try again later, browser environment check failed",
|
||||
"重置邮件发送成功,请检查邮箱!": "Reset email sent successfully, please check your email!",
|
||||
"密码重置": "Password reset",
|
||||
"重试": "Retry",
|
||||
"组": "Group",
|
||||
"令牌已重置并已复制到剪贴板": "Token has been reset and copied to the clipboard",
|
||||
"邀请链接已复制到剪切板": "Invitation link has been copied to the clipboard",
|
||||
"系统令牌已复制到剪切板": "System token has been copied to the clipboard",
|
||||
"请输入你的账户名以确认删除!": "Please enter your account name to confirm deletion!",
|
||||
"账户已删除!": "Account has been deleted!",
|
||||
"微信账户绑定成功!": "WeChat account binding successful!",
|
||||
"请稍后几秒重试,Turnstile 正在检查用户环境!": "Please try again in a few seconds, Turnstile is checking the user environment!",
|
||||
"验证码发送成功,请检查邮箱!": "Verification code sent successfully, please check your email!",
|
||||
"邮箱账户绑定成功!": "Email account binding successful!",
|
||||
"个人信息": "Personal information",
|
||||
"编辑个人信息": "Edit personal information",
|
||||
"生成系统访问令牌": "Generate system access token",
|
||||
"复制邀请链接": "Copy invitation link",
|
||||
"删除个人帐户": "Delete personal account",
|
||||
"普通用户": "Regular user",
|
||||
"管理员": "Administrator",
|
||||
"超级管理员": "Super administrator",
|
||||
"显示名称": "Display name",
|
||||
"GitHub 账号": "GitHub account",
|
||||
"微信账号": "WeChat account",
|
||||
"修改个人信息只允许在电脑端进行。生成的令牌用于系统管理,而非用于请求 OpenAI 相关的服务,请知悉。": "Modifying personal information is only allowed on a computer. The generated token is for system management, not for requesting OpenAI related services. Please be aware.",
|
||||
"可用模型": "Available models",
|
||||
"账号绑定": "Account binding",
|
||||
"绑定微信": "Bind WeChat",
|
||||
"绑定 GitHub": "Bind GitHub",
|
||||
"绑定邮箱": "Bind Email",
|
||||
"绑定": "Bind",
|
||||
"绑定邮箱地址": "Bind email address",
|
||||
"输入邮箱地址": "Enter email address",
|
||||
"重新发送": "Resend",
|
||||
"获取验证码": "Get verification code",
|
||||
"确认绑定": "Confirm binding",
|
||||
"取消": "Cancel",
|
||||
"危险操作": "Dangerous operation",
|
||||
"您正在删除自己的帐户,将清空所有数据且不可恢复": "You are deleting your own account, all data will be cleared and cannot be recovered",
|
||||
"输入你的账户名": "Enter your account name",
|
||||
"以确认删除": "To confirm deletion",
|
||||
"确认删除": "Confirm deletion",
|
||||
"未使用": "Not used",
|
||||
"已禁用": "Disabled",
|
||||
"已使用": "Used",
|
||||
"未知状态": "Unknown status",
|
||||
"操作成功完成!": "Operation successfully completed!",
|
||||
"搜索兑换码的 ID 和名称 ...": "Search for the ID and name of the redemption code ...",
|
||||
"名称": "Name",
|
||||
"状态": "Status",
|
||||
"额度": "Quota",
|
||||
"创建时间": "Creation time",
|
||||
"兑换时间": "Redemption time",
|
||||
"操作": "Operation",
|
||||
"尚未兑换": "Not yet redeemed",
|
||||
"已复制到剪贴板!": "Copied to clipboard!",
|
||||
"无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。": "Unable to copy to clipboard, please copy manually. The redemption code has been filled in the search box.",
|
||||
"复制": "Copy",
|
||||
"删除": "Delete",
|
||||
"禁用": "Disable",
|
||||
"启用": "Enable",
|
||||
"编辑": "Edit",
|
||||
"添加新的兑换码": "Add new redemption code",
|
||||
"密码长度不得小于 8 位!": "Password length must not be less than 8 characters!",
|
||||
"两次输入的密码不一致": "The two passwords entered do not match",
|
||||
"注册成功!": "Registration successful!",
|
||||
"请填写注册邮箱!": "Please fill in the registration email!",
|
||||
"请在${verificationTimeout}秒后再试": "Please try again after ${verificationTimeout} seconds",
|
||||
"验证码发送成功,请检查你的邮箱!": "Verification code sent successfully, please check your email!",
|
||||
"已有账户?": "Already have an account?",
|
||||
"请输入用户名(最长 12 位)": "Please enter a username (up to 12 characters)",
|
||||
"请输入密码(最短 8 位,最长 20 位)": "Please enter a password (minimum 8 characters, maximum 20 characters)",
|
||||
"请再次输入密码": "Please enter the password again",
|
||||
"请输入邮箱地址": "Please enter an email address",
|
||||
"秒后可重发": "Can be resent after seconds",
|
||||
"请输入邮箱验证码": "Please enter the email verification code",
|
||||
"已过期": "Expired",
|
||||
"已启用": "Enabled",
|
||||
"已耗尽": "Exhausted",
|
||||
"无": "None",
|
||||
"令牌密钥": "API Key",
|
||||
"令牌状态": "Key status",
|
||||
"已用额度": "Used quota",
|
||||
"剩余额度": "Remaining quota",
|
||||
"过期时间": "Expiration time",
|
||||
"你确定要删除这个令牌吗?": "Are you sure you want to delete this key?",
|
||||
"无法复制到剪贴板,请手动复制,已将令牌密钥填入搜索框": "Unable to copy to clipboard, please copy manually. The key key has been filled in the search box.",
|
||||
"无限制": "Unlimited",
|
||||
"永不过期": "Never expires",
|
||||
"使用 API 访问令牌进行服务鉴权和计费。": "Use API Key for service authentication and billing.",
|
||||
"API 访问令牌关系到您的个人利益,请妥善留存,不要与其他人共享,也不要保存在客户端代码中。": "API Key is related to your personal interests. Please keep it properly. Do not share it with others or save it in client code.",
|
||||
"创建令牌": "Create Key",
|
||||
"什么都还没有,快去创建一个令牌开始使用吧!": "Nothing yet, go create a key to start using!",
|
||||
"你确定要删除该令牌吗": "Are you sure you want to delete this key",
|
||||
"导出令牌信息": "Export key information",
|
||||
"错误:未登录或登录已过期,请重新登录!": "Error: Not logged in or login has expired, please log in again!",
|
||||
"错误:请求次数过多,请稍后再试!": "Error: Too many requests, please try again later!",
|
||||
"错误:服务器内部错误,请联系管理员!": "Error: Server internal error, please contact the online customer service!",
|
||||
"本站仅作演示之用,无服务端!": "This site is for demonstration purposes only, no server!",
|
||||
"错误:": "Error:",
|
||||
"加载首页内容失败...": "Failed to load homepage content...",
|
||||
"系统状况": "System status",
|
||||
"系统信息": "System information",
|
||||
"系统信息总览": "System information overview",
|
||||
"名称:": "Name:",
|
||||
"版本:": "Version:",
|
||||
"源码:": "Source code:",
|
||||
"启动时间:": "Startup time:",
|
||||
"系统配置": "System configuration",
|
||||
"系统配置总览": "System configuration overview",
|
||||
"邮箱验证:": "Email verification:",
|
||||
"未启用": "Not enabled",
|
||||
"Turnstile 用户校验:": "Turnstile user verification:",
|
||||
"页面不存在": "Page does not exist",
|
||||
"请检查你的浏览器地址是否正确": "Please check if your browser address is correct",
|
||||
"个人设置": "Personal settings",
|
||||
"运营设置": "Operations settings",
|
||||
"系统设置": "System settings",
|
||||
"其他设置": "Other settings",
|
||||
"默认令牌": "Default key",
|
||||
"过期时间必须在当前时间之后!": "Expiration time must be after the current time!",
|
||||
"额度必须大于等于 0!": "Quota must be greater than or equal to 0!",
|
||||
"过期时间格式错误!": "Expiration time format error!",
|
||||
"创建令牌数量必须大于等于 1!": "The number of keys to create must be greater than or equal to 1!",
|
||||
"令牌修改成功": "API Key modification successful",
|
||||
"令牌创建成功": "API Key creation successful",
|
||||
"更新令牌信息": "Update key information",
|
||||
"创建新的令牌": "Create a new key",
|
||||
"请输入名称": "Please enter a name",
|
||||
"请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制": "Please enter the expiration time, the format is yyyy-MM-dd HH:mm:ss, -1 means unlimited",
|
||||
"无限额度": "Unlimited quota",
|
||||
"注意:启用无限额度后,已用额度将不再进行计算。": "Note: After enabling unlimited quota, the used quota will no longer be calculated.",
|
||||
"等于": "Equals",
|
||||
"请输入额度(单位:token)": "Please enter the quota (unit: token)",
|
||||
"创建令牌数量": "Create key quantity",
|
||||
"请输入令牌数量": "Please enter the number of keys",
|
||||
"注意:令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。": "Note: The quota of the key is only used to limit the maximum quota usage of the key itself, and the actual usage is subject to the remaining quota of the account.",
|
||||
"我的令牌": "My keys",
|
||||
"请输入额度兑换码!": "Please enter the redeem code!",
|
||||
"充值成功!": "Recharge successful!",
|
||||
"请求失败": "Request failed",
|
||||
"超级管理员未设置充值链接!": "The super administrator did not set a recharge link!",
|
||||
"充值额度": "Recharge quota",
|
||||
"兑换中...": "Redeeming...",
|
||||
"请点击充值以获取额度兑换码。": "Please click recharge to get the quota redemption code.",
|
||||
"用户信息更新成功!": "User information updated successfully!",
|
||||
"更新用户信息": "Update user information",
|
||||
"请输入新的用户名": "Please enter a new username",
|
||||
"请输入新的密码,最短 8 位": "Please enter a new password, at least 8 characters",
|
||||
"请输入新的显示名称": "Please enter a new display name",
|
||||
"分组": "Group",
|
||||
"请选择分组": "Please select a group",
|
||||
"请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit the group rate on the system settings page to add a new group:",
|
||||
"请输入新的剩余额度": "Please enter a new remaining quota",
|
||||
"已绑定的 GitHub 账户": "Bound GitHub account",
|
||||
"此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "This item is read-only, users need to bind through the relevant binding button on the personal settings page, cannot be directly modified",
|
||||
"已绑定的微信账户": "Bound WeChat account",
|
||||
"已绑定的邮箱账户": "Bound email account",
|
||||
"新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面": "New version available: ${data.version}, please refresh the page using the shortcut key Shift + F5",
|
||||
"无法正常连接至服务器!": "Unable to connect to the server normally!",
|
||||
"提示:": "Input:",
|
||||
"补全:": "Output:",
|
||||
"搜索令牌名称": "Search key name",
|
||||
"测试所有渠道": "Test all channels",
|
||||
"更新已启用渠道余额": "Update the balance of enabled channels"
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
|
||||
def list_file_paths(path):
|
||||
file_paths = []
|
||||
for root, dirs, files in os.walk(path):
|
||||
if "node_modules" in dirs:
|
||||
dirs.remove("node_modules")
|
||||
if "build" in dirs:
|
||||
dirs.remove("build")
|
||||
if "i18n" in dirs:
|
||||
dirs.remove("i18n")
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
if file_path.endswith("png") or file_path.endswith("ico") or file_path.endswith("db") or file_path.endswith("exe"):
|
||||
continue
|
||||
file_paths.append(file_path)
|
||||
|
||||
for dir in dirs:
|
||||
dir_path = os.path.join(root, dir)
|
||||
file_paths += list_file_paths(dir_path)
|
||||
|
||||
return file_paths
|
||||
|
||||
|
||||
def replace_keys_in_repository(repo_path, json_file_path):
|
||||
with open(json_file_path, 'r', encoding="utf-8") as json_file:
|
||||
key_value_pairs = json.load(json_file)
|
||||
|
||||
pairs = []
|
||||
for key, value in key_value_pairs.items():
|
||||
pairs.append((key, value))
|
||||
pairs.sort(key=lambda x: len(x[0]), reverse=True)
|
||||
|
||||
files = list_file_paths(repo_path)
|
||||
print('Total files: {}'.format(len(files)))
|
||||
for file_path in files:
|
||||
replace_keys_in_file(file_path, pairs)
|
||||
|
||||
|
||||
def replace_keys_in_file(file_path, pairs):
|
||||
try:
|
||||
with open(file_path, 'r', encoding="utf-8") as file:
|
||||
content = file.read()
|
||||
|
||||
for key, value in pairs:
|
||||
content = content.replace(key, value)
|
||||
|
||||
with open(file_path, 'w', encoding="utf-8") as file:
|
||||
file.write(content)
|
||||
except UnicodeDecodeError:
|
||||
print('UnicodeDecodeError: {}'.format(file_path))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Replace keys in repository.')
|
||||
parser.add_argument('--repository_path', help='Path to repository')
|
||||
parser.add_argument('--json_file_path', help='Path to JSON file')
|
||||
args = parser.parse_args()
|
||||
replace_keys_in_repository(args.repository_path, args.json_file_path)
|
13
main.go
13
main.go
@ -3,21 +3,24 @@ package main
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/client"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/i18n"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"github.com/songquanpeng/one-api/controller"
|
||||
"github.com/songquanpeng/one-api/middleware"
|
||||
"github.com/songquanpeng/one-api/model"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/openai"
|
||||
"github.com/songquanpeng/one-api/router"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
//go:embed web/build/*
|
||||
@ -91,12 +94,18 @@ func main() {
|
||||
openai.InitTokenEncoders()
|
||||
client.Init()
|
||||
|
||||
// Initialize i18n
|
||||
if err := i18n.Init(); err != nil {
|
||||
logger.FatalLog("failed to initialize i18n: " + err.Error())
|
||||
}
|
||||
|
||||
// Initialize HTTP server
|
||||
server := gin.New()
|
||||
server.Use(gin.Recovery())
|
||||
// This will cause SSE not to work!!!
|
||||
//server.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||
server.Use(middleware.RequestId())
|
||||
server.Use(middleware.Language())
|
||||
middleware.SetUpLogger(server)
|
||||
// Initialize session store
|
||||
store := cookie.NewStore([]byte(config.SessionSecret))
|
||||
|
25
middleware/language.go
Normal file
25
middleware/language.go
Normal file
@ -0,0 +1,25 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/i18n"
|
||||
)
|
||||
|
||||
func Language() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
lang := c.GetHeader("Accept-Language")
|
||||
if lang == "" {
|
||||
lang = "en"
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(lang), "zh") {
|
||||
lang = "zh-CN"
|
||||
} else {
|
||||
lang = "en"
|
||||
}
|
||||
c.Set(i18n.ContextKey, lang)
|
||||
c.Next()
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
)
|
||||
@ -71,7 +72,7 @@ func memoryRateLimiter(c *gin.Context, maxRequestNum int, duration int64, mark s
|
||||
}
|
||||
|
||||
func rateLimitFactory(maxRequestNum int, duration int64, mark string) func(c *gin.Context) {
|
||||
if maxRequestNum == 0 {
|
||||
if maxRequestNum == 0 || config.DebugEnabled {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
}
|
||||
|
@ -2,10 +2,13 @@ package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"gorm.io/gorm"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/utils"
|
||||
)
|
||||
|
||||
type Ability struct {
|
||||
@ -49,6 +52,7 @@ func GetRandomSatisfiedChannel(group string, model string, ignoreFirstPriority b
|
||||
|
||||
func (channel *Channel) AddAbilities() error {
|
||||
models_ := strings.Split(channel.Models, ",")
|
||||
models_ = utils.DeDuplication(models_)
|
||||
groups_ := strings.Split(channel.Group, ",")
|
||||
abilities := make([]Ability, 0, len(models_))
|
||||
for _, model := range models_ {
|
||||
|
@ -3,12 +3,14 @@ package model
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/helper"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"github.com/songquanpeng/one-api/common/message"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -238,16 +240,31 @@ func PreConsumeTokenQuota(tokenId int, quota int64) (err error) {
|
||||
if err != nil {
|
||||
logger.SysError("failed to fetch user email: " + err.Error())
|
||||
}
|
||||
prompt := "您的额度即将用尽"
|
||||
prompt := "额度提醒"
|
||||
var contentText string
|
||||
if noMoreQuota {
|
||||
prompt = "您的额度已用尽"
|
||||
contentText = "您的额度已用尽"
|
||||
} else {
|
||||
contentText = "您的额度即将用尽"
|
||||
}
|
||||
if email != "" {
|
||||
topUpLink := fmt.Sprintf("%s/topup", config.ServerAddress)
|
||||
err = message.SendEmail(prompt, email,
|
||||
fmt.Sprintf("%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。<br/>充值链接:<a href='%s'>%s</a>", prompt, userQuota, topUpLink, topUpLink))
|
||||
content := message.EmailTemplate(
|
||||
prompt,
|
||||
fmt.Sprintf(`
|
||||
<p>您好!</p>
|
||||
<p>%s,当前剩余额度为 <strong>%d</strong>。</p>
|
||||
<p>为了不影响您的使用,请及时充值。</p>
|
||||
<p style="text-align: center; margin: 30px 0;">
|
||||
<a href="%s" style="background-color: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">立即充值</a>
|
||||
</p>
|
||||
<p style="color: #666;">如果按钮无法点击,请复制以下链接到浏览器中打开:</p>
|
||||
<p style="background-color: #f8f8f8; padding: 10px; border-radius: 4px; word-break: break-all;">%s</p>
|
||||
`, contentText, userQuota, topUpLink, topUpLink),
|
||||
)
|
||||
err = message.SendEmail(prompt, email, content)
|
||||
if err != nil {
|
||||
logger.SysError("failed to send email" + err.Error())
|
||||
logger.SysError("failed to send email: " + err.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
@ -2,6 +2,7 @@ package monitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"github.com/songquanpeng/one-api/common/message"
|
||||
@ -30,17 +31,32 @@ func notifyRootUser(subject string, content string) {
|
||||
func DisableChannel(channelId int, channelName string, reason string) {
|
||||
model.UpdateChannelStatusById(channelId, model.ChannelStatusAutoDisabled)
|
||||
logger.SysLog(fmt.Sprintf("channel #%d has been disabled: %s", channelId, reason))
|
||||
subject := fmt.Sprintf("渠道「%s」(#%d)已被禁用", channelName, channelId)
|
||||
content := fmt.Sprintf("渠道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason)
|
||||
subject := fmt.Sprintf("渠道状态变更提醒")
|
||||
content := message.EmailTemplate(
|
||||
subject,
|
||||
fmt.Sprintf(`
|
||||
<p>您好!</p>
|
||||
<p>渠道「<strong>%s</strong>」(#%d)已被禁用。</p>
|
||||
<p>禁用原因:</p>
|
||||
<p style="background-color: #f8f8f8; padding: 10px; border-radius: 4px;">%s</p>
|
||||
`, channelName, channelId, reason),
|
||||
)
|
||||
notifyRootUser(subject, content)
|
||||
}
|
||||
|
||||
func MetricDisableChannel(channelId int, successRate float64) {
|
||||
model.UpdateChannelStatusById(channelId, model.ChannelStatusAutoDisabled)
|
||||
logger.SysLog(fmt.Sprintf("channel #%d has been disabled due to low success rate: %.2f", channelId, successRate*100))
|
||||
subject := fmt.Sprintf("渠道 #%d 已被禁用", channelId)
|
||||
content := fmt.Sprintf("该渠道(#%d)在最近 %d 次调用中成功率为 %.2f%%,低于阈值 %.2f%%,因此被系统自动禁用。",
|
||||
channelId, config.MetricQueueSize, successRate*100, config.MetricSuccessRateThreshold*100)
|
||||
subject := fmt.Sprintf("渠道状态变更提醒")
|
||||
content := message.EmailTemplate(
|
||||
subject,
|
||||
fmt.Sprintf(`
|
||||
<p>您好!</p>
|
||||
<p>渠道 #%d 已被系统自动禁用。</p>
|
||||
<p>禁用原因:</p>
|
||||
<p style="background-color: #f8f8f8; padding: 10px; border-radius: 4px;">该渠道在最近 %d 次调用中成功率为 <strong>%.2f%%</strong>,低于系统阈值 <strong>%.2f%%</strong>。</p>
|
||||
`, channelId, config.MetricQueueSize, successRate*100, config.MetricSuccessRateThreshold*100),
|
||||
)
|
||||
notifyRootUser(subject, content)
|
||||
}
|
||||
|
||||
@ -48,7 +64,14 @@ func MetricDisableChannel(channelId int, successRate float64) {
|
||||
func EnableChannel(channelId int, channelName string) {
|
||||
model.UpdateChannelStatusById(channelId, model.ChannelStatusEnabled)
|
||||
logger.SysLog(fmt.Sprintf("channel #%d has been enabled", channelId))
|
||||
subject := fmt.Sprintf("渠道「%s」(#%d)已被启用", channelName, channelId)
|
||||
content := fmt.Sprintf("渠道「%s」(#%d)已被启用", channelName, channelId)
|
||||
subject := fmt.Sprintf("渠道状态变更提醒")
|
||||
content := message.EmailTemplate(
|
||||
subject,
|
||||
fmt.Sprintf(`
|
||||
<p>您好!</p>
|
||||
<p>渠道「<strong>%s</strong>」(#%d)已被重新启用。</p>
|
||||
<p>您现在可以继续使用该渠道了。</p>
|
||||
`, channelName, channelId),
|
||||
)
|
||||
notifyRootUser(subject, content)
|
||||
}
|
||||
|
@ -35,6 +35,8 @@ func ShouldDisableChannel(err *model.Error, statusCode int) bool {
|
||||
strings.Contains(lowerMessage, "balance") ||
|
||||
strings.Contains(lowerMessage, "permission denied") ||
|
||||
strings.Contains(lowerMessage, "organization has been restricted") || // groq
|
||||
strings.Contains(lowerMessage, "api key not valid") || // gemini
|
||||
strings.Contains(lowerMessage, "api key expired") || // gemini
|
||||
strings.Contains(lowerMessage, "已欠费") {
|
||||
return true
|
||||
}
|
||||
|
@ -14,10 +14,14 @@ var ModelList = []string{
|
||||
"qwen2-72b-instruct", "qwen2-57b-a14b-instruct", "qwen2-7b-instruct", "qwen2-1.5b-instruct", "qwen2-0.5b-instruct",
|
||||
"qwen1.5-110b-chat", "qwen1.5-72b-chat", "qwen1.5-32b-chat", "qwen1.5-14b-chat", "qwen1.5-7b-chat", "qwen1.5-1.8b-chat", "qwen1.5-0.5b-chat",
|
||||
"qwen-72b-chat", "qwen-14b-chat", "qwen-7b-chat", "qwen-1.8b-chat", "qwen-1.8b-longcontext-chat",
|
||||
"qvq-72b-preview",
|
||||
"qwen2.5-vl-72b-instruct", "qwen2.5-vl-7b-instruct", "qwen2.5-vl-2b-instruct", "qwen2.5-vl-1b-instruct", "qwen2.5-vl-0.5b-instruct",
|
||||
"qwen2-vl-7b-instruct", "qwen2-vl-2b-instruct", "qwen-vl-v1", "qwen-vl-chat-v1",
|
||||
"qwen2-audio-instruct", "qwen-audio-chat",
|
||||
"qwen2.5-math-72b-instruct", "qwen2.5-math-7b-instruct", "qwen2.5-math-1.5b-instruct", "qwen2-math-72b-instruct", "qwen2-math-7b-instruct", "qwen2-math-1.5b-instruct",
|
||||
"qwen2.5-coder-32b-instruct", "qwen2.5-coder-14b-instruct", "qwen2.5-coder-7b-instruct", "qwen2.5-coder-3b-instruct", "qwen2.5-coder-1.5b-instruct", "qwen2.5-coder-0.5b-instruct",
|
||||
"text-embedding-v1", "text-embedding-v3", "text-embedding-v2", "text-embedding-async-v2", "text-embedding-async-v1",
|
||||
"ali-stable-diffusion-xl", "ali-stable-diffusion-v1.5", "wanx-v1",
|
||||
"qwen-mt-plus", "qwen-mt-turbo",
|
||||
"deepseek-r1", "deepseek-v3", "deepseek-r1-distill-qwen-1.5b", "deepseek-r1-distill-qwen-7b", "deepseek-r1-distill-qwen-14b", "deepseek-r1-distill-qwen-32b", "deepseek-r1-distill-llama-8b", "deepseek-r1-distill-llama-70b",
|
||||
}
|
||||
|
20
relay/adaptor/alibailian/constants.go
Normal file
20
relay/adaptor/alibailian/constants.go
Normal file
@ -0,0 +1,20 @@
|
||||
package alibailian
|
||||
|
||||
// https://help.aliyun.com/zh/model-studio/getting-started/models
|
||||
|
||||
var ModelList = []string{
|
||||
"qwen-turbo",
|
||||
"qwen-plus",
|
||||
"qwen-long",
|
||||
"qwen-max",
|
||||
"qwen-coder-plus",
|
||||
"qwen-coder-plus-latest",
|
||||
"qwen-coder-turbo",
|
||||
"qwen-coder-turbo-latest",
|
||||
"qwen-mt-plus",
|
||||
"qwen-mt-turbo",
|
||||
"qwq-32b-preview",
|
||||
|
||||
"deepseek-r1",
|
||||
"deepseek-v3",
|
||||
}
|
19
relay/adaptor/alibailian/main.go
Normal file
19
relay/adaptor/alibailian/main.go
Normal file
@ -0,0 +1,19 @@
|
||||
package alibailian
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/songquanpeng/one-api/relay/meta"
|
||||
"github.com/songquanpeng/one-api/relay/relaymode"
|
||||
)
|
||||
|
||||
func GetRequestURL(meta *meta.Meta) (string, error) {
|
||||
switch meta.Mode {
|
||||
case relaymode.ChatCompletions:
|
||||
return fmt.Sprintf("%s/compatible-mode/v1/chat/completions", meta.BaseURL), nil
|
||||
case relaymode.Embeddings:
|
||||
return fmt.Sprintf("%s/compatible-mode/v1/embeddings", meta.BaseURL), nil
|
||||
default:
|
||||
}
|
||||
return "", fmt.Errorf("unsupported relay mode %d for ali bailian", meta.Mode)
|
||||
}
|
@ -4,6 +4,7 @@ var ModelList = []string{
|
||||
"claude-instant-1.2", "claude-2.0", "claude-2.1",
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-3-5-haiku-20241022",
|
||||
"claude-3-5-haiku-latest",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-5-sonnet-20240620",
|
||||
|
30
relay/adaptor/baiduv2/constants.go
Normal file
30
relay/adaptor/baiduv2/constants.go
Normal file
@ -0,0 +1,30 @@
|
||||
package baiduv2
|
||||
|
||||
// https://console.bce.baidu.com/support/?_=1692863460488×tamp=1739074632076#/api?product=QIANFAN&project=%E5%8D%83%E5%B8%86ModelBuilder&parent=%E5%AF%B9%E8%AF%9DChat%20V2&api=v2%2Fchat%2Fcompletions&method=post
|
||||
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu#%E6%94%AF%E6%8C%81%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8
|
||||
|
||||
var ModelList = []string{
|
||||
"ernie-4.0-8k-latest",
|
||||
"ernie-4.0-8k-preview",
|
||||
"ernie-4.0-8k",
|
||||
"ernie-4.0-turbo-8k-latest",
|
||||
"ernie-4.0-turbo-8k-preview",
|
||||
"ernie-4.0-turbo-8k",
|
||||
"ernie-4.0-turbo-128k",
|
||||
"ernie-3.5-8k-preview",
|
||||
"ernie-3.5-8k",
|
||||
"ernie-3.5-128k",
|
||||
"ernie-speed-8k",
|
||||
"ernie-speed-128k",
|
||||
"ernie-speed-pro-128k",
|
||||
"ernie-lite-8k",
|
||||
"ernie-lite-pro-128k",
|
||||
"ernie-tiny-8k",
|
||||
"ernie-char-8k",
|
||||
"ernie-char-fiction-8k",
|
||||
"ernie-novel-8k",
|
||||
"deepseek-v3",
|
||||
"deepseek-r1",
|
||||
"deepseek-r1-distill-qwen-32b",
|
||||
"deepseek-r1-distill-qwen-14b",
|
||||
}
|
17
relay/adaptor/baiduv2/main.go
Normal file
17
relay/adaptor/baiduv2/main.go
Normal file
@ -0,0 +1,17 @@
|
||||
package baiduv2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/songquanpeng/one-api/relay/meta"
|
||||
"github.com/songquanpeng/one-api/relay/relaymode"
|
||||
)
|
||||
|
||||
func GetRequestURL(meta *meta.Meta) (string, error) {
|
||||
switch meta.Mode {
|
||||
case relaymode.ChatCompletions:
|
||||
return fmt.Sprintf("%s/v2/chat/completions", meta.BaseURL), nil
|
||||
default:
|
||||
}
|
||||
return "", fmt.Errorf("unsupported relay mode %d for baidu v2", meta.Mode)
|
||||
}
|
@ -2,5 +2,5 @@ package deepseek
|
||||
|
||||
var ModelList = []string{
|
||||
"deepseek-chat",
|
||||
"deepseek-coder",
|
||||
"deepseek-reasoner",
|
||||
}
|
||||
|
@ -5,8 +5,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/helper"
|
||||
channelhelper "github.com/songquanpeng/one-api/relay/adaptor"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/openai"
|
||||
@ -19,15 +21,12 @@ type Adaptor struct {
|
||||
}
|
||||
|
||||
func (a *Adaptor) Init(meta *meta.Meta) {
|
||||
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
|
||||
var defaultVersion string
|
||||
switch meta.ActualModelName {
|
||||
case "gemini-2.0-flash-exp",
|
||||
"gemini-2.0-flash-thinking-exp",
|
||||
"gemini-2.0-flash-thinking-exp-01-21":
|
||||
defaultVersion := config.GeminiVersion
|
||||
if strings.Contains(meta.ActualModelName, "gemini-2.0") ||
|
||||
strings.Contains(meta.ActualModelName, "gemini-1.5") {
|
||||
defaultVersion = "v1beta"
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,35 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/geminiv2"
|
||||
)
|
||||
|
||||
// https://ai.google.dev/models/gemini
|
||||
|
||||
var ModelList = []string{
|
||||
"gemini-pro", "gemini-1.0-pro",
|
||||
"gemini-1.5-flash", "gemini-1.5-pro",
|
||||
"text-embedding-004", "aqa",
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-2.0-flash-thinking-exp", "gemini-2.0-flash-thinking-exp-01-21",
|
||||
var ModelList = geminiv2.ModelList
|
||||
|
||||
// ModelsSupportSystemInstruction is the list of models that support system instruction.
|
||||
//
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/system-instructions
|
||||
var ModelsSupportSystemInstruction = []string{
|
||||
// "gemini-1.0-pro-002",
|
||||
// "gemini-1.5-flash", "gemini-1.5-flash-001", "gemini-1.5-flash-002",
|
||||
// "gemini-1.5-flash-8b",
|
||||
// "gemini-1.5-pro", "gemini-1.5-pro-001", "gemini-1.5-pro-002",
|
||||
// "gemini-1.5-pro-experimental",
|
||||
"gemini-2.0-flash", "gemini-2.0-flash-exp",
|
||||
"gemini-2.0-flash-thinking-exp-01-21",
|
||||
}
|
||||
|
||||
// IsModelSupportSystemInstruction check if the model support system instruction.
|
||||
//
|
||||
// Because the main version of Go is 1.20, slice.Contains cannot be used
|
||||
func IsModelSupportSystemInstruction(model string) bool {
|
||||
for _, m := range ModelsSupportSystemInstruction {
|
||||
if m == model {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -132,9 +132,16 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *ChatRequest {
|
||||
}
|
||||
// Converting system prompt to prompt from user for the same reason
|
||||
if content.Role == "system" {
|
||||
content.Role = "user"
|
||||
shouldAddDummyModelMessage = true
|
||||
if IsModelSupportSystemInstruction(textRequest.Model) {
|
||||
geminiRequest.SystemInstruction = &content
|
||||
geminiRequest.SystemInstruction.Role = ""
|
||||
continue
|
||||
} else {
|
||||
content.Role = "user"
|
||||
}
|
||||
}
|
||||
|
||||
geminiRequest.Contents = append(geminiRequest.Contents, content)
|
||||
|
||||
// If a system message is the last message, we need to add a dummy model message to make gemini happy
|
||||
|
@ -1,10 +1,11 @@
|
||||
package gemini
|
||||
|
||||
type ChatRequest struct {
|
||||
Contents []ChatContent `json:"contents"`
|
||||
SafetySettings []ChatSafetySettings `json:"safety_settings,omitempty"`
|
||||
GenerationConfig ChatGenerationConfig `json:"generation_config,omitempty"`
|
||||
Tools []ChatTools `json:"tools,omitempty"`
|
||||
Contents []ChatContent `json:"contents"`
|
||||
SafetySettings []ChatSafetySettings `json:"safety_settings,omitempty"`
|
||||
GenerationConfig ChatGenerationConfig `json:"generation_config,omitempty"`
|
||||
Tools []ChatTools `json:"tools,omitempty"`
|
||||
SystemInstruction *ChatContent `json:"system_instruction,omitempty"`
|
||||
}
|
||||
|
||||
type EmbeddingRequest struct {
|
||||
|
15
relay/adaptor/geminiv2/constants.go
Normal file
15
relay/adaptor/geminiv2/constants.go
Normal file
@ -0,0 +1,15 @@
|
||||
package geminiv2
|
||||
|
||||
// https://ai.google.dev/models/gemini
|
||||
|
||||
var ModelList = []string{
|
||||
"gemini-pro", "gemini-1.0-pro",
|
||||
// "gemma-2-2b-it", "gemma-2-9b-it", "gemma-2-27b-it",
|
||||
"gemini-1.5-flash", "gemini-1.5-flash-8b",
|
||||
"gemini-1.5-pro", "gemini-1.5-pro-experimental",
|
||||
"text-embedding-004", "aqa",
|
||||
"gemini-2.0-flash", "gemini-2.0-flash-exp",
|
||||
"gemini-2.0-flash-lite-preview-02-05",
|
||||
"gemini-2.0-flash-thinking-exp-01-21",
|
||||
"gemini-2.0-pro-exp-02-05",
|
||||
}
|
14
relay/adaptor/geminiv2/main.go
Normal file
14
relay/adaptor/geminiv2/main.go
Normal file
@ -0,0 +1,14 @@
|
||||
package geminiv2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/songquanpeng/one-api/relay/meta"
|
||||
)
|
||||
|
||||
func GetRequestURL(meta *meta.Meta) (string, error) {
|
||||
baseURL := strings.TrimSuffix(meta.BaseURL, "/")
|
||||
requestPath := strings.TrimPrefix(meta.RequestURLPath, "/v1")
|
||||
return fmt.Sprintf("%s%s", baseURL, requestPath), nil
|
||||
}
|
@ -3,7 +3,6 @@ package groq
|
||||
// https://console.groq.com/docs/models
|
||||
|
||||
var ModelList = []string{
|
||||
"gemma-7b-it",
|
||||
"gemma2-9b-it",
|
||||
"llama-3.1-70b-versatile",
|
||||
"llama-3.1-8b-instant",
|
||||
@ -11,7 +10,6 @@ var ModelList = []string{
|
||||
"llama-3.2-11b-vision-preview",
|
||||
"llama-3.2-1b-preview",
|
||||
"llama-3.2-3b-preview",
|
||||
"llama-3.2-11b-vision-preview",
|
||||
"llama-3.2-90b-text-preview",
|
||||
"llama-3.2-90b-vision-preview",
|
||||
"llama-guard-3-8b",
|
||||
@ -24,4 +22,6 @@ var ModelList = []string{
|
||||
"distil-whisper-large-v3-en",
|
||||
"whisper-large-v3",
|
||||
"whisper-large-v3-turbo",
|
||||
"deepseek-r1-distill-llama-70b-specdec",
|
||||
"deepseek-r1-distill-llama-70b",
|
||||
}
|
||||
|
@ -8,4 +8,6 @@ var ModelList = []string{
|
||||
"abab6-chat",
|
||||
"abab5.5-chat",
|
||||
"abab5.5s-chat",
|
||||
"MiniMax-VL-01",
|
||||
"MiniMax-Text-01",
|
||||
}
|
||||
|
@ -8,8 +8,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/relay/adaptor"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/alibailian"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/baiduv2"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/doubao"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/geminiv2"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/minimax"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/novita"
|
||||
"github.com/songquanpeng/one-api/relay/channeltype"
|
||||
@ -52,6 +56,12 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
|
||||
return doubao.GetRequestURL(meta)
|
||||
case channeltype.Novita:
|
||||
return novita.GetRequestURL(meta)
|
||||
case channeltype.BaiduV2:
|
||||
return baiduv2.GetRequestURL(meta)
|
||||
case channeltype.AliBailian:
|
||||
return alibailian.GetRequestURL(meta)
|
||||
case channeltype.GeminiOpenAICompatible:
|
||||
return geminiv2.GetRequestURL(meta)
|
||||
default:
|
||||
return GetFullRequestURL(meta.BaseURL, meta.RequestURLPath, meta.ChannelType), nil
|
||||
}
|
||||
|
@ -2,19 +2,24 @@ package openai
|
||||
|
||||
import (
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/ai360"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/alibailian"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/baichuan"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/baiduv2"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/deepseek"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/doubao"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/geminiv2"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/groq"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/lingyiwanwu"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/minimax"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/mistral"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/moonshot"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/novita"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/openrouter"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/siliconflow"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/stepfun"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/togetherai"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/xai"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/xunfeiv2"
|
||||
"github.com/songquanpeng/one-api/relay/channeltype"
|
||||
)
|
||||
|
||||
@ -34,6 +39,8 @@ var CompatibleChannels = []int{
|
||||
channeltype.Novita,
|
||||
channeltype.SiliconFlow,
|
||||
channeltype.XAI,
|
||||
channeltype.BaiduV2,
|
||||
channeltype.XunfeiV2,
|
||||
}
|
||||
|
||||
func GetCompatibleChannelMeta(channelType int) (string, []string) {
|
||||
@ -68,6 +75,16 @@ func GetCompatibleChannelMeta(channelType int) (string, []string) {
|
||||
return "siliconflow", siliconflow.ModelList
|
||||
case channeltype.XAI:
|
||||
return "xai", xai.ModelList
|
||||
case channeltype.BaiduV2:
|
||||
return "baiduv2", baiduv2.ModelList
|
||||
case channeltype.XunfeiV2:
|
||||
return "xunfeiv2", xunfeiv2.ModelList
|
||||
case channeltype.OpenRouter:
|
||||
return "openrouter", openrouter.ModelList
|
||||
case channeltype.AliBailian:
|
||||
return "alibailian", alibailian.ModelList
|
||||
case channeltype.GeminiOpenAICompatible:
|
||||
return "geminiv2", geminiv2.ModelList
|
||||
default:
|
||||
return "openai", ModelList
|
||||
}
|
||||
|
@ -17,6 +17,9 @@ func ResponseText2Usage(responseText string, modelName string, promptTokens int)
|
||||
}
|
||||
|
||||
func GetFullRequestURL(baseURL string, requestURL string, channelType int) string {
|
||||
if channelType == channeltype.OpenAICompatible {
|
||||
return fmt.Sprintf("%s%s", strings.TrimSuffix(baseURL, "/"), strings.TrimPrefix(requestURL, "/v1"))
|
||||
}
|
||||
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
|
||||
|
||||
if strings.HasPrefix(baseURL, "https://gateway.ai.cloudflare.com") {
|
||||
|
@ -3,14 +3,16 @@ package openai
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/pkoukk/tiktoken-go"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/image"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
billingratio "github.com/songquanpeng/one-api/relay/billing/ratio"
|
||||
"github.com/songquanpeng/one-api/relay/model"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// tokenEncoderMap won't grow after initialization
|
||||
@ -21,7 +23,8 @@ func InitTokenEncoders() {
|
||||
logger.SysLog("initializing token encoders")
|
||||
gpt35TokenEncoder, err := tiktoken.EncodingForModel("gpt-3.5-turbo")
|
||||
if err != nil {
|
||||
logger.FatalLog(fmt.Sprintf("failed to get gpt-3.5-turbo token encoder: %s", err.Error()))
|
||||
logger.FatalLog(fmt.Sprintf("failed to get gpt-3.5-turbo token encoder: %s, "+
|
||||
"if you are using in offline environment, please set TIKTOKEN_CACHE_DIR to use exsited files, check this link for more information: https://stackoverflow.com/questions/76106366/how-to-use-tiktoken-in-offline-mode-computer ", err.Error()))
|
||||
}
|
||||
defaultTokenEncoder = gpt35TokenEncoder
|
||||
gpt4oTokenEncoder, err := tiktoken.EncodingForModel("gpt-4o")
|
||||
|
235
relay/adaptor/openrouter/constants.go
Normal file
235
relay/adaptor/openrouter/constants.go
Normal file
@ -0,0 +1,235 @@
|
||||
package openrouter
|
||||
|
||||
var ModelList = []string{
|
||||
"01-ai/yi-large",
|
||||
"aetherwiing/mn-starcannon-12b",
|
||||
"ai21/jamba-1-5-large",
|
||||
"ai21/jamba-1-5-mini",
|
||||
"ai21/jamba-instruct",
|
||||
"aion-labs/aion-1.0",
|
||||
"aion-labs/aion-1.0-mini",
|
||||
"aion-labs/aion-rp-llama-3.1-8b",
|
||||
"allenai/llama-3.1-tulu-3-405b",
|
||||
"alpindale/goliath-120b",
|
||||
"alpindale/magnum-72b",
|
||||
"amazon/nova-lite-v1",
|
||||
"amazon/nova-micro-v1",
|
||||
"amazon/nova-pro-v1",
|
||||
"anthracite-org/magnum-v2-72b",
|
||||
"anthracite-org/magnum-v4-72b",
|
||||
"anthropic/claude-2",
|
||||
"anthropic/claude-2.0",
|
||||
"anthropic/claude-2.0:beta",
|
||||
"anthropic/claude-2.1",
|
||||
"anthropic/claude-2.1:beta",
|
||||
"anthropic/claude-2:beta",
|
||||
"anthropic/claude-3-haiku",
|
||||
"anthropic/claude-3-haiku:beta",
|
||||
"anthropic/claude-3-opus",
|
||||
"anthropic/claude-3-opus:beta",
|
||||
"anthropic/claude-3-sonnet",
|
||||
"anthropic/claude-3-sonnet:beta",
|
||||
"anthropic/claude-3.5-haiku",
|
||||
"anthropic/claude-3.5-haiku-20241022",
|
||||
"anthropic/claude-3.5-haiku-20241022:beta",
|
||||
"anthropic/claude-3.5-haiku:beta",
|
||||
"anthropic/claude-3.5-sonnet",
|
||||
"anthropic/claude-3.5-sonnet-20240620",
|
||||
"anthropic/claude-3.5-sonnet-20240620:beta",
|
||||
"anthropic/claude-3.5-sonnet:beta",
|
||||
"cognitivecomputations/dolphin-mixtral-8x22b",
|
||||
"cognitivecomputations/dolphin-mixtral-8x7b",
|
||||
"cohere/command",
|
||||
"cohere/command-r",
|
||||
"cohere/command-r-03-2024",
|
||||
"cohere/command-r-08-2024",
|
||||
"cohere/command-r-plus",
|
||||
"cohere/command-r-plus-04-2024",
|
||||
"cohere/command-r-plus-08-2024",
|
||||
"cohere/command-r7b-12-2024",
|
||||
"databricks/dbrx-instruct",
|
||||
"deepseek/deepseek-chat",
|
||||
"deepseek/deepseek-chat-v2.5",
|
||||
"deepseek/deepseek-chat:free",
|
||||
"deepseek/deepseek-r1",
|
||||
"deepseek/deepseek-r1-distill-llama-70b",
|
||||
"deepseek/deepseek-r1-distill-llama-70b:free",
|
||||
"deepseek/deepseek-r1-distill-llama-8b",
|
||||
"deepseek/deepseek-r1-distill-qwen-1.5b",
|
||||
"deepseek/deepseek-r1-distill-qwen-14b",
|
||||
"deepseek/deepseek-r1-distill-qwen-32b",
|
||||
"deepseek/deepseek-r1:free",
|
||||
"eva-unit-01/eva-llama-3.33-70b",
|
||||
"eva-unit-01/eva-qwen-2.5-32b",
|
||||
"eva-unit-01/eva-qwen-2.5-72b",
|
||||
"google/gemini-2.0-flash-001",
|
||||
"google/gemini-2.0-flash-exp:free",
|
||||
"google/gemini-2.0-flash-lite-preview-02-05:free",
|
||||
"google/gemini-2.0-flash-thinking-exp-1219:free",
|
||||
"google/gemini-2.0-flash-thinking-exp:free",
|
||||
"google/gemini-2.0-pro-exp-02-05:free",
|
||||
"google/gemini-exp-1206:free",
|
||||
"google/gemini-flash-1.5",
|
||||
"google/gemini-flash-1.5-8b",
|
||||
"google/gemini-flash-1.5-8b-exp",
|
||||
"google/gemini-pro",
|
||||
"google/gemini-pro-1.5",
|
||||
"google/gemini-pro-vision",
|
||||
"google/gemma-2-27b-it",
|
||||
"google/gemma-2-9b-it",
|
||||
"google/gemma-2-9b-it:free",
|
||||
"google/gemma-7b-it",
|
||||
"google/learnlm-1.5-pro-experimental:free",
|
||||
"google/palm-2-chat-bison",
|
||||
"google/palm-2-chat-bison-32k",
|
||||
"google/palm-2-codechat-bison",
|
||||
"google/palm-2-codechat-bison-32k",
|
||||
"gryphe/mythomax-l2-13b",
|
||||
"gryphe/mythomax-l2-13b:free",
|
||||
"huggingfaceh4/zephyr-7b-beta:free",
|
||||
"infermatic/mn-inferor-12b",
|
||||
"inflection/inflection-3-pi",
|
||||
"inflection/inflection-3-productivity",
|
||||
"jondurbin/airoboros-l2-70b",
|
||||
"liquid/lfm-3b",
|
||||
"liquid/lfm-40b",
|
||||
"liquid/lfm-7b",
|
||||
"mancer/weaver",
|
||||
"meta-llama/llama-2-13b-chat",
|
||||
"meta-llama/llama-2-70b-chat",
|
||||
"meta-llama/llama-3-70b-instruct",
|
||||
"meta-llama/llama-3-8b-instruct",
|
||||
"meta-llama/llama-3-8b-instruct:free",
|
||||
"meta-llama/llama-3.1-405b",
|
||||
"meta-llama/llama-3.1-405b-instruct",
|
||||
"meta-llama/llama-3.1-70b-instruct",
|
||||
"meta-llama/llama-3.1-8b-instruct",
|
||||
"meta-llama/llama-3.2-11b-vision-instruct",
|
||||
"meta-llama/llama-3.2-11b-vision-instruct:free",
|
||||
"meta-llama/llama-3.2-1b-instruct",
|
||||
"meta-llama/llama-3.2-3b-instruct",
|
||||
"meta-llama/llama-3.2-90b-vision-instruct",
|
||||
"meta-llama/llama-3.3-70b-instruct",
|
||||
"meta-llama/llama-3.3-70b-instruct:free",
|
||||
"meta-llama/llama-guard-2-8b",
|
||||
"microsoft/phi-3-medium-128k-instruct",
|
||||
"microsoft/phi-3-medium-128k-instruct:free",
|
||||
"microsoft/phi-3-mini-128k-instruct",
|
||||
"microsoft/phi-3-mini-128k-instruct:free",
|
||||
"microsoft/phi-3.5-mini-128k-instruct",
|
||||
"microsoft/phi-4",
|
||||
"microsoft/wizardlm-2-7b",
|
||||
"microsoft/wizardlm-2-8x22b",
|
||||
"minimax/minimax-01",
|
||||
"mistralai/codestral-2501",
|
||||
"mistralai/codestral-mamba",
|
||||
"mistralai/ministral-3b",
|
||||
"mistralai/ministral-8b",
|
||||
"mistralai/mistral-7b-instruct",
|
||||
"mistralai/mistral-7b-instruct-v0.1",
|
||||
"mistralai/mistral-7b-instruct-v0.3",
|
||||
"mistralai/mistral-7b-instruct:free",
|
||||
"mistralai/mistral-large",
|
||||
"mistralai/mistral-large-2407",
|
||||
"mistralai/mistral-large-2411",
|
||||
"mistralai/mistral-medium",
|
||||
"mistralai/mistral-nemo",
|
||||
"mistralai/mistral-nemo:free",
|
||||
"mistralai/mistral-small",
|
||||
"mistralai/mistral-small-24b-instruct-2501",
|
||||
"mistralai/mistral-small-24b-instruct-2501:free",
|
||||
"mistralai/mistral-tiny",
|
||||
"mistralai/mixtral-8x22b-instruct",
|
||||
"mistralai/mixtral-8x7b",
|
||||
"mistralai/mixtral-8x7b-instruct",
|
||||
"mistralai/pixtral-12b",
|
||||
"mistralai/pixtral-large-2411",
|
||||
"neversleep/llama-3-lumimaid-70b",
|
||||
"neversleep/llama-3-lumimaid-8b",
|
||||
"neversleep/llama-3-lumimaid-8b:extended",
|
||||
"neversleep/llama-3.1-lumimaid-70b",
|
||||
"neversleep/llama-3.1-lumimaid-8b",
|
||||
"neversleep/noromaid-20b",
|
||||
"nothingiisreal/mn-celeste-12b",
|
||||
"nousresearch/hermes-2-pro-llama-3-8b",
|
||||
"nousresearch/hermes-3-llama-3.1-405b",
|
||||
"nousresearch/hermes-3-llama-3.1-70b",
|
||||
"nousresearch/nous-hermes-2-mixtral-8x7b-dpo",
|
||||
"nousresearch/nous-hermes-llama2-13b",
|
||||
"nvidia/llama-3.1-nemotron-70b-instruct",
|
||||
"nvidia/llama-3.1-nemotron-70b-instruct:free",
|
||||
"openai/chatgpt-4o-latest",
|
||||
"openai/gpt-3.5-turbo",
|
||||
"openai/gpt-3.5-turbo-0125",
|
||||
"openai/gpt-3.5-turbo-0613",
|
||||
"openai/gpt-3.5-turbo-1106",
|
||||
"openai/gpt-3.5-turbo-16k",
|
||||
"openai/gpt-3.5-turbo-instruct",
|
||||
"openai/gpt-4",
|
||||
"openai/gpt-4-0314",
|
||||
"openai/gpt-4-1106-preview",
|
||||
"openai/gpt-4-32k",
|
||||
"openai/gpt-4-32k-0314",
|
||||
"openai/gpt-4-turbo",
|
||||
"openai/gpt-4-turbo-preview",
|
||||
"openai/gpt-4o",
|
||||
"openai/gpt-4o-2024-05-13",
|
||||
"openai/gpt-4o-2024-08-06",
|
||||
"openai/gpt-4o-2024-11-20",
|
||||
"openai/gpt-4o-mini",
|
||||
"openai/gpt-4o-mini-2024-07-18",
|
||||
"openai/gpt-4o:extended",
|
||||
"openai/o1",
|
||||
"openai/o1-mini",
|
||||
"openai/o1-mini-2024-09-12",
|
||||
"openai/o1-preview",
|
||||
"openai/o1-preview-2024-09-12",
|
||||
"openai/o3-mini",
|
||||
"openai/o3-mini-high",
|
||||
"openchat/openchat-7b",
|
||||
"openchat/openchat-7b:free",
|
||||
"openrouter/auto",
|
||||
"perplexity/llama-3.1-sonar-huge-128k-online",
|
||||
"perplexity/llama-3.1-sonar-large-128k-chat",
|
||||
"perplexity/llama-3.1-sonar-large-128k-online",
|
||||
"perplexity/llama-3.1-sonar-small-128k-chat",
|
||||
"perplexity/llama-3.1-sonar-small-128k-online",
|
||||
"perplexity/sonar",
|
||||
"perplexity/sonar-reasoning",
|
||||
"pygmalionai/mythalion-13b",
|
||||
"qwen/qvq-72b-preview",
|
||||
"qwen/qwen-2-72b-instruct",
|
||||
"qwen/qwen-2-7b-instruct",
|
||||
"qwen/qwen-2-7b-instruct:free",
|
||||
"qwen/qwen-2-vl-72b-instruct",
|
||||
"qwen/qwen-2-vl-7b-instruct",
|
||||
"qwen/qwen-2.5-72b-instruct",
|
||||
"qwen/qwen-2.5-7b-instruct",
|
||||
"qwen/qwen-2.5-coder-32b-instruct",
|
||||
"qwen/qwen-max",
|
||||
"qwen/qwen-plus",
|
||||
"qwen/qwen-turbo",
|
||||
"qwen/qwen-vl-plus:free",
|
||||
"qwen/qwen2.5-vl-72b-instruct:free",
|
||||
"qwen/qwq-32b-preview",
|
||||
"raifle/sorcererlm-8x22b",
|
||||
"sao10k/fimbulvetr-11b-v2",
|
||||
"sao10k/l3-euryale-70b",
|
||||
"sao10k/l3-lunaris-8b",
|
||||
"sao10k/l3.1-70b-hanami-x1",
|
||||
"sao10k/l3.1-euryale-70b",
|
||||
"sao10k/l3.3-euryale-70b",
|
||||
"sophosympatheia/midnight-rose-70b",
|
||||
"sophosympatheia/rogue-rose-103b-v0.2:free",
|
||||
"teknium/openhermes-2.5-mistral-7b",
|
||||
"thedrummer/rocinante-12b",
|
||||
"thedrummer/unslopnemo-12b",
|
||||
"undi95/remm-slerp-l2-13b",
|
||||
"undi95/toppy-m-7b",
|
||||
"undi95/toppy-m-7b:free",
|
||||
"x-ai/grok-2-1212",
|
||||
"x-ai/grok-2-vision-1212",
|
||||
"x-ai/grok-beta",
|
||||
"x-ai/grok-vision-beta",
|
||||
"xwin-lm/xwin-lm-70b",
|
||||
}
|
@ -16,10 +16,12 @@ import (
|
||||
|
||||
var ModelList = []string{
|
||||
"gemini-pro", "gemini-pro-vision",
|
||||
"gemini-1.5-pro-001", "gemini-1.5-flash-001",
|
||||
"gemini-1.5-pro-002", "gemini-1.5-flash-002",
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-2.0-flash-thinking-exp", "gemini-2.0-flash-thinking-exp-01-21",
|
||||
"gemini-exp-1206",
|
||||
"gemini-1.5-pro-001", "gemini-1.5-pro-002",
|
||||
"gemini-1.5-flash-001", "gemini-1.5-flash-002",
|
||||
"gemini-2.0-flash-exp", "gemini-2.0-flash-001",
|
||||
"gemini-2.0-flash-lite-preview-02-05",
|
||||
"gemini-2.0-flash-thinking-exp-01-21",
|
||||
}
|
||||
|
||||
type Adaptor struct {
|
||||
|
@ -1,5 +1,14 @@
|
||||
package xai
|
||||
|
||||
//https://console.x.ai/
|
||||
|
||||
var ModelList = []string{
|
||||
"grok-2",
|
||||
"grok-vision-beta",
|
||||
"grok-2-vision-1212",
|
||||
"grok-2-vision",
|
||||
"grok-2-vision-latest",
|
||||
"grok-2-1212",
|
||||
"grok-2-latest",
|
||||
"grok-beta",
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
package xunfei
|
||||
|
||||
var ModelList = []string{
|
||||
"SparkDesk",
|
||||
"SparkDesk-v1.1",
|
||||
"SparkDesk-v2.1",
|
||||
"SparkDesk-v3.1",
|
||||
"SparkDesk-v3.1-128K",
|
||||
"SparkDesk-v3.5",
|
||||
"SparkDesk-v3.5-32K",
|
||||
"SparkDesk-v4.0",
|
||||
"Spark-Lite",
|
||||
"Spark-Pro",
|
||||
"Spark-Pro-128K",
|
||||
"Spark-Max",
|
||||
"Spark-Max-32K",
|
||||
"Spark-4.0-Ultra",
|
||||
}
|
||||
|
97
relay/adaptor/xunfei/domain.go
Normal file
97
relay/adaptor/xunfei/domain.go
Normal file
@ -0,0 +1,97 @@
|
||||
package xunfei
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
|
||||
|
||||
//Spark4.0 Ultra 请求地址,对应的domain参数为4.0Ultra:
|
||||
//
|
||||
//wss://spark-api.xf-yun.com/v4.0/chat
|
||||
//Spark Max-32K请求地址,对应的domain参数为max-32k
|
||||
//
|
||||
//wss://spark-api.xf-yun.com/chat/max-32k
|
||||
//Spark Max请求地址,对应的domain参数为generalv3.5
|
||||
//
|
||||
//wss://spark-api.xf-yun.com/v3.5/chat
|
||||
//Spark Pro-128K请求地址,对应的domain参数为pro-128k:
|
||||
//
|
||||
// wss://spark-api.xf-yun.com/chat/pro-128k
|
||||
//Spark Pro请求地址,对应的domain参数为generalv3:
|
||||
//
|
||||
//wss://spark-api.xf-yun.com/v3.1/chat
|
||||
//Spark Lite请求地址,对应的domain参数为lite:
|
||||
//
|
||||
//wss://spark-api.xf-yun.com/v1.1/chat
|
||||
|
||||
// Lite、Pro、Pro-128K、Max、Max-32K和4.0 Ultra
|
||||
|
||||
func parseAPIVersionByModelName(modelName string) string {
|
||||
apiVersion := modelName2APIVersion(modelName)
|
||||
if apiVersion != "" {
|
||||
return apiVersion
|
||||
}
|
||||
|
||||
index := strings.IndexAny(modelName, "-")
|
||||
if index != -1 {
|
||||
return modelName[index+1:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func modelName2APIVersion(modelName string) string {
|
||||
switch modelName {
|
||||
case "Spark-Lite":
|
||||
return "v1.1"
|
||||
case "Spark-Pro":
|
||||
return "v3.1"
|
||||
case "Spark-Pro-128K":
|
||||
return "v3.1-128K"
|
||||
case "Spark-Max":
|
||||
return "v3.5"
|
||||
case "Spark-Max-32K":
|
||||
return "v3.5-32K"
|
||||
case "Spark-4.0-Ultra":
|
||||
return "v4.0"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
|
||||
func apiVersion2domain(apiVersion string) string {
|
||||
switch apiVersion {
|
||||
case "v1.1":
|
||||
return "lite"
|
||||
case "v2.1":
|
||||
return "generalv2"
|
||||
case "v3.1":
|
||||
return "generalv3"
|
||||
case "v3.1-128K":
|
||||
return "pro-128k"
|
||||
case "v3.5":
|
||||
return "generalv3.5"
|
||||
case "v3.5-32K":
|
||||
return "max-32k"
|
||||
case "v4.0":
|
||||
return "4.0Ultra"
|
||||
}
|
||||
return "general" + apiVersion
|
||||
}
|
||||
|
||||
func getXunfeiAuthUrl(apiVersion string, apiKey string, apiSecret string) (string, string) {
|
||||
var authUrl string
|
||||
domain := apiVersion2domain(apiVersion)
|
||||
switch apiVersion {
|
||||
case "v3.1-128K":
|
||||
authUrl = buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/chat/pro-128k"), apiKey, apiSecret)
|
||||
break
|
||||
case "v3.5-32K":
|
||||
authUrl = buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/chat/max-32k"), apiKey, apiSecret)
|
||||
break
|
||||
default:
|
||||
authUrl = buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/%s/chat", apiVersion), apiKey, apiSecret)
|
||||
}
|
||||
return domain, authUrl
|
||||
}
|
@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/helper"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
@ -270,48 +271,3 @@ func xunfeiMakeRequest(textRequest model.GeneralOpenAIRequest, domain, authUrl,
|
||||
|
||||
return dataChan, stopChan, nil
|
||||
}
|
||||
|
||||
func parseAPIVersionByModelName(modelName string) string {
|
||||
index := strings.IndexAny(modelName, "-")
|
||||
if index != -1 {
|
||||
return modelName[index+1:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
|
||||
func apiVersion2domain(apiVersion string) string {
|
||||
switch apiVersion {
|
||||
case "v1.1":
|
||||
return "lite"
|
||||
case "v2.1":
|
||||
return "generalv2"
|
||||
case "v3.1":
|
||||
return "generalv3"
|
||||
case "v3.1-128K":
|
||||
return "pro-128k"
|
||||
case "v3.5":
|
||||
return "generalv3.5"
|
||||
case "v3.5-32K":
|
||||
return "max-32k"
|
||||
case "v4.0":
|
||||
return "4.0Ultra"
|
||||
}
|
||||
return "general" + apiVersion
|
||||
}
|
||||
|
||||
func getXunfeiAuthUrl(apiVersion string, apiKey string, apiSecret string) (string, string) {
|
||||
var authUrl string
|
||||
domain := apiVersion2domain(apiVersion)
|
||||
switch apiVersion {
|
||||
case "v3.1-128K":
|
||||
authUrl = buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/chat/pro-128k"), apiKey, apiSecret)
|
||||
break
|
||||
case "v3.5-32K":
|
||||
authUrl = buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/chat/max-32k"), apiKey, apiSecret)
|
||||
break
|
||||
default:
|
||||
authUrl = buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/%s/chat", apiVersion), apiKey, apiSecret)
|
||||
}
|
||||
return domain, authUrl
|
||||
}
|
||||
|
12
relay/adaptor/xunfeiv2/constants.go
Normal file
12
relay/adaptor/xunfeiv2/constants.go
Normal file
@ -0,0 +1,12 @@
|
||||
package xunfeiv2
|
||||
|
||||
// https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html#_3-%E8%AF%B7%E6%B1%82%E8%AF%B4%E6%98%8E
|
||||
|
||||
var ModelList = []string{
|
||||
"lite",
|
||||
"generalv3",
|
||||
"pro-128k",
|
||||
"generalv3.5",
|
||||
"max-32k",
|
||||
"4.0Ultra",
|
||||
}
|
@ -1,7 +1,14 @@
|
||||
package zhipu
|
||||
|
||||
// https://open.bigmodel.cn/pricing
|
||||
|
||||
var ModelList = []string{
|
||||
"chatglm_turbo", "chatglm_pro", "chatglm_std", "chatglm_lite",
|
||||
"glm-4", "glm-4v", "glm-3-turbo", "embedding-2",
|
||||
"cogview-3",
|
||||
"glm-zero-preview", "glm-4-plus", "glm-4-0520", "glm-4-airx",
|
||||
"glm-4-air", "glm-4-long", "glm-4-flashx", "glm-4-flash",
|
||||
"glm-4", "glm-3-turbo",
|
||||
"glm-4v-plus", "glm-4v", "glm-4v-flash",
|
||||
"cogview-3-plus", "cogview-3", "cogview-3-flash",
|
||||
"cogviewx", "cogviewx-flash",
|
||||
"charglm-4", "emohaa", "codegeex-4",
|
||||
"embedding-2", "embedding-3",
|
||||
}
|
||||
|
@ -3,8 +3,10 @@ package ratio
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var groupRatioLock sync.RWMutex
|
||||
var GroupRatio = map[string]float64{
|
||||
"default": 1,
|
||||
"vip": 1,
|
||||
@ -20,11 +22,15 @@ func GroupRatio2JSONString() string {
|
||||
}
|
||||
|
||||
func UpdateGroupRatioByJSONString(jsonStr string) error {
|
||||
groupRatioLock.Lock()
|
||||
defer groupRatioLock.Unlock()
|
||||
GroupRatio = make(map[string]float64)
|
||||
return json.Unmarshal([]byte(jsonStr), &GroupRatio)
|
||||
}
|
||||
|
||||
func GetGroupRatio(name string) float64 {
|
||||
groupRatioLock.RLock()
|
||||
defer groupRatioLock.RUnlock()
|
||||
ratio, ok := GroupRatio[name]
|
||||
if !ok {
|
||||
logger.SysError("group ratio not found: " + name)
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
)
|
||||
@ -15,6 +16,8 @@ const (
|
||||
RMB = USD / USD2RMB
|
||||
)
|
||||
|
||||
var modelRatioLock sync.RWMutex
|
||||
|
||||
// ModelRatio
|
||||
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
||||
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Blfmc9dlf
|
||||
@ -56,6 +59,8 @@ var ModelRatio = map[string]float64{
|
||||
"o1-preview-2024-09-12": 7.5,
|
||||
"o1-mini": 1.5, // $3.00 / 1M input tokens
|
||||
"o1-mini-2024-09-12": 1.5,
|
||||
"o3-mini": 1.5, // $3.00 / 1M input tokens
|
||||
"o3-mini-2025-01-31": 1.5,
|
||||
"davinci-002": 1, // $0.002 / 1K tokens
|
||||
"babbage-002": 0.2, // $0.0004 / 1K tokens
|
||||
"text-ada-001": 0.2,
|
||||
@ -82,15 +87,17 @@ var ModelRatio = map[string]float64{
|
||||
"text-moderation-latest": 0.1,
|
||||
"dall-e-2": 0.02 * USD, // $0.016 - $0.020 / image
|
||||
"dall-e-3": 0.04 * USD, // $0.040 - $0.120 / image
|
||||
// https://www.anthropic.com/api#pricing
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models
|
||||
"claude-instant-1.2": 0.8 / 1000 * USD,
|
||||
"claude-2.0": 8.0 / 1000 * USD,
|
||||
"claude-2.1": 8.0 / 1000 * USD,
|
||||
"claude-3-haiku-20240307": 0.25 / 1000 * USD,
|
||||
"claude-3-5-haiku-20241022": 1.0 / 1000 * USD,
|
||||
"claude-3-5-haiku-latest": 1.0 / 1000 * USD,
|
||||
"claude-3-sonnet-20240229": 3.0 / 1000 * USD,
|
||||
"claude-3-5-sonnet-20240620": 3.0 / 1000 * USD,
|
||||
"claude-3-5-sonnet-20241022": 3.0 / 1000 * USD,
|
||||
"claude-3-5-sonnet-latest": 3.0 / 1000 * USD,
|
||||
"claude-3-opus-20240229": 15.0 / 1000 * USD,
|
||||
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/hlrk4akp7
|
||||
"ERNIE-4.0-8K": 0.120 * RMB,
|
||||
@ -110,115 +117,162 @@ var ModelRatio = map[string]float64{
|
||||
"bge-large-en": 0.002 * RMB,
|
||||
"tao-8k": 0.002 * RMB,
|
||||
// https://ai.google.dev/pricing
|
||||
"gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
|
||||
"gemini-1.0-pro": 1,
|
||||
"gemini-1.5-pro": 1,
|
||||
"gemini-1.5-pro-001": 1,
|
||||
"gemini-1.5-flash": 1,
|
||||
"gemini-1.5-flash-001": 1,
|
||||
"gemini-2.0-flash-exp": 1,
|
||||
"gemini-2.0-flash-thinking-exp": 1,
|
||||
"gemini-2.0-flash-thinking-exp-01-21": 1,
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/pricing
|
||||
// "gemma-2-2b-it": 0,
|
||||
// "gemma-2-9b-it": 0,
|
||||
// "gemma-2-27b-it": 0,
|
||||
"gemini-pro": 0.25 * MILLI_USD, // $0.00025 / 1k characters -> $0.001 / 1k tokens
|
||||
"gemini-1.0-pro": 0.125 * MILLI_USD,
|
||||
"gemini-1.5-pro": 1.25 * MILLI_USD,
|
||||
"gemini-1.5-pro-001": 1.25 * MILLI_USD,
|
||||
"gemini-1.5-pro-experimental": 1.25 * MILLI_USD,
|
||||
"gemini-1.5-flash": 0.075 * MILLI_USD,
|
||||
"gemini-1.5-flash-001": 0.075 * MILLI_USD,
|
||||
"gemini-1.5-flash-8b": 0.0375 * MILLI_USD,
|
||||
"gemini-2.0-flash-exp": 0.075 * MILLI_USD,
|
||||
"gemini-2.0-flash": 0.15 * MILLI_USD,
|
||||
"gemini-2.0-flash-001": 0.15 * MILLI_USD,
|
||||
"gemini-2.0-flash-lite-preview-02-05": 0.075 * MILLI_USD,
|
||||
"gemini-2.0-flash-thinking-exp-01-21": 0.075 * MILLI_USD,
|
||||
"gemini-2.0-pro-exp-02-05": 1.25 * MILLI_USD,
|
||||
"aqa": 1,
|
||||
// https://open.bigmodel.cn/pricing
|
||||
"glm-4": 0.1 * RMB,
|
||||
"glm-4v": 0.1 * RMB,
|
||||
"glm-3-turbo": 0.005 * RMB,
|
||||
"embedding-2": 0.0005 * RMB,
|
||||
"chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens
|
||||
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
|
||||
"chatglm_std": 0.3572, // ¥0.005 / 1k tokens
|
||||
"chatglm_lite": 0.1429, // ¥0.002 / 1k tokens
|
||||
"cogview-3": 0.25 * RMB,
|
||||
"glm-zero-preview": 0.01 * RMB,
|
||||
"glm-4-plus": 0.05 * RMB,
|
||||
"glm-4-0520": 0.1 * RMB,
|
||||
"glm-4-airx": 0.01 * RMB,
|
||||
"glm-4-air": 0.0005 * RMB,
|
||||
"glm-4-long": 0.001 * RMB,
|
||||
"glm-4-flashx": 0.0001 * RMB,
|
||||
"glm-4-flash": 0,
|
||||
"glm-4": 0.1 * RMB, // deprecated model, available until 2025/06
|
||||
"glm-3-turbo": 0.001 * RMB, // deprecated model, available until 2025/06
|
||||
"glm-4v-plus": 0.004 * RMB,
|
||||
"glm-4v": 0.05 * RMB,
|
||||
"glm-4v-flash": 0,
|
||||
"cogview-3-plus": 0.06 * RMB,
|
||||
"cogview-3": 0.1 * RMB,
|
||||
"cogview-3-flash": 0,
|
||||
"cogviewx": 0.5 * RMB,
|
||||
"cogviewx-flash": 0,
|
||||
"charglm-4": 0.001 * RMB,
|
||||
"emohaa": 0.015 * RMB,
|
||||
"codegeex-4": 0.0001 * RMB,
|
||||
"embedding-2": 0.0005 * RMB,
|
||||
"embedding-3": 0.0005 * RMB,
|
||||
// https://help.aliyun.com/zh/dashscope/developer-reference/tongyi-thousand-questions-metering-and-billing
|
||||
"qwen-turbo": 1.4286, // ¥0.02 / 1k tokens
|
||||
"qwen-turbo-latest": 1.4286,
|
||||
"qwen-plus": 1.4286,
|
||||
"qwen-plus-latest": 1.4286,
|
||||
"qwen-max": 1.4286,
|
||||
"qwen-max-latest": 1.4286,
|
||||
"qwen-max-longcontext": 1.4286,
|
||||
"qwen-vl-max": 1.4286,
|
||||
"qwen-vl-max-latest": 1.4286,
|
||||
"qwen-vl-plus": 1.4286,
|
||||
"qwen-vl-plus-latest": 1.4286,
|
||||
"qwen-vl-ocr": 1.4286,
|
||||
"qwen-vl-ocr-latest": 1.4286,
|
||||
"qwen-audio-turbo": 1.4286,
|
||||
"qwen-math-plus": 1.4286,
|
||||
"qwen-math-plus-latest": 1.4286,
|
||||
"qwen-math-turbo": 1.4286,
|
||||
"qwen-math-turbo-latest": 1.4286,
|
||||
"qwen-coder-plus": 1.4286,
|
||||
"qwen-coder-plus-latest": 1.4286,
|
||||
"qwen-coder-turbo": 1.4286,
|
||||
"qwen-coder-turbo-latest": 1.4286,
|
||||
"qwq-32b-preview": 1.4286,
|
||||
"qwen2.5-72b-instruct": 1.4286,
|
||||
"qwen2.5-32b-instruct": 1.4286,
|
||||
"qwen2.5-14b-instruct": 1.4286,
|
||||
"qwen2.5-7b-instruct": 1.4286,
|
||||
"qwen2.5-3b-instruct": 1.4286,
|
||||
"qwen2.5-1.5b-instruct": 1.4286,
|
||||
"qwen2.5-0.5b-instruct": 1.4286,
|
||||
"qwen2-72b-instruct": 1.4286,
|
||||
"qwen2-57b-a14b-instruct": 1.4286,
|
||||
"qwen2-7b-instruct": 1.4286,
|
||||
"qwen2-1.5b-instruct": 1.4286,
|
||||
"qwen2-0.5b-instruct": 1.4286,
|
||||
"qwen1.5-110b-chat": 1.4286,
|
||||
"qwen1.5-72b-chat": 1.4286,
|
||||
"qwen1.5-32b-chat": 1.4286,
|
||||
"qwen1.5-14b-chat": 1.4286,
|
||||
"qwen1.5-7b-chat": 1.4286,
|
||||
"qwen1.5-1.8b-chat": 1.4286,
|
||||
"qwen1.5-0.5b-chat": 1.4286,
|
||||
"qwen-72b-chat": 1.4286,
|
||||
"qwen-14b-chat": 1.4286,
|
||||
"qwen-7b-chat": 1.4286,
|
||||
"qwen-1.8b-chat": 1.4286,
|
||||
"qwen-1.8b-longcontext-chat": 1.4286,
|
||||
"qwen2-vl-7b-instruct": 1.4286,
|
||||
"qwen2-vl-2b-instruct": 1.4286,
|
||||
"qwen-vl-v1": 1.4286,
|
||||
"qwen-vl-chat-v1": 1.4286,
|
||||
"qwen2-audio-instruct": 1.4286,
|
||||
"qwen-audio-chat": 1.4286,
|
||||
"qwen2.5-math-72b-instruct": 1.4286,
|
||||
"qwen2.5-math-7b-instruct": 1.4286,
|
||||
"qwen2.5-math-1.5b-instruct": 1.4286,
|
||||
"qwen2-math-72b-instruct": 1.4286,
|
||||
"qwen2-math-7b-instruct": 1.4286,
|
||||
"qwen2-math-1.5b-instruct": 1.4286,
|
||||
"qwen2.5-coder-32b-instruct": 1.4286,
|
||||
"qwen2.5-coder-14b-instruct": 1.4286,
|
||||
"qwen2.5-coder-7b-instruct": 1.4286,
|
||||
"qwen2.5-coder-3b-instruct": 1.4286,
|
||||
"qwen2.5-coder-1.5b-instruct": 1.4286,
|
||||
"qwen2.5-coder-0.5b-instruct": 1.4286,
|
||||
"text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens
|
||||
"text-embedding-v3": 0.05,
|
||||
"text-embedding-v2": 0.05,
|
||||
"text-embedding-async-v2": 0.05,
|
||||
"text-embedding-async-v1": 0.05,
|
||||
"ali-stable-diffusion-xl": 8.00,
|
||||
"ali-stable-diffusion-v1.5": 8.00,
|
||||
"wanx-v1": 8.00,
|
||||
"SparkDesk": 1.2858, // ¥0.018 / 1k tokens
|
||||
"SparkDesk-v1.1": 1.2858, // ¥0.018 / 1k tokens
|
||||
"SparkDesk-v2.1": 1.2858, // ¥0.018 / 1k tokens
|
||||
"SparkDesk-v3.1": 1.2858, // ¥0.018 / 1k tokens
|
||||
"SparkDesk-v3.1-128K": 1.2858, // ¥0.018 / 1k tokens
|
||||
"SparkDesk-v3.5": 1.2858, // ¥0.018 / 1k tokens
|
||||
"SparkDesk-v3.5-32K": 1.2858, // ¥0.018 / 1k tokens
|
||||
"SparkDesk-v4.0": 1.2858, // ¥0.018 / 1k tokens
|
||||
"360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens
|
||||
"embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens
|
||||
"embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
||||
"semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
||||
"hunyuan": 7.143, // ¥0.1 / 1k tokens // https://cloud.tencent.com/document/product/1729/97731#e0e6be58-60c8-469f-bdeb-6c264ce3b4d0
|
||||
"ChatStd": 0.01 * RMB,
|
||||
"ChatPro": 0.1 * RMB,
|
||||
"qwen-turbo": 0.0003 * RMB,
|
||||
"qwen-turbo-latest": 0.0003 * RMB,
|
||||
"qwen-plus": 0.0008 * RMB,
|
||||
"qwen-plus-latest": 0.0008 * RMB,
|
||||
"qwen-max": 0.0024 * RMB,
|
||||
"qwen-max-latest": 0.0024 * RMB,
|
||||
"qwen-max-longcontext": 0.0005 * RMB,
|
||||
"qwen-vl-max": 0.003 * RMB,
|
||||
"qwen-vl-max-latest": 0.003 * RMB,
|
||||
"qwen-vl-plus": 0.0015 * RMB,
|
||||
"qwen-vl-plus-latest": 0.0015 * RMB,
|
||||
"qwen-vl-ocr": 0.005 * RMB,
|
||||
"qwen-vl-ocr-latest": 0.005 * RMB,
|
||||
"qwen-audio-turbo": 1.4286,
|
||||
"qwen-math-plus": 0.004 * RMB,
|
||||
"qwen-math-plus-latest": 0.004 * RMB,
|
||||
"qwen-math-turbo": 0.002 * RMB,
|
||||
"qwen-math-turbo-latest": 0.002 * RMB,
|
||||
"qwen-coder-plus": 0.0035 * RMB,
|
||||
"qwen-coder-plus-latest": 0.0035 * RMB,
|
||||
"qwen-coder-turbo": 0.002 * RMB,
|
||||
"qwen-coder-turbo-latest": 0.002 * RMB,
|
||||
"qwen-mt-plus": 0.015 * RMB,
|
||||
"qwen-mt-turbo": 0.001 * RMB,
|
||||
"qwq-32b-preview": 0.002 * RMB,
|
||||
"qwen2.5-72b-instruct": 0.004 * RMB,
|
||||
"qwen2.5-32b-instruct": 0.03 * RMB,
|
||||
"qwen2.5-14b-instruct": 0.001 * RMB,
|
||||
"qwen2.5-7b-instruct": 0.0005 * RMB,
|
||||
"qwen2.5-3b-instruct": 0.006 * RMB,
|
||||
"qwen2.5-1.5b-instruct": 0.0003 * RMB,
|
||||
"qwen2.5-0.5b-instruct": 0.0003 * RMB,
|
||||
"qwen2-72b-instruct": 0.004 * RMB,
|
||||
"qwen2-57b-a14b-instruct": 0.0035 * RMB,
|
||||
"qwen2-7b-instruct": 0.001 * RMB,
|
||||
"qwen2-1.5b-instruct": 0.001 * RMB,
|
||||
"qwen2-0.5b-instruct": 0.001 * RMB,
|
||||
"qwen1.5-110b-chat": 0.007 * RMB,
|
||||
"qwen1.5-72b-chat": 0.005 * RMB,
|
||||
"qwen1.5-32b-chat": 0.0035 * RMB,
|
||||
"qwen1.5-14b-chat": 0.002 * RMB,
|
||||
"qwen1.5-7b-chat": 0.001 * RMB,
|
||||
"qwen1.5-1.8b-chat": 0.001 * RMB,
|
||||
"qwen1.5-0.5b-chat": 0.001 * RMB,
|
||||
"qwen-72b-chat": 0.02 * RMB,
|
||||
"qwen-14b-chat": 0.008 * RMB,
|
||||
"qwen-7b-chat": 0.006 * RMB,
|
||||
"qwen-1.8b-chat": 0.006 * RMB,
|
||||
"qwen-1.8b-longcontext-chat": 0.006 * RMB,
|
||||
"qvq-72b-preview": 0.012 * RMB,
|
||||
"qwen2.5-vl-72b-instruct": 0.016 * RMB,
|
||||
"qwen2.5-vl-7b-instruct": 0.002 * RMB,
|
||||
"qwen2.5-vl-3b-instruct": 0.0012 * RMB,
|
||||
"qwen2-vl-7b-instruct": 0.016 * RMB,
|
||||
"qwen2-vl-2b-instruct": 0.002 * RMB,
|
||||
"qwen-vl-v1": 0.002 * RMB,
|
||||
"qwen-vl-chat-v1": 0.002 * RMB,
|
||||
"qwen2-audio-instruct": 0.002 * RMB,
|
||||
"qwen-audio-chat": 0.002 * RMB,
|
||||
"qwen2.5-math-72b-instruct": 0.004 * RMB,
|
||||
"qwen2.5-math-7b-instruct": 0.001 * RMB,
|
||||
"qwen2.5-math-1.5b-instruct": 0.001 * RMB,
|
||||
"qwen2-math-72b-instruct": 0.004 * RMB,
|
||||
"qwen2-math-7b-instruct": 0.001 * RMB,
|
||||
"qwen2-math-1.5b-instruct": 0.001 * RMB,
|
||||
"qwen2.5-coder-32b-instruct": 0.002 * RMB,
|
||||
"qwen2.5-coder-14b-instruct": 0.002 * RMB,
|
||||
"qwen2.5-coder-7b-instruct": 0.001 * RMB,
|
||||
"qwen2.5-coder-3b-instruct": 0.001 * RMB,
|
||||
"qwen2.5-coder-1.5b-instruct": 0.001 * RMB,
|
||||
"qwen2.5-coder-0.5b-instruct": 0.001 * RMB,
|
||||
"text-embedding-v1": 0.0007 * RMB, // ¥0.0007 / 1k tokens
|
||||
"text-embedding-v3": 0.0007 * RMB,
|
||||
"text-embedding-v2": 0.0007 * RMB,
|
||||
"text-embedding-async-v2": 0.0007 * RMB,
|
||||
"text-embedding-async-v1": 0.0007 * RMB,
|
||||
"ali-stable-diffusion-xl": 8.00,
|
||||
"ali-stable-diffusion-v1.5": 8.00,
|
||||
"wanx-v1": 8.00,
|
||||
"deepseek-r1": 0.002 * RMB,
|
||||
"deepseek-v3": 0.001 * RMB,
|
||||
"deepseek-r1-distill-qwen-1.5b": 0.001 * RMB,
|
||||
"deepseek-r1-distill-qwen-7b": 0.0005 * RMB,
|
||||
"deepseek-r1-distill-qwen-14b": 0.001 * RMB,
|
||||
"deepseek-r1-distill-qwen-32b": 0.002 * RMB,
|
||||
"deepseek-r1-distill-llama-8b": 0.0005 * RMB,
|
||||
"deepseek-r1-distill-llama-70b": 0.004 * RMB,
|
||||
"SparkDesk": 1.2858, // ¥0.018 / 1k tokens
|
||||
"SparkDesk-v1.1": 1.2858, // ¥0.018 / 1k tokens
|
||||
"SparkDesk-v2.1": 1.2858, // ¥0.018 / 1k tokens
|
||||
"SparkDesk-v3.1": 1.2858, // ¥0.018 / 1k tokens
|
||||
"SparkDesk-v3.1-128K": 1.2858, // ¥0.018 / 1k tokens
|
||||
"SparkDesk-v3.5": 1.2858, // ¥0.018 / 1k tokens
|
||||
"SparkDesk-v3.5-32K": 1.2858, // ¥0.018 / 1k tokens
|
||||
"SparkDesk-v4.0": 1.2858, // ¥0.018 / 1k tokens
|
||||
"360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens
|
||||
"embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens
|
||||
"embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
||||
"semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
||||
// https://cloud.tencent.com/document/product/1729/97731#e0e6be58-60c8-469f-bdeb-6c264ce3b4d0
|
||||
"hunyuan-turbo": 0.015 * RMB,
|
||||
"hunyuan-large": 0.004 * RMB,
|
||||
"hunyuan-large-longcontext": 0.006 * RMB,
|
||||
"hunyuan-standard": 0.0008 * RMB,
|
||||
"hunyuan-standard-256K": 0.0005 * RMB,
|
||||
"hunyuan-translation-lite": 0.005 * RMB,
|
||||
"hunyuan-role": 0.004 * RMB,
|
||||
"hunyuan-functioncall": 0.004 * RMB,
|
||||
"hunyuan-code": 0.004 * RMB,
|
||||
"hunyuan-turbo-vision": 0.08 * RMB,
|
||||
"hunyuan-vision": 0.018 * RMB,
|
||||
"hunyuan-embedding": 0.0007 * RMB,
|
||||
// https://platform.moonshot.cn/pricing
|
||||
"moonshot-v1-8k": 0.012 * RMB,
|
||||
"moonshot-v1-32k": 0.024 * RMB,
|
||||
@ -333,6 +387,238 @@ var ModelRatio = map[string]float64{
|
||||
"mistralai/mistral-7b-instruct-v0.2": 0.050 * USD,
|
||||
"mistralai/mistral-7b-v0.1": 0.050 * USD,
|
||||
"mistralai/mixtral-8x7b-instruct-v0.1": 0.300 * USD,
|
||||
//https://openrouter.ai/models
|
||||
"01-ai/yi-large": 1.5,
|
||||
"aetherwiing/mn-starcannon-12b": 0.6,
|
||||
"ai21/jamba-1-5-large": 4.0,
|
||||
"ai21/jamba-1-5-mini": 0.2,
|
||||
"ai21/jamba-instruct": 0.35,
|
||||
"aion-labs/aion-1.0": 6.0,
|
||||
"aion-labs/aion-1.0-mini": 1.2,
|
||||
"aion-labs/aion-rp-llama-3.1-8b": 0.1,
|
||||
"allenai/llama-3.1-tulu-3-405b": 5.0,
|
||||
"alpindale/goliath-120b": 4.6875,
|
||||
"alpindale/magnum-72b": 1.125,
|
||||
"amazon/nova-lite-v1": 0.12,
|
||||
"amazon/nova-micro-v1": 0.07,
|
||||
"amazon/nova-pro-v1": 1.6,
|
||||
"anthracite-org/magnum-v2-72b": 1.5,
|
||||
"anthracite-org/magnum-v4-72b": 1.125,
|
||||
"anthropic/claude-2": 12.0,
|
||||
"anthropic/claude-2.0": 12.0,
|
||||
"anthropic/claude-2.0:beta": 12.0,
|
||||
"anthropic/claude-2.1": 12.0,
|
||||
"anthropic/claude-2.1:beta": 12.0,
|
||||
"anthropic/claude-2:beta": 12.0,
|
||||
"anthropic/claude-3-haiku": 0.625,
|
||||
"anthropic/claude-3-haiku:beta": 0.625,
|
||||
"anthropic/claude-3-opus": 37.5,
|
||||
"anthropic/claude-3-opus:beta": 37.5,
|
||||
"anthropic/claude-3-sonnet": 7.5,
|
||||
"anthropic/claude-3-sonnet:beta": 7.5,
|
||||
"anthropic/claude-3.5-haiku": 2.0,
|
||||
"anthropic/claude-3.5-haiku-20241022": 2.0,
|
||||
"anthropic/claude-3.5-haiku-20241022:beta": 2.0,
|
||||
"anthropic/claude-3.5-haiku:beta": 2.0,
|
||||
"anthropic/claude-3.5-sonnet": 7.5,
|
||||
"anthropic/claude-3.5-sonnet-20240620": 7.5,
|
||||
"anthropic/claude-3.5-sonnet-20240620:beta": 7.5,
|
||||
"anthropic/claude-3.5-sonnet:beta": 7.5,
|
||||
"cognitivecomputations/dolphin-mixtral-8x22b": 0.45,
|
||||
"cognitivecomputations/dolphin-mixtral-8x7b": 0.25,
|
||||
"cohere/command": 0.95,
|
||||
"cohere/command-r": 0.7125,
|
||||
"cohere/command-r-03-2024": 0.7125,
|
||||
"cohere/command-r-08-2024": 0.285,
|
||||
"cohere/command-r-plus": 7.125,
|
||||
"cohere/command-r-plus-04-2024": 7.125,
|
||||
"cohere/command-r-plus-08-2024": 4.75,
|
||||
"cohere/command-r7b-12-2024": 0.075,
|
||||
"databricks/dbrx-instruct": 0.6,
|
||||
"deepseek/deepseek-chat": 0.445,
|
||||
"deepseek/deepseek-chat-v2.5": 1.0,
|
||||
"deepseek/deepseek-chat:free": 0.0,
|
||||
"deepseek/deepseek-r1": 1.2,
|
||||
"deepseek/deepseek-r1-distill-llama-70b": 0.345,
|
||||
"deepseek/deepseek-r1-distill-llama-70b:free": 0.0,
|
||||
"deepseek/deepseek-r1-distill-llama-8b": 0.02,
|
||||
"deepseek/deepseek-r1-distill-qwen-1.5b": 0.09,
|
||||
"deepseek/deepseek-r1-distill-qwen-14b": 0.075,
|
||||
"deepseek/deepseek-r1-distill-qwen-32b": 0.09,
|
||||
"deepseek/deepseek-r1:free": 0.0,
|
||||
"eva-unit-01/eva-llama-3.33-70b": 3.0,
|
||||
"eva-unit-01/eva-qwen-2.5-32b": 1.7,
|
||||
"eva-unit-01/eva-qwen-2.5-72b": 3.0,
|
||||
"google/gemini-2.0-flash-001": 0.2,
|
||||
"google/gemini-2.0-flash-exp:free": 0.0,
|
||||
"google/gemini-2.0-flash-lite-preview-02-05:free": 0.0,
|
||||
"google/gemini-2.0-flash-thinking-exp-1219:free": 0.0,
|
||||
"google/gemini-2.0-flash-thinking-exp:free": 0.0,
|
||||
"google/gemini-2.0-pro-exp-02-05:free": 0.0,
|
||||
"google/gemini-exp-1206:free": 0.0,
|
||||
"google/gemini-flash-1.5": 0.15,
|
||||
"google/gemini-flash-1.5-8b": 0.075,
|
||||
"google/gemini-flash-1.5-8b-exp": 0.0,
|
||||
"google/gemini-pro": 0.75,
|
||||
"google/gemini-pro-1.5": 2.5,
|
||||
"google/gemini-pro-vision": 0.75,
|
||||
"google/gemma-2-27b-it": 0.135,
|
||||
"google/gemma-2-9b-it": 0.03,
|
||||
"google/gemma-2-9b-it:free": 0.0,
|
||||
"google/gemma-7b-it": 0.075,
|
||||
"google/learnlm-1.5-pro-experimental:free": 0.0,
|
||||
"google/palm-2-chat-bison": 1.0,
|
||||
"google/palm-2-chat-bison-32k": 1.0,
|
||||
"google/palm-2-codechat-bison": 1.0,
|
||||
"google/palm-2-codechat-bison-32k": 1.0,
|
||||
"gryphe/mythomax-l2-13b": 0.0325,
|
||||
"gryphe/mythomax-l2-13b:free": 0.0,
|
||||
"huggingfaceh4/zephyr-7b-beta:free": 0.0,
|
||||
"infermatic/mn-inferor-12b": 0.6,
|
||||
"inflection/inflection-3-pi": 5.0,
|
||||
"inflection/inflection-3-productivity": 5.0,
|
||||
"jondurbin/airoboros-l2-70b": 0.25,
|
||||
"liquid/lfm-3b": 0.01,
|
||||
"liquid/lfm-40b": 0.075,
|
||||
"liquid/lfm-7b": 0.005,
|
||||
"mancer/weaver": 1.125,
|
||||
"meta-llama/llama-2-13b-chat": 0.11,
|
||||
"meta-llama/llama-2-70b-chat": 0.45,
|
||||
"meta-llama/llama-3-70b-instruct": 0.2,
|
||||
"meta-llama/llama-3-8b-instruct": 0.03,
|
||||
"meta-llama/llama-3-8b-instruct:free": 0.0,
|
||||
"meta-llama/llama-3.1-405b": 1.0,
|
||||
"meta-llama/llama-3.1-405b-instruct": 0.4,
|
||||
"meta-llama/llama-3.1-70b-instruct": 0.15,
|
||||
"meta-llama/llama-3.1-8b-instruct": 0.025,
|
||||
"meta-llama/llama-3.2-11b-vision-instruct": 0.0275,
|
||||
"meta-llama/llama-3.2-11b-vision-instruct:free": 0.0,
|
||||
"meta-llama/llama-3.2-1b-instruct": 0.005,
|
||||
"meta-llama/llama-3.2-3b-instruct": 0.0125,
|
||||
"meta-llama/llama-3.2-90b-vision-instruct": 0.8,
|
||||
"meta-llama/llama-3.3-70b-instruct": 0.15,
|
||||
"meta-llama/llama-3.3-70b-instruct:free": 0.0,
|
||||
"meta-llama/llama-guard-2-8b": 0.1,
|
||||
"microsoft/phi-3-medium-128k-instruct": 0.5,
|
||||
"microsoft/phi-3-medium-128k-instruct:free": 0.0,
|
||||
"microsoft/phi-3-mini-128k-instruct": 0.05,
|
||||
"microsoft/phi-3-mini-128k-instruct:free": 0.0,
|
||||
"microsoft/phi-3.5-mini-128k-instruct": 0.05,
|
||||
"microsoft/phi-4": 0.07,
|
||||
"microsoft/wizardlm-2-7b": 0.035,
|
||||
"microsoft/wizardlm-2-8x22b": 0.25,
|
||||
"minimax/minimax-01": 0.55,
|
||||
"mistralai/codestral-2501": 0.45,
|
||||
"mistralai/codestral-mamba": 0.125,
|
||||
"mistralai/ministral-3b": 0.02,
|
||||
"mistralai/ministral-8b": 0.05,
|
||||
"mistralai/mistral-7b-instruct": 0.0275,
|
||||
"mistralai/mistral-7b-instruct-v0.1": 0.1,
|
||||
"mistralai/mistral-7b-instruct-v0.3": 0.0275,
|
||||
"mistralai/mistral-7b-instruct:free": 0.0,
|
||||
"mistralai/mistral-large": 3.0,
|
||||
"mistralai/mistral-large-2407": 3.0,
|
||||
"mistralai/mistral-large-2411": 3.0,
|
||||
"mistralai/mistral-medium": 4.05,
|
||||
"mistralai/mistral-nemo": 0.04,
|
||||
"mistralai/mistral-nemo:free": 0.0,
|
||||
"mistralai/mistral-small": 0.3,
|
||||
"mistralai/mistral-small-24b-instruct-2501": 0.07,
|
||||
"mistralai/mistral-small-24b-instruct-2501:free": 0.0,
|
||||
"mistralai/mistral-tiny": 0.125,
|
||||
"mistralai/mixtral-8x22b-instruct": 0.45,
|
||||
"mistralai/mixtral-8x7b": 0.3,
|
||||
"mistralai/mixtral-8x7b-instruct": 0.12,
|
||||
"mistralai/pixtral-12b": 0.05,
|
||||
"mistralai/pixtral-large-2411": 3.0,
|
||||
"neversleep/llama-3-lumimaid-70b": 2.25,
|
||||
"neversleep/llama-3-lumimaid-8b": 0.5625,
|
||||
"neversleep/llama-3-lumimaid-8b:extended": 0.5625,
|
||||
"neversleep/llama-3.1-lumimaid-70b": 2.25,
|
||||
"neversleep/llama-3.1-lumimaid-8b": 0.5625,
|
||||
"neversleep/noromaid-20b": 1.125,
|
||||
"nothingiisreal/mn-celeste-12b": 0.6,
|
||||
"nousresearch/hermes-2-pro-llama-3-8b": 0.02,
|
||||
"nousresearch/hermes-3-llama-3.1-405b": 0.4,
|
||||
"nousresearch/hermes-3-llama-3.1-70b": 0.15,
|
||||
"nousresearch/nous-hermes-2-mixtral-8x7b-dpo": 0.3,
|
||||
"nousresearch/nous-hermes-llama2-13b": 0.085,
|
||||
"nvidia/llama-3.1-nemotron-70b-instruct": 0.15,
|
||||
"nvidia/llama-3.1-nemotron-70b-instruct:free": 0.0,
|
||||
"openai/chatgpt-4o-latest": 7.5,
|
||||
"openai/gpt-3.5-turbo": 0.75,
|
||||
"openai/gpt-3.5-turbo-0125": 0.75,
|
||||
"openai/gpt-3.5-turbo-0613": 1.0,
|
||||
"openai/gpt-3.5-turbo-1106": 1.0,
|
||||
"openai/gpt-3.5-turbo-16k": 2.0,
|
||||
"openai/gpt-3.5-turbo-instruct": 1.0,
|
||||
"openai/gpt-4": 30.0,
|
||||
"openai/gpt-4-0314": 30.0,
|
||||
"openai/gpt-4-1106-preview": 15.0,
|
||||
"openai/gpt-4-32k": 60.0,
|
||||
"openai/gpt-4-32k-0314": 60.0,
|
||||
"openai/gpt-4-turbo": 15.0,
|
||||
"openai/gpt-4-turbo-preview": 15.0,
|
||||
"openai/gpt-4o": 5.0,
|
||||
"openai/gpt-4o-2024-05-13": 7.5,
|
||||
"openai/gpt-4o-2024-08-06": 5.0,
|
||||
"openai/gpt-4o-2024-11-20": 5.0,
|
||||
"openai/gpt-4o-mini": 0.3,
|
||||
"openai/gpt-4o-mini-2024-07-18": 0.3,
|
||||
"openai/gpt-4o:extended": 9.0,
|
||||
"openai/o1": 30.0,
|
||||
"openai/o1-mini": 2.2,
|
||||
"openai/o1-mini-2024-09-12": 2.2,
|
||||
"openai/o1-preview": 30.0,
|
||||
"openai/o1-preview-2024-09-12": 30.0,
|
||||
"openai/o3-mini": 2.2,
|
||||
"openai/o3-mini-high": 2.2,
|
||||
"openchat/openchat-7b": 0.0275,
|
||||
"openchat/openchat-7b:free": 0.0,
|
||||
"openrouter/auto": -500000.0,
|
||||
"perplexity/llama-3.1-sonar-huge-128k-online": 2.5,
|
||||
"perplexity/llama-3.1-sonar-large-128k-chat": 0.5,
|
||||
"perplexity/llama-3.1-sonar-large-128k-online": 0.5,
|
||||
"perplexity/llama-3.1-sonar-small-128k-chat": 0.1,
|
||||
"perplexity/llama-3.1-sonar-small-128k-online": 0.1,
|
||||
"perplexity/sonar": 0.5,
|
||||
"perplexity/sonar-reasoning": 2.5,
|
||||
"pygmalionai/mythalion-13b": 0.6,
|
||||
"qwen/qvq-72b-preview": 0.25,
|
||||
"qwen/qwen-2-72b-instruct": 0.45,
|
||||
"qwen/qwen-2-7b-instruct": 0.027,
|
||||
"qwen/qwen-2-7b-instruct:free": 0.0,
|
||||
"qwen/qwen-2-vl-72b-instruct": 0.2,
|
||||
"qwen/qwen-2-vl-7b-instruct": 0.05,
|
||||
"qwen/qwen-2.5-72b-instruct": 0.2,
|
||||
"qwen/qwen-2.5-7b-instruct": 0.025,
|
||||
"qwen/qwen-2.5-coder-32b-instruct": 0.08,
|
||||
"qwen/qwen-max": 3.2,
|
||||
"qwen/qwen-plus": 0.6,
|
||||
"qwen/qwen-turbo": 0.1,
|
||||
"qwen/qwen-vl-plus:free": 0.0,
|
||||
"qwen/qwen2.5-vl-72b-instruct:free": 0.0,
|
||||
"qwen/qwq-32b-preview": 0.09,
|
||||
"raifle/sorcererlm-8x22b": 2.25,
|
||||
"sao10k/fimbulvetr-11b-v2": 0.6,
|
||||
"sao10k/l3-euryale-70b": 0.4,
|
||||
"sao10k/l3-lunaris-8b": 0.03,
|
||||
"sao10k/l3.1-70b-hanami-x1": 1.5,
|
||||
"sao10k/l3.1-euryale-70b": 0.4,
|
||||
"sao10k/l3.3-euryale-70b": 0.4,
|
||||
"sophosympatheia/midnight-rose-70b": 0.4,
|
||||
"sophosympatheia/rogue-rose-103b-v0.2:free": 0.0,
|
||||
"teknium/openhermes-2.5-mistral-7b": 0.085,
|
||||
"thedrummer/rocinante-12b": 0.25,
|
||||
"thedrummer/unslopnemo-12b": 0.25,
|
||||
"undi95/remm-slerp-l2-13b": 0.6,
|
||||
"undi95/toppy-m-7b": 0.035,
|
||||
"undi95/toppy-m-7b:free": 0.0,
|
||||
"x-ai/grok-2-1212": 5.0,
|
||||
"x-ai/grok-2-vision-1212": 5.0,
|
||||
"x-ai/grok-beta": 7.5,
|
||||
"x-ai/grok-vision-beta": 7.5,
|
||||
"xwin-lm/xwin-lm-70b": 1.875,
|
||||
}
|
||||
|
||||
var CompletionRatio = map[string]float64{
|
||||
@ -391,11 +677,15 @@ func ModelRatio2JSONString() string {
|
||||
}
|
||||
|
||||
func UpdateModelRatioByJSONString(jsonStr string) error {
|
||||
modelRatioLock.Lock()
|
||||
defer modelRatioLock.Unlock()
|
||||
ModelRatio = make(map[string]float64)
|
||||
return json.Unmarshal([]byte(jsonStr), &ModelRatio)
|
||||
}
|
||||
|
||||
func GetModelRatio(name string, channelType int) float64 {
|
||||
modelRatioLock.RLock()
|
||||
defer modelRatioLock.RUnlock()
|
||||
if strings.HasPrefix(name, "qwen-") && strings.HasSuffix(name, "-internet") {
|
||||
name = strings.TrimSuffix(name, "-internet")
|
||||
}
|
||||
|
@ -48,5 +48,10 @@ const (
|
||||
SiliconFlow
|
||||
XAI
|
||||
Replicate
|
||||
BaiduV2
|
||||
XunfeiV2
|
||||
AliBailian
|
||||
OpenAICompatible
|
||||
GeminiOpenAICompatible
|
||||
Dummy
|
||||
)
|
||||
|
@ -48,6 +48,12 @@ var ChannelBaseURLs = []string{
|
||||
"https://api.siliconflow.cn", // 44
|
||||
"https://api.x.ai", // 45
|
||||
"https://api.replicate.com/v1/models/", // 46
|
||||
"https://qianfan.baidubce.com", // 47
|
||||
"https://spark-api-open.xf-yun.com", // 48
|
||||
"https://dashscope.aliyuncs.com", // 49
|
||||
"", // 50
|
||||
|
||||
"https://generativelanguage.googleapis.com/v1beta/openai/", // 51
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -38,7 +38,7 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode {
|
||||
textRequest.Model, _ = getMappedModelName(textRequest.Model, meta.ModelMapping)
|
||||
meta.ActualModelName = textRequest.Model
|
||||
// set system prompt if not empty
|
||||
systemPromptReset := setSystemPrompt(ctx, textRequest, meta.SystemPrompt)
|
||||
systemPromptReset := setSystemPrompt(ctx, textRequest, meta.ForcedSystemPrompt)
|
||||
// get model ratio & group ratio
|
||||
modelRatio := billingratio.GetModelRatio(textRequest.Model, meta.ChannelType)
|
||||
groupRatio := billingratio.GetGroupRatio(meta.Group)
|
||||
@ -88,7 +88,11 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode {
|
||||
}
|
||||
|
||||
func getRequestBody(c *gin.Context, meta *meta.Meta, textRequest *model.GeneralOpenAIRequest, adaptor adaptor.Adaptor) (io.Reader, error) {
|
||||
if !config.EnforceIncludeUsage && meta.APIType == apitype.OpenAI && meta.OriginModelName == meta.ActualModelName && meta.ChannelType != channeltype.Baichuan {
|
||||
if !config.EnforceIncludeUsage &&
|
||||
meta.APIType == apitype.OpenAI &&
|
||||
meta.OriginModelName == meta.ActualModelName &&
|
||||
meta.ChannelType != channeltype.Baichuan &&
|
||||
meta.ForcedSystemPrompt == "" {
|
||||
// no need to convert request for openai
|
||||
return c.Request.Body, nil
|
||||
}
|
||||
|
@ -30,29 +30,29 @@ type Meta struct {
|
||||
// OriginModelName is the model name from the raw user request
|
||||
OriginModelName string
|
||||
// ActualModelName is the model name after mapping
|
||||
ActualModelName string
|
||||
RequestURLPath string
|
||||
PromptTokens int // only for DoResponse
|
||||
SystemPrompt string
|
||||
StartTime time.Time
|
||||
ActualModelName string
|
||||
RequestURLPath string
|
||||
PromptTokens int // only for DoResponse
|
||||
ForcedSystemPrompt string
|
||||
StartTime time.Time
|
||||
}
|
||||
|
||||
func GetByContext(c *gin.Context) *Meta {
|
||||
meta := Meta{
|
||||
Mode: relaymode.GetByPath(c.Request.URL.Path),
|
||||
ChannelType: c.GetInt(ctxkey.Channel),
|
||||
ChannelId: c.GetInt(ctxkey.ChannelId),
|
||||
TokenId: c.GetInt(ctxkey.TokenId),
|
||||
TokenName: c.GetString(ctxkey.TokenName),
|
||||
UserId: c.GetInt(ctxkey.Id),
|
||||
Group: c.GetString(ctxkey.Group),
|
||||
ModelMapping: c.GetStringMapString(ctxkey.ModelMapping),
|
||||
OriginModelName: c.GetString(ctxkey.RequestModel),
|
||||
BaseURL: c.GetString(ctxkey.BaseURL),
|
||||
APIKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
|
||||
RequestURLPath: c.Request.URL.String(),
|
||||
SystemPrompt: c.GetString(ctxkey.SystemPrompt),
|
||||
StartTime: time.Now(),
|
||||
Mode: relaymode.GetByPath(c.Request.URL.Path),
|
||||
ChannelType: c.GetInt(ctxkey.Channel),
|
||||
ChannelId: c.GetInt(ctxkey.ChannelId),
|
||||
TokenId: c.GetInt(ctxkey.TokenId),
|
||||
TokenName: c.GetString(ctxkey.TokenName),
|
||||
UserId: c.GetInt(ctxkey.Id),
|
||||
Group: c.GetString(ctxkey.Group),
|
||||
ModelMapping: c.GetStringMapString(ctxkey.ModelMapping),
|
||||
OriginModelName: c.GetString(ctxkey.RequestModel),
|
||||
BaseURL: c.GetString(ctxkey.BaseURL),
|
||||
APIKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
|
||||
RequestURLPath: c.Request.URL.String(),
|
||||
ForcedSystemPrompt: c.GetString(ctxkey.SystemPrompt),
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
cfg, ok := c.Get(ctxkey.Config)
|
||||
if ok {
|
||||
|
@ -26,6 +26,7 @@ type GeneralOpenAIRequest struct {
|
||||
Messages []Message `json:"messages,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Store *bool `json:"store,omitempty"`
|
||||
ReasoningEffort *string `json:"reasoning_effort,omitempty"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"`
|
||||
LogitBias any `json:"logit_bias,omitempty"`
|
||||
|
@ -1,11 +1,12 @@
|
||||
package model
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role,omitempty"`
|
||||
Content any `json:"content,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
ToolCalls []Tool `json:"tool_calls,omitempty"`
|
||||
ToolCallId string `json:"tool_call_id,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
Content any `json:"content,omitempty"`
|
||||
ReasoningContent any `json:"reasoning_content,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
ToolCalls []Tool `json:"tool_calls,omitempty"`
|
||||
ToolCallId string `json:"tool_call_id,omitempty"`
|
||||
}
|
||||
|
||||
func (m Message) IsStringContent() bool {
|
||||
|
@ -4,6 +4,14 @@ type Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
|
||||
CompletionTokensDetails *CompletionTokensDetails `json:"completion_tokens_details,omitempty"`
|
||||
}
|
||||
|
||||
type CompletionTokensDetails struct {
|
||||
ReasoningTokens int `json:"reasoning_tokens"`
|
||||
AcceptedPredictionTokens int `json:"accepted_prediction_tokens"`
|
||||
RejectedPredictionTokens int `json:"rejected_prediction_tokens"`
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
|
@ -7,7 +7,7 @@ export const CHANNEL_OPTIONS = [
|
||||
{ key: 24, text: 'Google Gemini', value: 24, color: 'orange' },
|
||||
{ key: 28, text: 'Mistral AI', value: 28, color: 'orange' },
|
||||
{ key: 41, text: 'Novita', value: 41, color: 'purple' },
|
||||
{ key: 40, text: '字节跳动豆包', value: 40, color: 'blue' },
|
||||
{key: 40, text: '字节火山引擎', value: 40, color: 'blue'},
|
||||
{ key: 15, text: '百度文心千帆', value: 15, color: 'blue' },
|
||||
{ key: 17, text: '阿里通义千问', value: 17, color: 'orange' },
|
||||
{ key: 18, text: '讯飞星火认知', value: 18, color: 'blue' },
|
||||
@ -35,7 +35,7 @@ export const CHANNEL_OPTIONS = [
|
||||
{ key: 8, text: '自定义渠道', value: 8, color: 'pink' },
|
||||
{ key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' },
|
||||
{ key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' },
|
||||
{ key: 20, text: '代理:OpenRouter', value: 20, color: 'black' },
|
||||
{key: 20, text: 'OpenRouter', value: 20, color: 'black'},
|
||||
{ key: 2, text: '代理:API2D', value: 2, color: 'blue' },
|
||||
{ key: 5, text: '代理:OpenAI-SB', value: 5, color: 'brown' },
|
||||
{ key: 7, text: '代理:OhMyGPT', value: 7, color: 'purple' },
|
||||
|
@ -49,7 +49,7 @@ export const CHANNEL_OPTIONS = {
|
||||
},
|
||||
40: {
|
||||
key: 40,
|
||||
text: '字节跳动豆包',
|
||||
text: '字节火山引擎',
|
||||
value: 40,
|
||||
color: 'primary'
|
||||
},
|
||||
@ -217,7 +217,7 @@ export const CHANNEL_OPTIONS = {
|
||||
},
|
||||
20: {
|
||||
key: 20,
|
||||
text: '代理:OpenRouter',
|
||||
text: 'OpenRouter',
|
||||
value: 20,
|
||||
color: 'success'
|
||||
},
|
||||
|
@ -5,10 +5,14 @@
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"history": "^5.3.0",
|
||||
"i18next": "^24.2.2",
|
||||
"i18next-browser-languagedetector": "^8.0.2",
|
||||
"marked": "^4.1.1",
|
||||
"moment": "^2.30.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",
|
||||
|
@ -42,32 +42,37 @@ function App() {
|
||||
}
|
||||
};
|
||||
const loadStatus = async () => {
|
||||
const res = await API.get('/api/status');
|
||||
const { success, data } = res.data;
|
||||
if (success) {
|
||||
localStorage.setItem('status', JSON.stringify(data));
|
||||
statusDispatch({ type: 'set', payload: data });
|
||||
localStorage.setItem('system_name', data.system_name);
|
||||
localStorage.setItem('logo', data.logo);
|
||||
localStorage.setItem('footer_html', data.footer_html);
|
||||
localStorage.setItem('quota_per_unit', data.quota_per_unit);
|
||||
localStorage.setItem('display_in_currency', data.display_in_currency);
|
||||
if (data.chat_link) {
|
||||
localStorage.setItem('chat_link', data.chat_link);
|
||||
try {
|
||||
const res = await API.get('/api/status');
|
||||
const { success, message, data } = res.data || {}; // Add default empty object
|
||||
if (success && data) {
|
||||
// Check data exists
|
||||
localStorage.setItem('status', JSON.stringify(data));
|
||||
statusDispatch({ type: 'set', payload: data });
|
||||
localStorage.setItem('system_name', data.system_name);
|
||||
localStorage.setItem('logo', data.logo);
|
||||
localStorage.setItem('footer_html', data.footer_html);
|
||||
localStorage.setItem('quota_per_unit', data.quota_per_unit);
|
||||
localStorage.setItem('display_in_currency', data.display_in_currency);
|
||||
if (data.chat_link) {
|
||||
localStorage.setItem('chat_link', data.chat_link);
|
||||
} else {
|
||||
localStorage.removeItem('chat_link');
|
||||
}
|
||||
if (
|
||||
data.version !== process.env.REACT_APP_VERSION &&
|
||||
data.version !== 'v0.0.0' &&
|
||||
process.env.REACT_APP_VERSION !== ''
|
||||
) {
|
||||
showNotice(
|
||||
`新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('chat_link');
|
||||
showError(message || '无法正常连接至服务器!');
|
||||
}
|
||||
if (
|
||||
data.version !== process.env.REACT_APP_VERSION &&
|
||||
data.version !== 'v0.0.0' &&
|
||||
process.env.REACT_APP_VERSION !== ''
|
||||
) {
|
||||
showNotice(
|
||||
`新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showError('无法正常连接至服务器!');
|
||||
} catch (error) {
|
||||
showError(error.message || '无法正常连接至服务器!');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,16 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Form,
|
||||
Input,
|
||||
Label,
|
||||
Message,
|
||||
Pagination,
|
||||
Popup,
|
||||
Table,
|
||||
} from 'semantic-ui-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {Button, Dropdown, Form, Input, Label, Message, Pagination, Popup, Table,} from 'semantic-ui-react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {
|
||||
API,
|
||||
loadChannelModels,
|
||||
@ -22,8 +13,8 @@ import {
|
||||
timestamp2string,
|
||||
} from '../helpers';
|
||||
|
||||
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderGroup, renderNumber } from '../helpers/render';
|
||||
import {CHANNEL_OPTIONS, ITEMS_PER_PAGE} from '../constants';
|
||||
import {renderGroup, renderNumber} from '../helpers/render';
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return <>{timestamp2string(timestamp)}</>;
|
||||
@ -31,13 +22,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,9 +41,12 @@ function renderType(type) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderBalance(type, balance) {
|
||||
function renderBalance(type, balance, t) {
|
||||
switch (type) {
|
||||
case 1: // OpenAI
|
||||
if (balance === 0) {
|
||||
return <span>{t('channel.table.balance_not_supported')}</span>;
|
||||
}
|
||||
return <span>${balance.toFixed(2)}</span>;
|
||||
case 4: // CloseAI
|
||||
return <span>¥{balance.toFixed(2)}</span>;
|
||||
@ -62,12 +60,14 @@ function renderBalance(type, balance) {
|
||||
return <span>¥{balance.toFixed(2)}</span>;
|
||||
case 13: // AIGC2D
|
||||
return <span>{renderNumber(balance)}</span>;
|
||||
case 20: // OpenRouter
|
||||
return <span>${balance.toFixed(2)}</span>;
|
||||
case 36: // DeepSeek
|
||||
return <span>¥{balance.toFixed(2)}</span>;
|
||||
case 44: // SiliconFlow
|
||||
return <span>¥{balance.toFixed(2)}</span>;
|
||||
default:
|
||||
return <span>不支持</span>;
|
||||
return <span>{t('channel.table.balance_not_supported')}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,6 +78,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);
|
||||
@ -87,30 +88,32 @@ const ChannelsTable = () => {
|
||||
const [showPrompt, setShowPrompt] = useState(shouldShowPrompt(promptID));
|
||||
const [showDetail, setShowDetail] = useState(isShowDetail());
|
||||
|
||||
const processChannelData = (channel) => {
|
||||
if (channel.models === '') {
|
||||
channel.models = [];
|
||||
channel.test_model = '';
|
||||
} else {
|
||||
channel.models = channel.models.split(',');
|
||||
if (channel.models.length > 0) {
|
||||
channel.test_model = channel.models[0];
|
||||
}
|
||||
channel.model_options = channel.models.map((model) => {
|
||||
return {
|
||||
key: model,
|
||||
text: model,
|
||||
value: model,
|
||||
};
|
||||
});
|
||||
console.log('channel', channel);
|
||||
}
|
||||
return channel;
|
||||
};
|
||||
|
||||
const loadChannels = async (startIdx) => {
|
||||
const res = await API.get(`/api/channel/?p=${startIdx}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let localChannels = data.map((channel) => {
|
||||
if (channel.models === '') {
|
||||
channel.models = [];
|
||||
channel.test_model = '';
|
||||
} else {
|
||||
channel.models = channel.models.split(',');
|
||||
if (channel.models.length > 0) {
|
||||
channel.test_model = channel.models[0];
|
||||
}
|
||||
channel.model_options = channel.models.map((model) => {
|
||||
return {
|
||||
key: model,
|
||||
text: model,
|
||||
value: model,
|
||||
};
|
||||
});
|
||||
console.log('channel', channel);
|
||||
}
|
||||
return channel;
|
||||
});
|
||||
let localChannels = data.map(processChannelData);
|
||||
if (startIdx === 0) {
|
||||
setChannels(localChannels);
|
||||
} else {
|
||||
@ -192,7 +195,7 @@ const ChannelsTable = () => {
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('操作成功完成!');
|
||||
showSuccess(t('channel.messages.operation_success'));
|
||||
let channel = res.data.data;
|
||||
let newChannels = [...channels];
|
||||
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
|
||||
@ -207,12 +210,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 +223,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 +235,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) {
|
||||
@ -295,7 +298,8 @@ const ChannelsTable = () => {
|
||||
const res = await API.get(`/api/channel/search?keyword=${searchKeyword}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setChannels(data);
|
||||
let localChannels = data.map(processChannelData);
|
||||
setChannels(localChannels);
|
||||
setActivePage(1);
|
||||
} else {
|
||||
showError(message);
|
||||
@ -319,10 +323,8 @@ const ChannelsTable = () => {
|
||||
newChannels[realIdx].response_time = time * 1000;
|
||||
newChannels[realIdx].test_time = Date.now() / 1000;
|
||||
setChannels(newChannels);
|
||||
showInfo(
|
||||
`渠道 ${name} 测试成功,模型 ${model},耗时 ${time.toFixed(
|
||||
2
|
||||
)} 秒,模型输出:${message}`
|
||||
showSuccess(
|
||||
t('channel.messages.test_success', { name, model, time, message })
|
||||
);
|
||||
} else {
|
||||
showError(message);
|
||||
@ -338,7 +340,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 +350,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 +368,7 @@ const ChannelsTable = () => {
|
||||
newChannels[realIdx].balance = balance;
|
||||
newChannels[realIdx].balance_updated_time = Date.now() / 1000;
|
||||
setChannels(newChannels);
|
||||
showInfo(`渠道 ${name} 余额更新成功!`);
|
||||
showSuccess(t('channel.messages.balance_update_success', { name }));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@ -375,7 +379,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 +417,7 @@ const ChannelsTable = () => {
|
||||
icon='search'
|
||||
fluid
|
||||
iconPosition='left'
|
||||
placeholder='搜索渠道的 ID,名称和密钥 ...'
|
||||
placeholder={t('channel.search')}
|
||||
value={searchKeyword}
|
||||
loading={searching}
|
||||
onChange={handleKeywordChange}
|
||||
@ -426,13 +430,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 +446,7 @@ const ChannelsTable = () => {
|
||||
sortChannel('id');
|
||||
}}
|
||||
>
|
||||
ID
|
||||
{t('channel.table.id')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -452,7 +454,7 @@ const ChannelsTable = () => {
|
||||
sortChannel('name');
|
||||
}}
|
||||
>
|
||||
名称
|
||||
{t('channel.table.name')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -460,7 +462,7 @@ const ChannelsTable = () => {
|
||||
sortChannel('group');
|
||||
}}
|
||||
>
|
||||
分组
|
||||
{t('channel.table.group')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -468,7 +470,7 @@ const ChannelsTable = () => {
|
||||
sortChannel('type');
|
||||
}}
|
||||
>
|
||||
类型
|
||||
{t('channel.table.type')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -476,7 +478,7 @@ const ChannelsTable = () => {
|
||||
sortChannel('status');
|
||||
}}
|
||||
>
|
||||
状态
|
||||
{t('channel.table.status')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -484,27 +486,29 @@ const ChannelsTable = () => {
|
||||
sortChannel('response_time');
|
||||
}}
|
||||
>
|
||||
响应时间
|
||||
{t('channel.table.response_time')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortChannel('balance');
|
||||
}}
|
||||
hidden={!showDetail}
|
||||
>
|
||||
余额
|
||||
{t('channel.table.balance')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortChannel('priority');
|
||||
}}
|
||||
hidden={!showDetail}
|
||||
>
|
||||
优先级
|
||||
{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,23 +523,25 @@ 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>
|
||||
<Table.Cell hidden={!showDetail}>
|
||||
<Table.Cell>
|
||||
<Popup
|
||||
trigger={
|
||||
<span
|
||||
@ -544,14 +550,14 @@ 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>
|
||||
<Table.Cell>
|
||||
<Table.Cell hidden={!showDetail}>
|
||||
<Popup
|
||||
trigger={
|
||||
<Input
|
||||
@ -569,13 +575,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}
|
||||
@ -585,9 +591,17 @@ const ChannelsTable = () => {
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: '2px',
|
||||
rowGap: '6px',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size={'small'}
|
||||
size={'tiny'}
|
||||
positive
|
||||
onClick={() => {
|
||||
testChannel(
|
||||
@ -598,22 +612,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>
|
||||
删除
|
||||
<Button size='tiny' negative>
|
||||
{t('channel.buttons.delete')}
|
||||
</Button>
|
||||
}
|
||||
on='click'
|
||||
@ -621,16 +625,17 @@ const ChannelsTable = () => {
|
||||
hoverable
|
||||
>
|
||||
<Button
|
||||
size={'tiny'}
|
||||
negative
|
||||
onClick={() => {
|
||||
manageChannel(channel.id, 'delete', idx);
|
||||
}}
|
||||
>
|
||||
删除渠道 {channel.name}
|
||||
{t('channel.buttons.confirm_delete')} {channel.name}
|
||||
</Button>
|
||||
</Popup>
|
||||
<Button
|
||||
size={'small'}
|
||||
size={'tiny'}
|
||||
onClick={() => {
|
||||
manageChannel(
|
||||
channel.id,
|
||||
@ -639,14 +644,16 @@ const ChannelsTable = () => {
|
||||
);
|
||||
}}
|
||||
>
|
||||
{channel.status === 1 ? '禁用' : '启用'}
|
||||
{channel.status === 1
|
||||
? t('channel.buttons.disable')
|
||||
: t('channel.buttons.enable')}
|
||||
</Button>
|
||||
<Button
|
||||
size={'small'}
|
||||
size={'tiny'}
|
||||
as={Link}
|
||||
to={'/channel/edit/' + channel.id}
|
||||
>
|
||||
编辑
|
||||
{t('channel.buttons.edit')}
|
||||
</Button>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
@ -658,38 +665,31 @@ const ChannelsTable = () => {
|
||||
<Table.Footer>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell colSpan={showDetail ? '10' : '8'}>
|
||||
<Button
|
||||
size='small'
|
||||
as={Link}
|
||||
to='/channel/add'
|
||||
loading={loading}
|
||||
>
|
||||
添加新的渠道
|
||||
<Button size='tiny' as={Link} to='/channel/add' loading={loading}>
|
||||
{t('channel.buttons.add')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
size='tiny'
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
testChannels('all');
|
||||
}}
|
||||
>
|
||||
测试所有渠道
|
||||
{t('channel.buttons.test_all')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
size='tiny'
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
testChannels('disabled');
|
||||
}}
|
||||
>
|
||||
测试禁用渠道
|
||||
{t('channel.buttons.test_disabled')}
|
||||
</Button>
|
||||
{/*<Button size='small' onClick={updateAllChannelsBalance}*/}
|
||||
{/* loading={loading || updatingBalance}>更新已启用渠道余额</Button>*/}
|
||||
<Popup
|
||||
trigger={
|
||||
<Button size='small' loading={loading}>
|
||||
删除禁用渠道
|
||||
<Button size='tiny' loading={loading}>
|
||||
{t('channel.buttons.delete_disabled')}
|
||||
</Button>
|
||||
}
|
||||
on='click'
|
||||
@ -697,30 +697,32 @@ const ChannelsTable = () => {
|
||||
hoverable
|
||||
>
|
||||
<Button
|
||||
size='small'
|
||||
size='tiny'
|
||||
loading={loading}
|
||||
negative
|
||||
onClick={deleteAllDisabledChannels}
|
||||
>
|
||||
确认删除
|
||||
{t('channel.buttons.confirm_delete_disabled')}
|
||||
</Button>
|
||||
</Popup>
|
||||
<Pagination
|
||||
floated='right'
|
||||
activePage={activePage}
|
||||
onPageChange={onPaginationChange}
|
||||
size='small'
|
||||
size='tiny'
|
||||
siblingRange={1}
|
||||
totalPages={
|
||||
Math.ceil(channels.length / ITEMS_PER_PAGE) +
|
||||
(channels.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
|
||||
}
|
||||
/>
|
||||
<Button size='small' onClick={refresh} loading={loading}>
|
||||
刷新
|
||||
<Button size='tiny' onClick={refresh} loading={loading}>
|
||||
{t('channel.buttons.refresh')}
|
||||
</Button>
|
||||
<Button size='small' onClick={toggleShowDetail}>
|
||||
{showDetail ? '隐藏详情' : '详情'}
|
||||
<Button size='tiny' onClick={toggleShowDetail}>
|
||||
{showDetail
|
||||
? t('channel.buttons.hide_detail')
|
||||
: t('channel.buttons.show_detail')}
|
||||
</Button>
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Container, Segment } from 'semantic-ui-react';
|
||||
import { getFooterHTML, getSystemName } from '../helpers';
|
||||
|
||||
const Footer = () => {
|
||||
const { t } = useTranslation();
|
||||
const systemName = getSystemName();
|
||||
const [footer, setFooter] = useState(getFooterHTML());
|
||||
let remainCheckTimes = 5;
|
||||
@ -40,13 +41,13 @@ const Footer = () => {
|
||||
<a href='https://github.com/songquanpeng/one-api' target='_blank'>
|
||||
{systemName} {process.env.REACT_APP_VERSION}{' '}
|
||||
</a>
|
||||
由{' '}
|
||||
{t('footer.built_by')}{' '}
|
||||
<a href='https://github.com/songquanpeng' target='_blank'>
|
||||
JustSong
|
||||
{t('footer.built_by_name')}
|
||||
</a>{' '}
|
||||
构建,源代码遵循{' '}
|
||||
{t('footer.license')}{' '}
|
||||
<a href='https://opensource.org/licenses/mit-license.php'>
|
||||
MIT 协议
|
||||
{t('footer.mit')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
@ -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,50 @@ import '../index.css';
|
||||
// Header Buttons
|
||||
let headerButtons = [
|
||||
{
|
||||
name: '首页',
|
||||
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 +75,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 +109,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 +132,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 (
|
||||
<>
|
||||
@ -157,7 +165,13 @@ const Header = () => {
|
||||
: { borderTop: 'none', height: '52px' }
|
||||
}
|
||||
>
|
||||
<Container>
|
||||
<Container
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: isMobile() ? '100%' : '1200px',
|
||||
padding: isMobile() ? '0 10px' : '0 20px',
|
||||
}}
|
||||
>
|
||||
<Menu.Item as={Link} to='/'>
|
||||
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
|
||||
<div style={{ fontSize: '20px' }}>
|
||||
@ -175,10 +189,24 @@ const Header = () => {
|
||||
<Segment style={{ marginTop: 0, borderTop: '0' }}>
|
||||
<Menu secondary vertical style={{ width: '100%', margin: 0 }}>
|
||||
{renderButtons(true)}
|
||||
<Menu.Item>
|
||||
<Dropdown
|
||||
selection
|
||||
trigger={
|
||||
<Icon
|
||||
name='language'
|
||||
style={{ margin: 0, fontSize: '18px' }}
|
||||
/>
|
||||
}
|
||||
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 +216,7 @@ const Header = () => {
|
||||
navigate('/login');
|
||||
}}
|
||||
>
|
||||
登录
|
||||
{t('header.login')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
@ -196,7 +224,7 @@ const Header = () => {
|
||||
navigate('/register');
|
||||
}}
|
||||
>
|
||||
注册
|
||||
{t('header.register')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@ -220,7 +248,13 @@ const Header = () => {
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
<Container
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: isMobile() ? '100%' : '1200px',
|
||||
padding: isMobile() ? '0 10px' : '0 20px',
|
||||
}}
|
||||
>
|
||||
<Menu.Item as={Link} to='/' className={'hide-on-mobile'}>
|
||||
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
|
||||
<div
|
||||
@ -235,6 +269,21 @@ const Header = () => {
|
||||
</Menu.Item>
|
||||
{renderButtons(false)}
|
||||
<Menu.Menu position='right'>
|
||||
<Dropdown
|
||||
item
|
||||
trigger={
|
||||
<Icon name='language' style={{ margin: 0, fontSize: '18px' }} />
|
||||
}
|
||||
options={languageOptions}
|
||||
value={i18n.language}
|
||||
onChange={(_, { value }) => changeLanguage(value)}
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
fontWeight: '400',
|
||||
color: '#666',
|
||||
padding: '0 10px',
|
||||
}}
|
||||
/>
|
||||
{userState.user ? (
|
||||
<Dropdown
|
||||
text={userState.user.username}
|
||||
@ -255,13 +304,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'
|
||||
|
@ -12,12 +12,14 @@ import {
|
||||
Card,
|
||||
} from 'semantic-ui-react';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UserContext } from '../context/User';
|
||||
import { API, getLogo, showError, showSuccess, showWarning } from '../helpers';
|
||||
import { onGitHubOAuthClicked, onLarkOAuthClicked } from './utils';
|
||||
import larkIcon from '../images/lark.svg';
|
||||
|
||||
const LoginForm = () => {
|
||||
const { t } = useTranslation();
|
||||
const [inputs, setInputs] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
@ -33,7 +35,7 @@ const LoginForm = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get('expired')) {
|
||||
showError('未登录或登录已过期,请重新登录!');
|
||||
showError(t('messages.error.login_expired'));
|
||||
}
|
||||
let status = localStorage.getItem('status');
|
||||
if (status) {
|
||||
@ -57,7 +59,7 @@ const LoginForm = () => {
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
localStorage.setItem('user', JSON.stringify(data));
|
||||
navigate('/');
|
||||
showSuccess('登录成功!');
|
||||
showSuccess(t('messages.success.login'));
|
||||
setShowWeChatLoginModal(false);
|
||||
} else {
|
||||
showError(message);
|
||||
@ -82,11 +84,11 @@ const LoginForm = () => {
|
||||
localStorage.setItem('user', JSON.stringify(data));
|
||||
if (username === 'root' && password === '123456') {
|
||||
navigate('/user/edit');
|
||||
showSuccess('登录成功!');
|
||||
showWarning('请立刻修改默认密码!');
|
||||
showSuccess(t('messages.success.login'));
|
||||
showWarning(t('messages.error.root_password'));
|
||||
} else {
|
||||
navigate('/token');
|
||||
showSuccess('登录成功!');
|
||||
showSuccess(t('messages.success.login'));
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
@ -110,7 +112,7 @@ const LoginForm = () => {
|
||||
style={{ marginBottom: '1.5em' }}
|
||||
>
|
||||
<Image src={logo} style={{ marginBottom: '10px' }} />
|
||||
<Header.Content>用户登录</Header.Content>
|
||||
<Header.Content>{t('auth.login.title')}</Header.Content>
|
||||
</Header>
|
||||
</Card.Header>
|
||||
<Form size='large'>
|
||||
@ -118,7 +120,7 @@ const LoginForm = () => {
|
||||
fluid
|
||||
icon='user'
|
||||
iconPosition='left'
|
||||
placeholder='用户名 / 邮箱地址'
|
||||
placeholder={t('auth.login.username')}
|
||||
name='username'
|
||||
value={username}
|
||||
onChange={handleChange}
|
||||
@ -128,7 +130,7 @@ const LoginForm = () => {
|
||||
fluid
|
||||
icon='lock'
|
||||
iconPosition='left'
|
||||
placeholder='密码'
|
||||
placeholder={t('auth.login.password')}
|
||||
name='password'
|
||||
type='password'
|
||||
value={password}
|
||||
@ -145,7 +147,7 @@ const LoginForm = () => {
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
登录
|
||||
{t('auth.login.button')}
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
@ -160,15 +162,21 @@ const LoginForm = () => {
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
忘记密码?
|
||||
<Link to='/reset' style={{ color: '#2185d0' }}>
|
||||
点击重置
|
||||
{t('auth.login.forgot_password')}
|
||||
<Link
|
||||
to='/reset'
|
||||
style={{ color: '#2185d0', marginLeft: '2px' }}
|
||||
>
|
||||
{t('auth.login.reset_password')}
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
没有账户?
|
||||
<Link to='/register' style={{ color: '#2185d0' }}>
|
||||
点击注册
|
||||
{t('auth.login.no_account')}
|
||||
<Link
|
||||
to='/register'
|
||||
style={{ color: '#2185d0', marginLeft: '2px' }}
|
||||
>
|
||||
{t('auth.login.register')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@ -182,7 +190,7 @@ const LoginForm = () => {
|
||||
horizontal
|
||||
style={{ color: '#666', fontSize: '0.9em' }}
|
||||
>
|
||||
使用其他方式登录
|
||||
{t('auth.login.other_methods')}
|
||||
</Divider>
|
||||
<div
|
||||
style={{
|
||||
@ -240,6 +248,42 @@ 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>{t('auth.login.wechat.scan_tip')}</p>
|
||||
</div>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder={t('auth.login.wechat.code_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}
|
||||
>
|
||||
{t('auth.login.button')}
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
);
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
Segment,
|
||||
Select,
|
||||
Table,
|
||||
Popup,
|
||||
} from 'semantic-ui-react';
|
||||
import {
|
||||
API,
|
||||
@ -18,6 +19,7 @@ import {
|
||||
showWarning,
|
||||
timestamp2string,
|
||||
} from '../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderColorLabel, renderQuota } from '../helpers/render';
|
||||
@ -45,15 +47,6 @@ const MODE_OPTIONS = [
|
||||
{ key: 'self', text: '当前用户', value: 'self' },
|
||||
];
|
||||
|
||||
const LOG_OPTIONS = [
|
||||
{ key: '0', text: '全部', value: 0 },
|
||||
{ key: '1', text: '充值', value: 1 },
|
||||
{ key: '2', text: '消费', value: 2 },
|
||||
{ key: '3', text: '管理', value: 3 },
|
||||
{ key: '4', text: '系统', value: 4 },
|
||||
{ key: '5', text: '测试', value: 5 },
|
||||
];
|
||||
|
||||
function renderType(type) {
|
||||
switch (type) {
|
||||
case 1:
|
||||
@ -137,6 +130,7 @@ function renderDetail(log) {
|
||||
}
|
||||
|
||||
const LogsTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [showStat, setShowStat] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@ -168,6 +162,15 @@ const LogsTable = () => {
|
||||
token: 0,
|
||||
});
|
||||
|
||||
const LOG_OPTIONS = [
|
||||
{ key: '0', text: t('log.type.all'), value: 0 },
|
||||
{ key: '1', text: t('log.type.topup'), value: 1 },
|
||||
{ key: '2', text: t('log.type.usage'), value: 2 },
|
||||
{ key: '3', text: t('log.type.admin'), value: 3 },
|
||||
{ key: '4', text: t('log.type.system'), value: 4 },
|
||||
{ key: '5', text: t('log.type.test'), value: 5 },
|
||||
];
|
||||
|
||||
const handleInputChange = (e, { name, value }) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
@ -307,296 +310,302 @@ const LogsTable = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
<Header as='h3'>
|
||||
使用明细(总消耗额度:
|
||||
{showStat && renderQuota(stat.quota)}
|
||||
{!showStat && (
|
||||
<span
|
||||
onClick={handleEyeClick}
|
||||
style={{ cursor: 'pointer', color: 'gray' }}
|
||||
>
|
||||
点击查看
|
||||
</span>
|
||||
)}
|
||||
)
|
||||
</Header>
|
||||
<Form>
|
||||
<Form.Group>
|
||||
<Form.Input
|
||||
fluid
|
||||
label={'令牌名称'}
|
||||
width={3}
|
||||
value={token_name}
|
||||
placeholder={'可选值'}
|
||||
name='token_name'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Input
|
||||
fluid
|
||||
label='模型名称'
|
||||
width={3}
|
||||
value={model_name}
|
||||
placeholder='可选值'
|
||||
name='model_name'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Input
|
||||
fluid
|
||||
label='起始时间'
|
||||
width={4}
|
||||
value={start_timestamp}
|
||||
type='datetime-local'
|
||||
name='start_timestamp'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Input
|
||||
fluid
|
||||
label='结束时间'
|
||||
width={4}
|
||||
value={end_timestamp}
|
||||
type='datetime-local'
|
||||
name='end_timestamp'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Button fluid label='操作' width={2} onClick={refresh}>
|
||||
查询
|
||||
</Form.Button>
|
||||
</Form.Group>
|
||||
{isAdminUser && (
|
||||
<>
|
||||
<Form.Group>
|
||||
<Form.Input
|
||||
fluid
|
||||
label={'渠道 ID'}
|
||||
width={3}
|
||||
value={channel}
|
||||
placeholder='可选值'
|
||||
name='channel'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Input
|
||||
fluid
|
||||
label={'用户名称'}
|
||||
width={3}
|
||||
value={username}
|
||||
placeholder={'可选值'}
|
||||
name='username'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
<Table basic={'very'} compact size='small'>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('created_time');
|
||||
}}
|
||||
<Header as='h3'>
|
||||
{t('log.usage_details')}({t('log.total_quota')}:
|
||||
{showStat && renderQuota(stat.quota, t)}
|
||||
{!showStat && (
|
||||
<span
|
||||
onClick={handleEyeClick}
|
||||
style={{ cursor: 'pointer', color: 'gray' }}
|
||||
>
|
||||
{t('log.click_to_view')}
|
||||
</span>
|
||||
)}
|
||||
)
|
||||
</Header>
|
||||
<Form>
|
||||
<Form.Group>
|
||||
<Form.Input
|
||||
fluid
|
||||
label={t('log.table.token_name')}
|
||||
size={'small'}
|
||||
width={3}
|
||||
value={token_name}
|
||||
placeholder={t('log.table.token_name_placeholder')}
|
||||
name='token_name'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Input
|
||||
fluid
|
||||
label={t('log.table.model_name')}
|
||||
size={'small'}
|
||||
width={3}
|
||||
value={model_name}
|
||||
placeholder={t('log.table.model_name_placeholder')}
|
||||
name='model_name'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Input
|
||||
fluid
|
||||
label={t('log.table.start_time')}
|
||||
size={'small'}
|
||||
width={4}
|
||||
value={start_timestamp}
|
||||
type='datetime-local'
|
||||
name='start_timestamp'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Input
|
||||
fluid
|
||||
label={t('log.table.end_time')}
|
||||
size={'small'}
|
||||
width={4}
|
||||
value={end_timestamp}
|
||||
type='datetime-local'
|
||||
name='end_timestamp'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Button
|
||||
fluid
|
||||
label={t('log.buttons.query')}
|
||||
size={'small'}
|
||||
width={2}
|
||||
onClick={refresh}
|
||||
>
|
||||
{t('log.buttons.submit')}
|
||||
</Form.Button>
|
||||
</Form.Group>
|
||||
{isAdminUser && (
|
||||
<>
|
||||
<Form.Group>
|
||||
<Form.Input
|
||||
fluid
|
||||
label={t('log.table.channel_id')}
|
||||
size={'small'}
|
||||
width={3}
|
||||
>
|
||||
时间
|
||||
</Table.HeaderCell>
|
||||
{isAdminUser && (
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('channel');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
渠道
|
||||
</Table.HeaderCell>
|
||||
)}
|
||||
value={channel}
|
||||
placeholder={t('log.table.channel_id_placeholder')}
|
||||
name='channel'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Input
|
||||
fluid
|
||||
label={t('log.table.username')}
|
||||
size={'small'}
|
||||
width={3}
|
||||
value={username}
|
||||
placeholder={t('log.table.username_placeholder')}
|
||||
name='username'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
</>
|
||||
)}
|
||||
<Form.Input
|
||||
icon='search'
|
||||
placeholder={t('log.search')}
|
||||
value={searchKeyword}
|
||||
onChange={(e, { value }) => setSearchKeyword(value)}
|
||||
/>
|
||||
</Form>
|
||||
<Table basic={'very'} compact size='small'>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('created_time');
|
||||
}}
|
||||
width={3}
|
||||
>
|
||||
{t('log.table.time')}
|
||||
</Table.HeaderCell>
|
||||
{isAdminUser && (
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('type');
|
||||
sortLog('channel');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
类型
|
||||
{t('log.table.channel')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('model_name');
|
||||
}}
|
||||
width={2}
|
||||
>
|
||||
模型
|
||||
</Table.HeaderCell>
|
||||
{showUserTokenQuota() && (
|
||||
<>
|
||||
{isAdminUser && (
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('username');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
用户
|
||||
</Table.HeaderCell>
|
||||
)}
|
||||
)}
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('type');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
{t('log.table.type')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('model_name');
|
||||
}}
|
||||
width={2}
|
||||
>
|
||||
{t('log.table.model')}
|
||||
</Table.HeaderCell>
|
||||
{showUserTokenQuota() && (
|
||||
<>
|
||||
{isAdminUser && (
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('token_name');
|
||||
sortLog('username');
|
||||
}}
|
||||
width={1}
|
||||
width={2}
|
||||
>
|
||||
令牌
|
||||
{t('log.table.username')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('prompt_tokens');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
提示
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('completion_tokens');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
补全
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('quota');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
额度
|
||||
</Table.HeaderCell>
|
||||
</>
|
||||
)}
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('content');
|
||||
}}
|
||||
width={isAdminUser ? 4 : 6}
|
||||
>
|
||||
详情
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{logs
|
||||
.slice(
|
||||
(activePage - 1) * ITEMS_PER_PAGE,
|
||||
activePage * ITEMS_PER_PAGE
|
||||
)
|
||||
.map((log, idx) => {
|
||||
if (log.deleted) return <></>;
|
||||
return (
|
||||
<Table.Row key={log.id}>
|
||||
<Table.Cell>
|
||||
{renderTimestamp(log.created_at, log.request_id)}
|
||||
</Table.Cell>
|
||||
{isAdminUser && (
|
||||
<Table.Cell>
|
||||
{log.channel ? (
|
||||
<Label
|
||||
basic
|
||||
as={Link}
|
||||
to={`/channel/edit/${log.channel}`}
|
||||
>
|
||||
{log.channel}
|
||||
</Label>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Table.Cell>
|
||||
)}
|
||||
<Table.Cell>{renderType(log.type)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{log.model_name ? renderColorLabel(log.model_name) : ''}
|
||||
</Table.Cell>
|
||||
{showUserTokenQuota() && (
|
||||
<>
|
||||
{isAdminUser && (
|
||||
<Table.Cell>
|
||||
{log.username ? (
|
||||
<Label
|
||||
basic
|
||||
as={Link}
|
||||
to={`/user/edit/${log.user_id}`}
|
||||
>
|
||||
{log.username}
|
||||
</Label>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Table.Cell>
|
||||
)}
|
||||
<Table.Cell>
|
||||
{log.token_name
|
||||
? renderColorLabel(log.token_name)
|
||||
: ''}
|
||||
</Table.Cell>
|
||||
|
||||
<Table.Cell>
|
||||
{log.prompt_tokens ? log.prompt_tokens : ''}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{log.completion_tokens ? log.completion_tokens : ''}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{log.quota ? renderQuota(log.quota, 6) : ''}
|
||||
</Table.Cell>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Table.Cell>{renderDetail(log)}</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
})}
|
||||
</Table.Body>
|
||||
|
||||
<Table.Footer>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell colSpan={'10'}>
|
||||
<Select
|
||||
placeholder='选择明细分类'
|
||||
options={LOG_OPTIONS}
|
||||
style={{ marginRight: '8px' }}
|
||||
name='logType'
|
||||
value={logType}
|
||||
onChange={(e, { name, value }) => {
|
||||
setLogType(value);
|
||||
)}
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('token_name');
|
||||
}}
|
||||
/>
|
||||
<Button size='small' onClick={refresh} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
<Pagination
|
||||
floated='right'
|
||||
activePage={activePage}
|
||||
onPageChange={onPaginationChange}
|
||||
size='small'
|
||||
siblingRange={1}
|
||||
totalPages={
|
||||
Math.ceil(logs.length / ITEMS_PER_PAGE) +
|
||||
(logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
|
||||
}
|
||||
/>
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Footer>
|
||||
</Table>
|
||||
</>
|
||||
width={2}
|
||||
>
|
||||
{t('log.table.token_name')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('prompt_tokens');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
{t('log.table.prompt_tokens')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('completion_tokens');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
{t('log.table.completion_tokens')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('quota');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
{t('log.table.quota')}
|
||||
</Table.HeaderCell>
|
||||
</>
|
||||
)}
|
||||
<Table.HeaderCell>{t('log.table.detail')}</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{logs
|
||||
.slice(
|
||||
(activePage - 1) * ITEMS_PER_PAGE,
|
||||
activePage * ITEMS_PER_PAGE
|
||||
)
|
||||
.map((log, idx) => {
|
||||
if (log.deleted) return <></>;
|
||||
return (
|
||||
<Table.Row key={log.id}>
|
||||
<Table.Cell>
|
||||
{renderTimestamp(log.created_at, log.request_id)}
|
||||
</Table.Cell>
|
||||
{isAdminUser && (
|
||||
<Table.Cell>
|
||||
{log.channel ? (
|
||||
<Label
|
||||
basic
|
||||
as={Link}
|
||||
to={`/channel/edit/${log.channel}`}
|
||||
>
|
||||
{log.channel}
|
||||
</Label>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Table.Cell>
|
||||
)}
|
||||
<Table.Cell>{renderType(log.type)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{log.model_name ? renderColorLabel(log.model_name) : ''}
|
||||
</Table.Cell>
|
||||
{showUserTokenQuota() && (
|
||||
<>
|
||||
{isAdminUser && (
|
||||
<Table.Cell>
|
||||
{log.username ? (
|
||||
<Label
|
||||
basic
|
||||
as={Link}
|
||||
to={`/user/edit/${log.user_id}`}
|
||||
>
|
||||
{log.username}
|
||||
</Label>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Table.Cell>
|
||||
)}
|
||||
<Table.Cell>
|
||||
{log.token_name ? renderColorLabel(log.token_name) : ''}
|
||||
</Table.Cell>
|
||||
|
||||
<Table.Cell>
|
||||
{log.prompt_tokens ? log.prompt_tokens : ''}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{log.completion_tokens ? log.completion_tokens : ''}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{log.quota ? renderQuota(log.quota, t, 6) : ''}
|
||||
</Table.Cell>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Table.Cell>{renderDetail(log)}</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
})}
|
||||
</Table.Body>
|
||||
|
||||
<Table.Footer>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell colSpan={'10'}>
|
||||
<Select
|
||||
placeholder={t('log.type.select')}
|
||||
options={LOG_OPTIONS}
|
||||
style={{ marginRight: '8px' }}
|
||||
name='logType'
|
||||
value={logType}
|
||||
onChange={(e, { name, value }) => {
|
||||
setLogType(value);
|
||||
}}
|
||||
/>
|
||||
<Button size='small' onClick={refresh} loading={loading}>
|
||||
{t('log.buttons.refresh')}
|
||||
</Button>
|
||||
<Pagination
|
||||
floated='right'
|
||||
activePage={activePage}
|
||||
onPageChange={onPaginationChange}
|
||||
size='small'
|
||||
siblingRange={1}
|
||||
totalPages={
|
||||
Math.ceil(logs.length / ITEMS_PER_PAGE) +
|
||||
(logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
|
||||
}
|
||||
/>
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Footer>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,16 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Divider, Form, Grid, Header } from 'semantic-ui-react';
|
||||
import { API, showError, showSuccess, timestamp2string, verifyJSON } from '../helpers';
|
||||
import {
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
verifyJSON,
|
||||
} from '../helpers';
|
||||
|
||||
const OperationSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
let now = new Date();
|
||||
let [inputs, setInputs] = useState({
|
||||
QuotaForNewUser: 0,
|
||||
@ -23,11 +31,13 @@ const OperationSetting = () => {
|
||||
DisplayInCurrencyEnabled: '',
|
||||
DisplayTokenStatEnabled: '',
|
||||
ApproximateTokenEnabled: '',
|
||||
RetryTimes: 0
|
||||
RetryTimes: 0,
|
||||
});
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
let [loading, setLoading] = useState(false);
|
||||
let [historyTimestamp, setHistoryTimestamp] = useState(timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600)); // a month ago
|
||||
let [historyTimestamp, setHistoryTimestamp] = useState(
|
||||
timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600)
|
||||
); // a month ago
|
||||
|
||||
const getOptions = async () => {
|
||||
const res = await API.get('/api/option/');
|
||||
@ -35,7 +45,11 @@ const OperationSetting = () => {
|
||||
if (success) {
|
||||
let newInputs = {};
|
||||
data.forEach((item) => {
|
||||
if (item.key === 'ModelRatio' || item.key === 'GroupRatio' || item.key === 'CompletionRatio') {
|
||||
if (
|
||||
item.key === 'ModelRatio' ||
|
||||
item.key === 'GroupRatio' ||
|
||||
item.key === 'CompletionRatio'
|
||||
) {
|
||||
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||
}
|
||||
if (item.value === '{}') {
|
||||
@ -61,7 +75,7 @@ const OperationSetting = () => {
|
||||
}
|
||||
const res = await API.put('/api/option/', {
|
||||
key,
|
||||
value
|
||||
value,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
@ -83,11 +97,22 @@ const OperationSetting = () => {
|
||||
const submitConfig = async (group) => {
|
||||
switch (group) {
|
||||
case 'monitor':
|
||||
if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) {
|
||||
await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold);
|
||||
if (
|
||||
originInputs['ChannelDisableThreshold'] !==
|
||||
inputs.ChannelDisableThreshold
|
||||
) {
|
||||
await updateOption(
|
||||
'ChannelDisableThreshold',
|
||||
inputs.ChannelDisableThreshold
|
||||
);
|
||||
}
|
||||
if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
|
||||
await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
|
||||
if (
|
||||
originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold
|
||||
) {
|
||||
await updateOption(
|
||||
'QuotaRemindThreshold',
|
||||
inputs.QuotaRemindThreshold
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'ratio':
|
||||
@ -146,7 +171,9 @@ const OperationSetting = () => {
|
||||
|
||||
const deleteHistoryLogs = async () => {
|
||||
console.log(inputs);
|
||||
const res = await API.delete(`/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`);
|
||||
const res = await API.delete(
|
||||
`/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`
|
||||
);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
showSuccess(`${data} 条日志已清理!`);
|
||||
@ -159,40 +186,218 @@ const OperationSetting = () => {
|
||||
<Grid columns={1}>
|
||||
<Grid.Column>
|
||||
<Form loading={loading}>
|
||||
<Header as='h3'>
|
||||
通用设置
|
||||
</Header>
|
||||
<Header as='h3'>{t('setting.operation.quota.title')}</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label={t('setting.operation.quota.new_user')}
|
||||
name='QuotaForNewUser'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaForNewUser}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder={t('setting.operation.quota.new_user_placeholder')}
|
||||
/>
|
||||
<Form.Input
|
||||
label={t('setting.operation.quota.pre_consume')}
|
||||
name='PreConsumedQuota'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.PreConsumedQuota}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder={t('setting.operation.quota.pre_consume_placeholder')}
|
||||
/>
|
||||
<Form.Input
|
||||
label={t('setting.operation.quota.inviter_reward')}
|
||||
name='QuotaForInviter'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaForInviter}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder={t(
|
||||
'setting.operation.quota.inviter_reward_placeholder'
|
||||
)}
|
||||
/>
|
||||
<Form.Input
|
||||
label={t('setting.operation.quota.invitee_reward')}
|
||||
name='QuotaForInvitee'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaForInvitee}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder={t(
|
||||
'setting.operation.quota.invitee_reward_placeholder'
|
||||
)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button
|
||||
onClick={() => {
|
||||
submitConfig('quota').then();
|
||||
}}
|
||||
>
|
||||
{t('setting.operation.quota.buttons.save')}
|
||||
</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>{t('setting.operation.ratio.title')}</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label={t('setting.operation.ratio.model.title')}
|
||||
name='ModelRatio'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autoComplete='new-password'
|
||||
value={inputs.ModelRatio}
|
||||
placeholder={t('setting.operation.ratio.model.placeholder')}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label={t('setting.operation.ratio.completion.title')}
|
||||
name='CompletionRatio'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autoComplete='new-password'
|
||||
value={inputs.CompletionRatio}
|
||||
placeholder={t('setting.operation.ratio.completion.placeholder')}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label={t('setting.operation.ratio.group.title')}
|
||||
name='GroupRatio'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autoComplete='new-password'
|
||||
value={inputs.GroupRatio}
|
||||
placeholder={t('setting.operation.ratio.group.placeholder')}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button
|
||||
onClick={() => {
|
||||
submitConfig('ratio').then();
|
||||
}}
|
||||
>
|
||||
{t('setting.operation.ratio.buttons.save')}
|
||||
</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>{t('setting.operation.log.title')}</Header>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.LogConsumeEnabled === 'true'}
|
||||
label={t('setting.operation.log.enable_consume')}
|
||||
name='LogConsumeEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths={4}>
|
||||
<Form.Input
|
||||
label='充值链接'
|
||||
label={t('setting.operation.log.target_time')}
|
||||
value={historyTimestamp}
|
||||
type='datetime-local'
|
||||
name='history_timestamp'
|
||||
onChange={(e, { name, value }) => {
|
||||
setHistoryTimestamp(value);
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button
|
||||
onClick={() => {
|
||||
deleteHistoryLogs().then();
|
||||
}}
|
||||
>
|
||||
{t('setting.operation.log.buttons.clean')}
|
||||
</Form.Button>
|
||||
|
||||
<Divider />
|
||||
<Header as='h3'>{t('setting.operation.monitor.title')}</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label={t('setting.operation.monitor.max_response_time')}
|
||||
name='ChannelDisableThreshold'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.ChannelDisableThreshold}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder={t(
|
||||
'setting.operation.monitor.max_response_time_placeholder'
|
||||
)}
|
||||
/>
|
||||
<Form.Input
|
||||
label={t('setting.operation.monitor.quota_reminder')}
|
||||
name='QuotaRemindThreshold'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaRemindThreshold}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder={t(
|
||||
'setting.operation.monitor.quota_reminder_placeholder'
|
||||
)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.AutomaticDisableChannelEnabled === 'true'}
|
||||
label={t('setting.operation.monitor.auto_disable')}
|
||||
name='AutomaticDisableChannelEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.AutomaticEnableChannelEnabled === 'true'}
|
||||
label={t('setting.operation.monitor.auto_enable')}
|
||||
name='AutomaticEnableChannelEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button
|
||||
onClick={() => {
|
||||
submitConfig('monitor').then();
|
||||
}}
|
||||
>
|
||||
{t('setting.operation.monitor.buttons.save')}
|
||||
</Form.Button>
|
||||
|
||||
<Divider />
|
||||
<Header as='h3'>{t('setting.operation.general.title')}</Header>
|
||||
<Form.Group widths={4}>
|
||||
<Form.Input
|
||||
label={t('setting.operation.general.topup_link')}
|
||||
name='TopUpLink'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.TopUpLink}
|
||||
type='link'
|
||||
placeholder='例如发卡网站的购买链接'
|
||||
placeholder={t(
|
||||
'setting.operation.general.topup_link_placeholder'
|
||||
)}
|
||||
/>
|
||||
<Form.Input
|
||||
label='聊天页面链接'
|
||||
label={t('setting.operation.general.chat_link')}
|
||||
name='ChatLink'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.ChatLink}
|
||||
type='link'
|
||||
placeholder='例如 ChatGPT Next Web 的部署地址'
|
||||
placeholder={t('setting.operation.general.chat_link_placeholder')}
|
||||
/>
|
||||
<Form.Input
|
||||
label='单位美元额度'
|
||||
label={t('setting.operation.general.quota_per_unit')}
|
||||
name='QuotaPerUnit'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaPerUnit}
|
||||
type='number'
|
||||
step='0.01'
|
||||
placeholder='一单位货币能兑换的额度'
|
||||
placeholder={t(
|
||||
'setting.operation.general.quota_per_unit_placeholder'
|
||||
)}
|
||||
/>
|
||||
<Form.Input
|
||||
label='失败重试次数'
|
||||
label={t('setting.operation.general.retry_times')}
|
||||
name='RetryTimes'
|
||||
type={'number'}
|
||||
step='1'
|
||||
@ -200,186 +405,38 @@ const OperationSetting = () => {
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.RetryTimes}
|
||||
placeholder='失败重试次数'
|
||||
placeholder={t(
|
||||
'setting.operation.general.retry_times_placeholder'
|
||||
)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.DisplayInCurrencyEnabled === 'true'}
|
||||
label='以货币形式显示额度'
|
||||
label={t('setting.operation.general.display_in_currency')}
|
||||
name='DisplayInCurrencyEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.DisplayTokenStatEnabled === 'true'}
|
||||
label='Billing 相关 API 显示令牌额度而非用户额度'
|
||||
label={t('setting.operation.general.display_token_stat')}
|
||||
name='DisplayTokenStatEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.ApproximateTokenEnabled === 'true'}
|
||||
label='使用近似的方式估算 token 数以减少计算量'
|
||||
label={t('setting.operation.general.approximate_token')}
|
||||
name='ApproximateTokenEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('general').then();
|
||||
}}>保存通用设置</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
日志设置
|
||||
</Header>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.LogConsumeEnabled === 'true'}
|
||||
label='启用额度消费日志记录'
|
||||
name='LogConsumeEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths={4}>
|
||||
<Form.Input label='目标时间' value={historyTimestamp} type='datetime-local'
|
||||
name='history_timestamp'
|
||||
onChange={(e, { name, value }) => {
|
||||
setHistoryTimestamp(value);
|
||||
}} />
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
deleteHistoryLogs().then();
|
||||
}}>清理历史日志</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
监控设置
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='最长响应时间'
|
||||
name='ChannelDisableThreshold'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.ChannelDisableThreshold}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='单位秒,当运行渠道全部测试时,超过此时间将自动禁用渠道'
|
||||
/>
|
||||
<Form.Input
|
||||
label='额度提醒阈值'
|
||||
name='QuotaRemindThreshold'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaRemindThreshold}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='低于此额度时将发送邮件提醒用户'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.AutomaticDisableChannelEnabled === 'true'}
|
||||
label='失败时自动禁用渠道'
|
||||
name='AutomaticDisableChannelEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.AutomaticEnableChannelEnabled === 'true'}
|
||||
label='成功时自动启用渠道'
|
||||
name='AutomaticEnableChannelEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('monitor').then();
|
||||
}}>保存监控设置</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
额度设置
|
||||
</Header>
|
||||
<Form.Group widths={4}>
|
||||
<Form.Input
|
||||
label='新用户初始额度'
|
||||
name='QuotaForNewUser'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaForNewUser}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='例如:100'
|
||||
/>
|
||||
<Form.Input
|
||||
label='请求预扣费额度'
|
||||
name='PreConsumedQuota'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.PreConsumedQuota}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='请求结束后多退少补'
|
||||
/>
|
||||
<Form.Input
|
||||
label='邀请新用户奖励额度'
|
||||
name='QuotaForInviter'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaForInviter}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='例如:2000'
|
||||
/>
|
||||
<Form.Input
|
||||
label='新用户使用邀请码奖励额度'
|
||||
name='QuotaForInvitee'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaForInvitee}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='例如:1000'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('quota').then();
|
||||
}}>保存额度设置</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
倍率设置
|
||||
</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='模型倍率'
|
||||
name='ModelRatio'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autoComplete='new-password'
|
||||
value={inputs.ModelRatio}
|
||||
placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='补全倍率'
|
||||
name='CompletionRatio'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autoComplete='new-password'
|
||||
value={inputs.CompletionRatio}
|
||||
placeholder='为一个 JSON 文本,键为模型名称,值为倍率,此处的倍率设置是模型补全倍率相较于提示倍率的比例,使用该设置可强制覆盖 One API 的内部比例'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='分组倍率'
|
||||
name='GroupRatio'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autoComplete='new-password'
|
||||
value={inputs.GroupRatio}
|
||||
placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('ratio').then();
|
||||
}}>保存倍率设置</Form.Button>
|
||||
<Form.Button
|
||||
onClick={() => {
|
||||
submitConfig('general').then();
|
||||
}}
|
||||
>
|
||||
{t('setting.operation.general.buttons.save')}
|
||||
</Form.Button>
|
||||
</Form>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
|
@ -1,10 +1,20 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Divider, Form, Grid, Header, Message, Modal } from 'semantic-ui-react';
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import { marked } from 'marked';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
Grid,
|
||||
Header,
|
||||
Message,
|
||||
Modal,
|
||||
} from 'semantic-ui-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { API, showError, showSuccess, verifyJSON } from '../helpers';
|
||||
import { marked } from 'marked';
|
||||
|
||||
const OtherSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
let [inputs, setInputs] = useState({
|
||||
Footer: '',
|
||||
Notice: '',
|
||||
@ -12,13 +22,13 @@ const OtherSetting = () => {
|
||||
SystemName: '',
|
||||
Logo: '',
|
||||
HomePageContent: '',
|
||||
Theme: ''
|
||||
Theme: '',
|
||||
});
|
||||
let [loading, setLoading] = useState(false);
|
||||
const [showUpdateModal, setShowUpdateModal] = useState(false);
|
||||
const [updateData, setUpdateData] = useState({
|
||||
tag_name: '',
|
||||
content: ''
|
||||
content: '',
|
||||
});
|
||||
|
||||
const getOptions = async () => {
|
||||
@ -45,7 +55,7 @@ const OtherSetting = () => {
|
||||
setLoading(true);
|
||||
const res = await API.put('/api/option/', {
|
||||
key,
|
||||
value
|
||||
value,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
@ -64,10 +74,6 @@ const OtherSetting = () => {
|
||||
await updateOption('Notice', inputs.Notice);
|
||||
};
|
||||
|
||||
const submitFooter = async () => {
|
||||
await updateOption('Footer', inputs.Footer);
|
||||
};
|
||||
|
||||
const submitSystemName = async () => {
|
||||
await updateOption('SystemName', inputs.SystemName);
|
||||
};
|
||||
@ -89,8 +95,7 @@ const OtherSetting = () => {
|
||||
};
|
||||
|
||||
const openGitHubRelease = () => {
|
||||
window.location =
|
||||
'https://github.com/songquanpeng/one-api/releases/latest';
|
||||
window.location = 'https://github.com/songquanpeng/one-api/releases/latest';
|
||||
};
|
||||
|
||||
const checkUpdate = async () => {
|
||||
@ -103,7 +108,7 @@ const OtherSetting = () => {
|
||||
} else {
|
||||
setUpdateData({
|
||||
tag_name: tag_name,
|
||||
content: marked.parse(body)
|
||||
content: marked.parse(body),
|
||||
});
|
||||
setShowUpdateModal(true);
|
||||
}
|
||||
@ -113,87 +118,110 @@ const OtherSetting = () => {
|
||||
<Grid columns={1}>
|
||||
<Grid.Column>
|
||||
<Form loading={loading}>
|
||||
<Header as='h3'>通用设置</Header>
|
||||
<Form.Button onClick={checkUpdate}>检查更新</Form.Button>
|
||||
<Header as='h3'>{t('setting.other.notice.title')}</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='公告'
|
||||
placeholder='在此输入新的公告内容,支持 Markdown & HTML 代码'
|
||||
label={t('setting.other.notice.content')}
|
||||
placeholder={t('setting.other.notice.content_placeholder')}
|
||||
value={inputs.Notice}
|
||||
name='Notice'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
style={{ minHeight: 100, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitNotice}>保存公告</Form.Button>
|
||||
<Form.Button onClick={submitNotice}>
|
||||
{t('setting.other.notice.buttons.save')}
|
||||
</Form.Button>
|
||||
|
||||
<Divider />
|
||||
<Header as='h3'>个性化设置</Header>
|
||||
<Header as='h3'>{t('setting.other.system.title')}</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label='系统名称'
|
||||
placeholder='在此输入系统名称'
|
||||
label={t('setting.other.system.name')}
|
||||
placeholder={t('setting.other.system.name_placeholder')}
|
||||
value={inputs.SystemName}
|
||||
name='SystemName'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitSystemName}>设置系统名称</Form.Button>
|
||||
<Form.Button onClick={submitSystemName}>
|
||||
{t('setting.other.system.buttons.save_name')}
|
||||
</Form.Button>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label={<label>主题名称(<Link
|
||||
to='https://github.com/songquanpeng/one-api/blob/main/web/README.md'>当前可用主题</Link>)</label>}
|
||||
placeholder='请输入主题名称'
|
||||
label={
|
||||
<label>
|
||||
{t('setting.other.system.theme.title')}(
|
||||
<Link to='https://github.com/songquanpeng/one-api/blob/main/web/README.md'>
|
||||
{t('setting.other.system.theme.link')}
|
||||
</Link>
|
||||
)
|
||||
</label>
|
||||
}
|
||||
placeholder={t('setting.other.system.theme.placeholder')}
|
||||
value={inputs.Theme}
|
||||
name='Theme'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitTheme}>设置主题(重启生效)</Form.Button>
|
||||
<Form.Button onClick={submitTheme}>
|
||||
{t('setting.other.system.buttons.save_theme')}
|
||||
</Form.Button>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label='Logo 图片地址'
|
||||
placeholder='在此输入 Logo 图片地址'
|
||||
label={t('setting.other.system.logo')}
|
||||
placeholder={t('setting.other.system.logo_placeholder')}
|
||||
value={inputs.Logo}
|
||||
name='Logo'
|
||||
type='url'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitLogo}>设置 Logo</Form.Button>
|
||||
<Form.Button onClick={submitLogo}>
|
||||
{t('setting.other.system.buttons.save_logo')}
|
||||
</Form.Button>
|
||||
|
||||
<Divider />
|
||||
<Header as='h3'>{t('setting.other.content.title')}</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='首页内容'
|
||||
placeholder='在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'
|
||||
label={t('setting.other.content.homepage.title')}
|
||||
placeholder={t('setting.other.content.homepage.placeholder')}
|
||||
value={inputs.HomePageContent}
|
||||
name='HomePageContent'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => submitOption('HomePageContent')}>保存首页内容</Form.Button>
|
||||
<Form.Button onClick={() => submitOption('HomePageContent')}>
|
||||
{t('setting.other.content.buttons.save_homepage')}
|
||||
</Form.Button>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='关于'
|
||||
placeholder='在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'
|
||||
label={t('setting.other.content.about.title')}
|
||||
placeholder={t('setting.other.content.about.placeholder')}
|
||||
value={inputs.About}
|
||||
name='About'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitAbout}>保存关于</Form.Button>
|
||||
<Message>移除 One API
|
||||
的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。</Message>
|
||||
<Form.Button onClick={submitAbout}>
|
||||
{t('setting.other.content.buttons.save_about')}
|
||||
</Form.Button>
|
||||
<Message>{t('setting.other.copyright.notice')}</Message>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label='页脚'
|
||||
placeholder='在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码'
|
||||
label={t('setting.other.content.footer.title')}
|
||||
placeholder={t('setting.other.content.footer.placeholder')}
|
||||
value={inputs.Footer}
|
||||
name='Footer'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitFooter}>设置页脚</Form.Button>
|
||||
<Form.Button onClick={() => submitOption('Footer')}>
|
||||
{t('setting.other.content.buttons.save_footer')}
|
||||
</Form.Button>
|
||||
</Form>
|
||||
</Grid.Column>
|
||||
<Modal
|
||||
|
@ -8,29 +8,23 @@ import {
|
||||
Card,
|
||||
Message,
|
||||
} from 'semantic-ui-react';
|
||||
import {
|
||||
API,
|
||||
copy,
|
||||
showError,
|
||||
showInfo,
|
||||
showNotice,
|
||||
showSuccess,
|
||||
} from '../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, copy, getLogo, showError, showNotice } from '../helpers';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
const PasswordResetConfirm = () => {
|
||||
const { t } = useTranslation();
|
||||
const [inputs, setInputs] = useState({
|
||||
email: '',
|
||||
token: '',
|
||||
});
|
||||
const { email, token } = inputs;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [disableButton, setDisableButton] = useState(false);
|
||||
const [countdown, setCountdown] = useState(30);
|
||||
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const logo = getLogo();
|
||||
|
||||
const [countdown, setCountdown] = useState(30);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
useEffect(() => {
|
||||
@ -68,7 +62,7 @@ const PasswordResetConfirm = () => {
|
||||
let password = res.data.data;
|
||||
setNewPassword(password);
|
||||
await copy(password);
|
||||
showNotice(`新密码已复制到剪贴板:${password}`);
|
||||
showNotice(t('messages.notice.password_copied', { password }));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@ -90,8 +84,8 @@ const PasswordResetConfirm = () => {
|
||||
textAlign='center'
|
||||
style={{ marginBottom: '1.5em' }}
|
||||
>
|
||||
<Image src='/logo.png' style={{ marginBottom: '10px' }} />
|
||||
<Header.Content>密码重置确认</Header.Content>
|
||||
<Image src={logo} style={{ marginBottom: '10px' }} />
|
||||
<Header.Content>{t('auth.reset.confirm.title')}</Header.Content>
|
||||
</Header>
|
||||
</Card.Header>
|
||||
<Form size='large'>
|
||||
@ -99,7 +93,7 @@ const PasswordResetConfirm = () => {
|
||||
fluid
|
||||
icon='mail'
|
||||
iconPosition='left'
|
||||
placeholder='邮箱地址'
|
||||
placeholder={t('auth.reset.email')}
|
||||
name='email'
|
||||
value={email}
|
||||
readOnly
|
||||
@ -110,7 +104,7 @@ const PasswordResetConfirm = () => {
|
||||
fluid
|
||||
icon='lock'
|
||||
iconPosition='left'
|
||||
placeholder='新密码'
|
||||
placeholder={t('auth.reset.confirm.new_password')}
|
||||
name='newPassword'
|
||||
value={newPassword}
|
||||
readOnly
|
||||
@ -122,30 +116,31 @@ const PasswordResetConfirm = () => {
|
||||
onClick={(e) => {
|
||||
e.target.select();
|
||||
navigator.clipboard.writeText(newPassword);
|
||||
showNotice(`密码已复制到剪贴板:${newPassword}`);
|
||||
showNotice(t('auth.reset.confirm.notice'));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
color='blue'
|
||||
fluid
|
||||
size='large'
|
||||
onClick={handleSubmit}
|
||||
loading={loading}
|
||||
disabled={disableButton}
|
||||
style={{
|
||||
background: '#2F73FF', // 使用更现代的蓝色
|
||||
background: '#2F73FF',
|
||||
color: 'white',
|
||||
marginBottom: '1.5em',
|
||||
}}
|
||||
>
|
||||
{disableButton ? '密码重置完成' : '提交'}
|
||||
{disableButton
|
||||
? t('auth.reset.confirm.button_disabled')
|
||||
: t('auth.reset.confirm.button')}
|
||||
</Button>
|
||||
</Form>
|
||||
{newPassword && (
|
||||
<Message style={{ background: 'transparent', boxShadow: 'none' }}>
|
||||
<p style={{ fontSize: '0.9em', color: '#666' }}>
|
||||
新密码已生成,请点击密码框或上方按钮复制。请及时登录并修改密码!
|
||||
{t('auth.reset.confirm.notice')}
|
||||
</p>
|
||||
</Message>
|
||||
)}
|
||||
|
@ -8,21 +8,23 @@ import {
|
||||
Card,
|
||||
Message,
|
||||
} from 'semantic-ui-react';
|
||||
import { API, showError, showInfo, showSuccess } from '../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
|
||||
const PasswordResetForm = () => {
|
||||
const { t } = useTranslation();
|
||||
const [inputs, setInputs] = useState({
|
||||
email: '',
|
||||
});
|
||||
const { email } = inputs;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||
const [turnstileToken, setTurnstileToken] = useState('');
|
||||
const [disableButton, setDisableButton] = useState(false);
|
||||
const [countdown, setCountdown] = useState(30);
|
||||
const logo = getLogo();
|
||||
|
||||
useEffect(() => {
|
||||
let status = localStorage.getItem('status');
|
||||
@ -66,10 +68,12 @@ const PasswordResetForm = () => {
|
||||
);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('重置邮件发送成功,请检查邮箱!');
|
||||
showSuccess(t('auth.reset.notice'));
|
||||
setInputs({ ...inputs, email: '' });
|
||||
} else {
|
||||
showError(message);
|
||||
setDisableButton(false);
|
||||
setCountdown(30);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
@ -89,8 +93,8 @@ const PasswordResetForm = () => {
|
||||
textAlign='center'
|
||||
style={{ marginBottom: '1.5em' }}
|
||||
>
|
||||
<Image src='/logo.png' style={{ marginBottom: '10px' }} />
|
||||
<Header.Content>密码重置</Header.Content>
|
||||
<Image src={logo} style={{ marginBottom: '10px' }} />
|
||||
<Header.Content>{t('auth.reset.title')}</Header.Content>
|
||||
</Header>
|
||||
</Card.Header>
|
||||
<Form size='large'>
|
||||
@ -98,7 +102,7 @@ const PasswordResetForm = () => {
|
||||
fluid
|
||||
icon='mail'
|
||||
iconPosition='left'
|
||||
placeholder='邮箱地址'
|
||||
placeholder={t('auth.reset.email')}
|
||||
name='email'
|
||||
value={email}
|
||||
onChange={handleChange}
|
||||
@ -133,12 +137,14 @@ const PasswordResetForm = () => {
|
||||
marginBottom: '1.5em',
|
||||
}}
|
||||
>
|
||||
{disableButton ? `重试 (${countdown})` : '提交'}
|
||||
{disableButton
|
||||
? t('auth.register.get_code_retry', { countdown })
|
||||
: t('auth.reset.button')}
|
||||
</Button>
|
||||
</Form>
|
||||
<Message style={{ background: 'transparent', boxShadow: 'none' }}>
|
||||
<p style={{ fontSize: '0.9em', color: '#666' }}>
|
||||
系统将向您的邮箱发送一封包含重置链接的邮件,请注意查收。
|
||||
{t('auth.reset.notice')}
|
||||
</p>
|
||||
</Message>
|
||||
</Card.Content>
|
||||
|
@ -1,12 +1,29 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
Header,
|
||||
Image,
|
||||
Message,
|
||||
Modal,
|
||||
} from 'semantic-ui-react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
||||
import {
|
||||
API,
|
||||
copy,
|
||||
showError,
|
||||
showInfo,
|
||||
showNotice,
|
||||
showSuccess,
|
||||
} from '../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
import { UserContext } from '../context/User';
|
||||
import { onGitHubOAuthClicked, onLarkOAuthClicked } from './utils';
|
||||
|
||||
const PersonalSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
let navigate = useNavigate();
|
||||
|
||||
@ -14,7 +31,7 @@ const PersonalSetting = () => {
|
||||
wechat_verification_code: '',
|
||||
email_verification_code: '',
|
||||
email: '',
|
||||
self_account_deletion_confirmation: ''
|
||||
self_account_deletion_confirmation: '',
|
||||
});
|
||||
const [status, setStatus] = useState({});
|
||||
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
|
||||
@ -26,8 +43,8 @@ const PersonalSetting = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [disableButton, setDisableButton] = useState(false);
|
||||
const [countdown, setCountdown] = useState(30);
|
||||
const [affLink, setAffLink] = useState("");
|
||||
const [systemToken, setSystemToken] = useState("");
|
||||
const [affLink, setAffLink] = useState('');
|
||||
const [systemToken, setSystemToken] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let status = localStorage.getItem('status');
|
||||
@ -63,7 +80,7 @@ const PersonalSetting = () => {
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setSystemToken(data);
|
||||
setAffLink("");
|
||||
setAffLink('');
|
||||
await copy(data);
|
||||
showSuccess(`令牌已重置并已复制到剪贴板`);
|
||||
} else {
|
||||
@ -77,7 +94,7 @@ const PersonalSetting = () => {
|
||||
if (success) {
|
||||
let link = `${window.location.origin}/register?aff=${data}`;
|
||||
setAffLink(link);
|
||||
setSystemToken("");
|
||||
setSystemToken('');
|
||||
await copy(link);
|
||||
showSuccess(`邀请链接已复制到剪切板`);
|
||||
} else {
|
||||
@ -169,18 +186,24 @@ const PersonalSetting = () => {
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: '40px' }}>
|
||||
<Header as='h3'>通用设置</Header>
|
||||
<Message>
|
||||
注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI 相关的服务,请知悉。
|
||||
</Message>
|
||||
<Header as='h3'>{t('setting.personal.general.title')}</Header>
|
||||
<Message>{t('setting.personal.general.system_token_notice')}</Message>
|
||||
<Button as={Link} to={`/user/edit/`}>
|
||||
更新个人信息
|
||||
{t('setting.personal.general.buttons.update_profile')}
|
||||
</Button>
|
||||
<Button onClick={generateAccessToken}>
|
||||
{t('setting.personal.general.buttons.generate_token')}
|
||||
</Button>
|
||||
<Button onClick={getAffLink}>
|
||||
{t('setting.personal.general.buttons.copy_invite')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowAccountDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
{t('setting.personal.general.buttons.delete_account')}
|
||||
</Button>
|
||||
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
|
||||
<Button onClick={getAffLink}>复制邀请链接</Button>
|
||||
<Button onClick={() => {
|
||||
setShowAccountDeleteModal(true);
|
||||
}}>删除个人账户</Button>
|
||||
|
||||
{systemToken && (
|
||||
<Form.Input
|
||||
@ -201,18 +224,12 @@ const PersonalSetting = () => {
|
||||
/>
|
||||
)}
|
||||
<Divider />
|
||||
<Header as='h3'>账号绑定</Header>
|
||||
{
|
||||
status.wechat_login && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowWeChatBindModal(true);
|
||||
}}
|
||||
>
|
||||
绑定微信账号
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Header as='h3'>{t('setting.personal.binding.title')}</Header>
|
||||
{status.wechat_login && (
|
||||
<Button onClick={() => setShowWeChatBindModal(true)}>
|
||||
{t('setting.personal.binding.buttons.bind_wechat')}
|
||||
</Button>
|
||||
)}
|
||||
<Modal
|
||||
onClose={() => setShowWeChatBindModal(false)}
|
||||
onOpen={() => setShowWeChatBindModal(true)}
|
||||
@ -223,41 +240,37 @@ const PersonalSetting = () => {
|
||||
<Modal.Description>
|
||||
<Image src={status.wechat_qrcode} fluid />
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<p>
|
||||
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
||||
</p>
|
||||
<p>{t('setting.personal.binding.wechat.description')}</p>
|
||||
</div>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder='验证码'
|
||||
placeholder={t(
|
||||
'setting.personal.binding.wechat.verification_code'
|
||||
)}
|
||||
name='wechat_verification_code'
|
||||
value={inputs.wechat_verification_code}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Button color='' fluid size='large' onClick={bindWeChat}>
|
||||
绑定
|
||||
{t('setting.personal.binding.wechat.bind')}
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
{
|
||||
status.github_oauth && (
|
||||
<Button onClick={()=>{onGitHubOAuthClicked(status.github_client_id)}}>绑定 GitHub 账号</Button>
|
||||
)
|
||||
}
|
||||
{
|
||||
status.lark_client_id && (
|
||||
<Button onClick={()=>{onLarkOAuthClicked(status.lark_client_id)}}>绑定飞书账号</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowEmailBindModal(true);
|
||||
}}
|
||||
>
|
||||
绑定邮箱地址
|
||||
{status.github_oauth && (
|
||||
<Button onClick={() => onGitHubOAuthClicked(status.github_client_id)}>
|
||||
{t('setting.personal.binding.buttons.bind_github')}
|
||||
</Button>
|
||||
)}
|
||||
{status.lark_client_id && (
|
||||
<Button onClick={() => onLarkOAuthClicked(status.lark_client_id)}>
|
||||
{t('setting.personal.binding.buttons.bind_lark')}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => setShowEmailBindModal(true)}>
|
||||
{t('setting.personal.binding.buttons.bind_email')}
|
||||
</Button>
|
||||
<Modal
|
||||
onClose={() => setShowEmailBindModal(false)}
|
||||
@ -266,57 +279,72 @@ const PersonalSetting = () => {
|
||||
size={'tiny'}
|
||||
style={{ maxWidth: '450px' }}
|
||||
>
|
||||
<Modal.Header>绑定邮箱地址</Modal.Header>
|
||||
<Modal.Header>{t('setting.personal.binding.email.title')}</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Modal.Description>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder='输入邮箱地址'
|
||||
placeholder={t(
|
||||
'setting.personal.binding.email.email_placeholder'
|
||||
)}
|
||||
onChange={handleInputChange}
|
||||
name='email'
|
||||
type='email'
|
||||
action={
|
||||
<Button onClick={sendVerificationCode} disabled={disableButton || loading}>
|
||||
{disableButton ? `重新发送(${countdown})` : '获取验证码'}
|
||||
<Button
|
||||
onClick={sendVerificationCode}
|
||||
disabled={disableButton || loading}
|
||||
>
|
||||
{disableButton
|
||||
? t('setting.personal.binding.email.get_code_retry', {
|
||||
countdown,
|
||||
})
|
||||
: t('setting.personal.binding.email.get_code')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder='验证码'
|
||||
placeholder={t(
|
||||
'setting.personal.binding.email.code_placeholder'
|
||||
)}
|
||||
name='email_verification_code'
|
||||
value={inputs.email_verification_code}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{turnstileEnabled ? (
|
||||
{turnstileEnabled && (
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
|
||||
<Button
|
||||
color=''
|
||||
fluid
|
||||
size='large'
|
||||
onClick={bindEmail}
|
||||
loading={loading}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: '1rem',
|
||||
}}
|
||||
>
|
||||
确认绑定
|
||||
</Button>
|
||||
<div style={{ width: '1rem' }}></div>
|
||||
<Button
|
||||
fluid
|
||||
size='large'
|
||||
onClick={() => setShowEmailBindModal(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
color=''
|
||||
fluid
|
||||
size='large'
|
||||
onClick={bindEmail}
|
||||
loading={loading}
|
||||
>
|
||||
{t('setting.personal.binding.email.bind')}
|
||||
</Button>
|
||||
<div style={{ width: '1rem' }}></div>
|
||||
<Button
|
||||
fluid
|
||||
size='large'
|
||||
onClick={() => setShowEmailBindModal(false)}
|
||||
>
|
||||
{t('setting.personal.binding.email.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal.Description>
|
||||
@ -329,29 +357,40 @@ const PersonalSetting = () => {
|
||||
size={'tiny'}
|
||||
style={{ maxWidth: '450px' }}
|
||||
>
|
||||
<Modal.Header>危险操作</Modal.Header>
|
||||
<Modal.Header>
|
||||
{t('setting.personal.delete_account.title')}
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Message>您正在删除自己的帐户,将清空所有数据且不可恢复</Message>
|
||||
<Message>{t('setting.personal.delete_account.warning')}</Message>
|
||||
<Modal.Description>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
|
||||
placeholder={t(
|
||||
'setting.personal.delete_account.confirm_placeholder',
|
||||
{
|
||||
username: userState?.user?.username,
|
||||
}
|
||||
)}
|
||||
name='self_account_deletion_confirmation'
|
||||
value={inputs.self_account_deletion_confirmation}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{turnstileEnabled ? (
|
||||
{turnstileEnabled && (
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: '1rem',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
color='red'
|
||||
fluid
|
||||
@ -359,7 +398,7 @@ const PersonalSetting = () => {
|
||||
onClick={deleteAccount}
|
||||
loading={loading}
|
||||
>
|
||||
确认删除
|
||||
{t('setting.personal.delete_account.buttons.confirm')}
|
||||
</Button>
|
||||
<div style={{ width: '1rem' }}></div>
|
||||
<Button
|
||||
@ -367,7 +406,7 @@ const PersonalSetting = () => {
|
||||
size='large'
|
||||
onClick={() => setShowAccountDeleteModal(false)}
|
||||
>
|
||||
取消
|
||||
{t('setting.personal.delete_account.buttons.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
@ -25,39 +26,37 @@ function renderTimestamp(timestamp) {
|
||||
return <>{timestamp2string(timestamp)}</>;
|
||||
}
|
||||
|
||||
function renderStatus(status) {
|
||||
function renderStatus(status, t) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return (
|
||||
<Label basic color='green'>
|
||||
未使用
|
||||
{t('redemption.status.unused')}
|
||||
</Label>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Label basic color='red'>
|
||||
{' '}
|
||||
已禁用{' '}
|
||||
{t('redemption.status.disabled')}
|
||||
</Label>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Label basic color='grey'>
|
||||
{' '}
|
||||
已使用{' '}
|
||||
{t('redemption.status.used')}
|
||||
</Label>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Label basic color='black'>
|
||||
{' '}
|
||||
未知状态{' '}
|
||||
{t('redemption.status.unknown')}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const RedemptionsTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const [redemptions, setRedemptions] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
@ -117,7 +116,7 @@ const RedemptionsTable = () => {
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('操作成功完成!');
|
||||
showSuccess(t('token.messages.operation_success'));
|
||||
let redemption = res.data.data;
|
||||
let newRedemptions = [...redemptions];
|
||||
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
|
||||
@ -177,6 +176,12 @@ const RedemptionsTable = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
setLoading(true);
|
||||
await loadRedemptions(0);
|
||||
setActivePage(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form onSubmit={searchRedemptions}>
|
||||
@ -184,7 +189,7 @@ const RedemptionsTable = () => {
|
||||
icon='search'
|
||||
fluid
|
||||
iconPosition='left'
|
||||
placeholder='搜索兑换码的 ID 和名称 ...'
|
||||
placeholder={t('redemption.search')}
|
||||
value={searchKeyword}
|
||||
loading={searching}
|
||||
onChange={handleKeywordChange}
|
||||
@ -200,7 +205,7 @@ const RedemptionsTable = () => {
|
||||
sortRedemption('id');
|
||||
}}
|
||||
>
|
||||
ID
|
||||
{t('redemption.table.id')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -208,7 +213,7 @@ const RedemptionsTable = () => {
|
||||
sortRedemption('name');
|
||||
}}
|
||||
>
|
||||
名称
|
||||
{t('redemption.table.name')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -216,7 +221,7 @@ const RedemptionsTable = () => {
|
||||
sortRedemption('status');
|
||||
}}
|
||||
>
|
||||
状态
|
||||
{t('redemption.table.status')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -224,7 +229,7 @@ const RedemptionsTable = () => {
|
||||
sortRedemption('quota');
|
||||
}}
|
||||
>
|
||||
额度
|
||||
{t('redemption.table.quota')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -232,7 +237,7 @@ const RedemptionsTable = () => {
|
||||
sortRedemption('created_time');
|
||||
}}
|
||||
>
|
||||
创建时间
|
||||
{t('redemption.table.created_time')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -240,9 +245,9 @@ const RedemptionsTable = () => {
|
||||
sortRedemption('redeemed_time');
|
||||
}}
|
||||
>
|
||||
兑换时间
|
||||
{t('redemption.table.redeemed_time')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell>操作</Table.HeaderCell>
|
||||
<Table.HeaderCell>{t('redemption.table.actions')}</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
@ -258,40 +263,38 @@ const RedemptionsTable = () => {
|
||||
<Table.Row key={redemption.id}>
|
||||
<Table.Cell>{redemption.id}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{redemption.name ? redemption.name : '无'}
|
||||
{redemption.name ? redemption.name : t('redemption.table.no_name')}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{renderStatus(redemption.status)}</Table.Cell>
|
||||
<Table.Cell>{renderQuota(redemption.quota)}</Table.Cell>
|
||||
<Table.Cell>{renderStatus(redemption.status, t)}</Table.Cell>
|
||||
<Table.Cell>{renderQuota(redemption.quota, t)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{renderTimestamp(redemption.created_time)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{redemption.redeemed_time
|
||||
? renderTimestamp(redemption.redeemed_time)
|
||||
: '尚未兑换'}{' '}
|
||||
: t('redemption.table.not_redeemed')}{' '}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div>
|
||||
<Button
|
||||
size={'small'}
|
||||
size={'tiny'}
|
||||
positive
|
||||
onClick={async () => {
|
||||
if (await copy(redemption.key)) {
|
||||
showSuccess('已复制到剪贴板!');
|
||||
showSuccess(t('token.messages.copy_success'));
|
||||
} else {
|
||||
showWarning(
|
||||
'无法复制到剪贴板,请手动复制,已将兑换码填入搜索框。'
|
||||
);
|
||||
showWarning(t('token.messages.copy_failed'));
|
||||
setSearchKeyword(redemption.key);
|
||||
}
|
||||
}}
|
||||
>
|
||||
复制
|
||||
{t('redemption.buttons.copy')}
|
||||
</Button>
|
||||
<Popup
|
||||
trigger={
|
||||
<Button size='small' negative>
|
||||
删除
|
||||
<Button size='tiny' negative>
|
||||
{t('redemption.buttons.delete')}
|
||||
</Button>
|
||||
}
|
||||
on='click'
|
||||
@ -304,11 +307,11 @@ const RedemptionsTable = () => {
|
||||
manageRedemption(redemption.id, 'delete', idx);
|
||||
}}
|
||||
>
|
||||
确认删除
|
||||
{t('redemption.buttons.confirm_delete')}
|
||||
</Button>
|
||||
</Popup>
|
||||
<Button
|
||||
size={'small'}
|
||||
size={'tiny'}
|
||||
disabled={redemption.status === 3} // used
|
||||
onClick={() => {
|
||||
manageRedemption(
|
||||
@ -318,14 +321,16 @@ const RedemptionsTable = () => {
|
||||
);
|
||||
}}
|
||||
>
|
||||
{redemption.status === 1 ? '禁用' : '启用'}
|
||||
{redemption.status === 1
|
||||
? t('redemption.buttons.disable')
|
||||
: t('redemption.buttons.enable')}
|
||||
</Button>
|
||||
<Button
|
||||
size={'small'}
|
||||
size={'tiny'}
|
||||
as={Link}
|
||||
to={'/redemption/edit/' + redemption.id}
|
||||
>
|
||||
编辑
|
||||
{t('redemption.buttons.edit')}
|
||||
</Button>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
@ -336,14 +341,17 @@ const RedemptionsTable = () => {
|
||||
|
||||
<Table.Footer>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell colSpan='8'>
|
||||
<Table.HeaderCell colSpan='7'>
|
||||
<Button
|
||||
size='small'
|
||||
as={Link}
|
||||
to='/redemption/add'
|
||||
loading={loading}
|
||||
>
|
||||
添加新的兑换码
|
||||
{t('redemption.buttons.add')}
|
||||
</Button>
|
||||
<Button size='small' onClick={refresh} loading={loading}>
|
||||
{t('redemption.buttons.refresh')}
|
||||
</Button>
|
||||
<Pagination
|
||||
floated='right'
|
||||
|
@ -6,15 +6,16 @@ import {
|
||||
Header,
|
||||
Image,
|
||||
Message,
|
||||
Segment,
|
||||
Card,
|
||||
Divider,
|
||||
} from 'semantic-ui-react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
|
||||
const RegisterForm = () => {
|
||||
const { t } = useTranslation();
|
||||
const [inputs, setInputs] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
@ -28,6 +29,8 @@ const RegisterForm = () => {
|
||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||
const [turnstileToken, setTurnstileToken] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [disableButton, setDisableButton] = useState(false);
|
||||
const [countdown, setCountdown] = useState(30);
|
||||
const logo = getLogo();
|
||||
let affCode = new URLSearchParams(window.location.search).get('aff');
|
||||
if (affCode) {
|
||||
@ -46,6 +49,19 @@ const RegisterForm = () => {
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let countdownInterval = null;
|
||||
if (disableButton && countdown > 0) {
|
||||
countdownInterval = setInterval(() => {
|
||||
setCountdown(countdown - 1);
|
||||
}, 1000);
|
||||
} else if (countdown === 0) {
|
||||
setDisableButton(false);
|
||||
setCountdown(30);
|
||||
}
|
||||
return () => clearInterval(countdownInterval);
|
||||
}, [disableButton, countdown]);
|
||||
|
||||
let navigate = useNavigate();
|
||||
|
||||
function handleChange(e) {
|
||||
@ -56,16 +72,16 @@ const RegisterForm = () => {
|
||||
|
||||
async function handleSubmit(e) {
|
||||
if (password.length < 8) {
|
||||
showInfo('密码长度不得小于 8 位!');
|
||||
showInfo(t('messages.error.password_length'));
|
||||
return;
|
||||
}
|
||||
if (password !== password2) {
|
||||
showInfo('两次输入的密码不一致');
|
||||
showInfo(t('messages.error.password_mismatch'));
|
||||
return;
|
||||
}
|
||||
if (username && password) {
|
||||
if (turnstileEnabled && turnstileToken === '') {
|
||||
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||
showInfo(t('messages.error.turnstile_wait'));
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@ -80,7 +96,7 @@ const RegisterForm = () => {
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
navigate('/login');
|
||||
showSuccess('注册成功!');
|
||||
showSuccess(t('messages.success.register'));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@ -91,18 +107,21 @@ const RegisterForm = () => {
|
||||
const sendVerificationCode = async () => {
|
||||
if (inputs.email === '') return;
|
||||
if (turnstileEnabled && turnstileToken === '') {
|
||||
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||
showInfo(t('messages.error.turnstile_wait'));
|
||||
return;
|
||||
}
|
||||
setDisableButton(true);
|
||||
setLoading(true);
|
||||
const res = await API.get(
|
||||
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
|
||||
);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('验证码发送成功,请检查你的邮箱!');
|
||||
showSuccess(t('messages.success.verification_code'));
|
||||
} else {
|
||||
showError(message);
|
||||
setDisableButton(false);
|
||||
setCountdown(30);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
@ -123,7 +142,7 @@ const RegisterForm = () => {
|
||||
style={{ marginBottom: '1.5em' }}
|
||||
>
|
||||
<Image src={logo} style={{ marginBottom: '10px' }} />
|
||||
<Header.Content>新用户注册</Header.Content>
|
||||
<Header.Content>{t('auth.register.title')}</Header.Content>
|
||||
</Header>
|
||||
</Card.Header>
|
||||
<Form size='large'>
|
||||
@ -131,7 +150,7 @@ const RegisterForm = () => {
|
||||
fluid
|
||||
icon='user'
|
||||
iconPosition='left'
|
||||
placeholder='输入用户名,最长 12 位'
|
||||
placeholder={t('auth.register.username')}
|
||||
onChange={handleChange}
|
||||
name='username'
|
||||
style={{ marginBottom: '1em' }}
|
||||
@ -140,7 +159,7 @@ const RegisterForm = () => {
|
||||
fluid
|
||||
icon='lock'
|
||||
iconPosition='left'
|
||||
placeholder='输入密码,最短 8 位,最长 20 位'
|
||||
placeholder={t('auth.register.password')}
|
||||
onChange={handleChange}
|
||||
name='password'
|
||||
type='password'
|
||||
@ -150,7 +169,7 @@ const RegisterForm = () => {
|
||||
fluid
|
||||
icon='lock'
|
||||
iconPosition='left'
|
||||
placeholder='再次输入密码'
|
||||
placeholder={t('auth.register.confirm_password')}
|
||||
onChange={handleChange}
|
||||
name='password2'
|
||||
type='password'
|
||||
@ -163,17 +182,15 @@ const RegisterForm = () => {
|
||||
fluid
|
||||
icon='mail'
|
||||
iconPosition='left'
|
||||
placeholder='输入邮箱地址'
|
||||
placeholder={t('auth.register.email')}
|
||||
onChange={handleChange}
|
||||
name='email'
|
||||
type='email'
|
||||
action={
|
||||
<Button
|
||||
onClick={sendVerificationCode}
|
||||
disabled={loading}
|
||||
style={{ backgroundColor: '#2185d0', color: 'white' }}
|
||||
>
|
||||
获取验证码
|
||||
<Button onClick={sendVerificationCode} disabled={loading}>
|
||||
{disableButton
|
||||
? t('auth.register.get_code_retry', { countdown })
|
||||
: t('auth.register.get_code')}
|
||||
</Button>
|
||||
}
|
||||
style={{ marginBottom: '1em' }}
|
||||
@ -182,7 +199,7 @@ const RegisterForm = () => {
|
||||
fluid
|
||||
icon='lock'
|
||||
iconPosition='left'
|
||||
placeholder='输入验证码'
|
||||
placeholder={t('auth.register.verification_code')}
|
||||
onChange={handleChange}
|
||||
name='verification_code'
|
||||
style={{ marginBottom: '1em' }}
|
||||
@ -218,7 +235,7 @@ const RegisterForm = () => {
|
||||
}}
|
||||
loading={loading}
|
||||
>
|
||||
注册
|
||||
{t('auth.register.button')}
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
@ -231,9 +248,12 @@ const RegisterForm = () => {
|
||||
color: '#666',
|
||||
}}
|
||||
>
|
||||
已有账户?
|
||||
<Link to='/login' style={{ color: '#2185d0' }}>
|
||||
点击登录
|
||||
{t('auth.register.has_account')}
|
||||
<Link
|
||||
to='/login'
|
||||
style={{ color: '#2185d0', marginLeft: '2px' }}
|
||||
>
|
||||
{t('auth.register.login')}
|
||||
</Link>
|
||||
</div>
|
||||
</Message>
|
||||
|
@ -1,8 +1,18 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Divider, Form, Grid, Header, Modal, Message } from 'semantic-ui-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
Grid,
|
||||
Header,
|
||||
Modal,
|
||||
Message,
|
||||
} from 'semantic-ui-react';
|
||||
import { API, removeTrailingSlash, showError } from '../helpers';
|
||||
|
||||
const SystemSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
let [inputs, setInputs] = useState({
|
||||
PasswordLoginEnabled: '',
|
||||
PasswordRegisterEnabled: '',
|
||||
@ -31,13 +41,14 @@ const SystemSetting = () => {
|
||||
TurnstileSecretKey: '',
|
||||
RegisterEnabled: '',
|
||||
EmailDomainRestrictionEnabled: '',
|
||||
EmailDomainWhitelist: ''
|
||||
EmailDomainWhitelist: '',
|
||||
});
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
let [loading, setLoading] = useState(false);
|
||||
const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
|
||||
const [restrictedDomainInput, setRestrictedDomainInput] = useState('');
|
||||
const [showPasswordWarningModal, setShowPasswordWarningModal] = useState(false);
|
||||
const [showPasswordWarningModal, setShowPasswordWarningModal] =
|
||||
useState(false);
|
||||
|
||||
const getOptions = async () => {
|
||||
const res = await API.get('/api/option/');
|
||||
@ -49,13 +60,15 @@ const SystemSetting = () => {
|
||||
});
|
||||
setInputs({
|
||||
...newInputs,
|
||||
EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(',')
|
||||
EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(','),
|
||||
});
|
||||
setOriginInputs(newInputs);
|
||||
|
||||
setEmailDomainWhitelist(newInputs.EmailDomainWhitelist.split(',').map((item) => {
|
||||
return { key: item, text: item, value: item };
|
||||
}));
|
||||
setEmailDomainWhitelist(
|
||||
newInputs.EmailDomainWhitelist.split(',').map((item) => {
|
||||
return { key: item, text: item, value: item };
|
||||
})
|
||||
);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@ -83,7 +96,7 @@ const SystemSetting = () => {
|
||||
}
|
||||
const res = await API.put('/api/option/', {
|
||||
key,
|
||||
value
|
||||
value,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
@ -91,7 +104,8 @@ const SystemSetting = () => {
|
||||
value = value.split(',');
|
||||
}
|
||||
setInputs((inputs) => ({
|
||||
...inputs, [key]: value
|
||||
...inputs,
|
||||
[key]: value,
|
||||
}));
|
||||
} else {
|
||||
showError(message);
|
||||
@ -155,13 +169,16 @@ const SystemSetting = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const submitEmailDomainWhitelist = async () => {
|
||||
if (
|
||||
originInputs['EmailDomainWhitelist'] !== inputs.EmailDomainWhitelist.join(',') &&
|
||||
originInputs['EmailDomainWhitelist'] !==
|
||||
inputs.EmailDomainWhitelist.join(',') &&
|
||||
inputs.SMTPToken !== ''
|
||||
) {
|
||||
await updateOption('EmailDomainWhitelist', inputs.EmailDomainWhitelist.join(','));
|
||||
await updateOption(
|
||||
'EmailDomainWhitelist',
|
||||
inputs.EmailDomainWhitelist.join(',')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -216,7 +233,7 @@ const SystemSetting = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const submitLarkOAuth = async () => {
|
||||
const submitLarkOAuth = async () => {
|
||||
if (originInputs['LarkClientId'] !== inputs.LarkClientId) {
|
||||
await updateOption('LarkClientId', inputs.LarkClientId);
|
||||
}
|
||||
@ -242,60 +259,71 @@ const SystemSetting = () => {
|
||||
|
||||
const submitNewRestrictedDomain = () => {
|
||||
const localDomainList = inputs.EmailDomainWhitelist;
|
||||
if (restrictedDomainInput !== '' && !localDomainList.includes(restrictedDomainInput)) {
|
||||
if (
|
||||
restrictedDomainInput !== '' &&
|
||||
!localDomainList.includes(restrictedDomainInput)
|
||||
) {
|
||||
setRestrictedDomainInput('');
|
||||
setInputs({
|
||||
...inputs,
|
||||
EmailDomainWhitelist: [...localDomainList, restrictedDomainInput],
|
||||
});
|
||||
setEmailDomainWhitelist([...EmailDomainWhitelist, {
|
||||
key: restrictedDomainInput,
|
||||
text: restrictedDomainInput,
|
||||
value: restrictedDomainInput,
|
||||
}]);
|
||||
setEmailDomainWhitelist([
|
||||
...EmailDomainWhitelist,
|
||||
{
|
||||
key: restrictedDomainInput,
|
||||
text: restrictedDomainInput,
|
||||
value: restrictedDomainInput,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid columns={1}>
|
||||
<Grid.Column>
|
||||
<Form loading={loading}>
|
||||
<Header as='h3'>通用设置</Header>
|
||||
<Header as='h3'>{t('setting.system.general.title')}</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label='服务器地址'
|
||||
placeholder='例如:https://yourdomain.com'
|
||||
label={t('setting.system.general.server_address')}
|
||||
placeholder={t(
|
||||
'setting.system.general.server_address_placeholder'
|
||||
)}
|
||||
value={inputs.ServerAddress}
|
||||
name='ServerAddress'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitServerAddress}>
|
||||
更新服务器地址
|
||||
{t('setting.system.general.buttons.update')}
|
||||
</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>配置登录注册</Header>
|
||||
<Header as='h3'>{t('setting.system.login.title')}</Header>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.PasswordLoginEnabled === 'true'}
|
||||
label='允许通过密码进行登录'
|
||||
label={t('setting.system.login.password_login')}
|
||||
name='PasswordLoginEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{
|
||||
showPasswordWarningModal &&
|
||||
{showPasswordWarningModal && (
|
||||
<Modal
|
||||
open={showPasswordWarningModal}
|
||||
onClose={() => setShowPasswordWarningModal(false)}
|
||||
size={'tiny'}
|
||||
style={{ maxWidth: '450px' }}
|
||||
>
|
||||
<Modal.Header>警告</Modal.Header>
|
||||
<Modal.Header>
|
||||
{t('setting.system.password_login.warning.title')}
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<p>取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?</p>
|
||||
<p>{t('setting.system.password_login.warning.content')}</p>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={() => setShowPasswordWarningModal(false)}>取消</Button>
|
||||
<Button onClick={() => setShowPasswordWarningModal(false)}>
|
||||
{t('setting.system.password_login.warning.buttons.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
color='yellow'
|
||||
onClick={async () => {
|
||||
@ -303,32 +331,32 @@ const SystemSetting = () => {
|
||||
await updateOption('PasswordLoginEnabled', 'false');
|
||||
}}
|
||||
>
|
||||
确定
|
||||
{t('setting.system.password_login.warning.buttons.confirm')}
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
}
|
||||
)}
|
||||
<Form.Checkbox
|
||||
checked={inputs.PasswordRegisterEnabled === 'true'}
|
||||
label='允许通过密码进行注册'
|
||||
label={t('setting.system.login.password_register')}
|
||||
name='PasswordRegisterEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.EmailVerificationEnabled === 'true'}
|
||||
label='通过密码注册时需要进行邮箱验证'
|
||||
label={t('setting.system.login.email_verification')}
|
||||
name='EmailVerificationEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.GitHubOAuthEnabled === 'true'}
|
||||
label='允许通过 GitHub 账户登录 & 注册'
|
||||
label={t('setting.system.login.github_oauth')}
|
||||
name='GitHubOAuthEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.WeChatAuthEnabled === 'true'}
|
||||
label='允许通过微信登录 & 注册'
|
||||
label={t('setting.system.login.wechat_login')}
|
||||
name='WeChatAuthEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
@ -336,304 +364,295 @@ const SystemSetting = () => {
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.RegisterEnabled === 'true'}
|
||||
label='允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)'
|
||||
label={t('setting.system.login.registration')}
|
||||
name='RegisterEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.TurnstileCheckEnabled === 'true'}
|
||||
label='启用 Turnstile 用户校验'
|
||||
label={t('setting.system.login.turnstile')}
|
||||
name='TurnstileCheckEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
配置邮箱域名白名单
|
||||
<Header.Subheader>用以防止恶意用户利用临时邮箱批量注册</Header.Subheader>
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Header as='h3'>{t('setting.system.email_restriction.title')}</Header>
|
||||
<Message>{t('setting.system.email_restriction.subtitle')}</Message>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
label='启用邮箱域名白名单'
|
||||
checked={inputs.EmailDomainRestrictionEnabled === 'true'}
|
||||
label={t('setting.system.email_restriction.enable')}
|
||||
name='EmailDomainRestrictionEnabled'
|
||||
onChange={handleInputChange}
|
||||
checked={inputs.EmailDomainRestrictionEnabled === 'true'}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths={2}>
|
||||
<Form.Dropdown
|
||||
label='允许的邮箱域名'
|
||||
placeholder='允许的邮箱域名'
|
||||
name='EmailDomainWhitelist'
|
||||
required
|
||||
fluid
|
||||
multiple
|
||||
selection
|
||||
onChange={handleInputChange}
|
||||
value={inputs.EmailDomainWhitelist}
|
||||
autoComplete='new-password'
|
||||
options={EmailDomainWhitelist}
|
||||
/>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='添加新的允许的邮箱域名'
|
||||
action={
|
||||
<Button type='button' onClick={() => {
|
||||
submitNewRestrictedDomain();
|
||||
}}>填入</Button>
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
submitNewRestrictedDomain();
|
||||
}
|
||||
}}
|
||||
autoComplete='new-password'
|
||||
placeholder='输入新的允许的邮箱域名'
|
||||
label={t('setting.system.email_restriction.add_domain')}
|
||||
placeholder={t(
|
||||
'setting.system.email_restriction.add_domain_placeholder'
|
||||
)}
|
||||
value={restrictedDomainInput}
|
||||
onChange={(e, { value }) => {
|
||||
setRestrictedDomainInput(value);
|
||||
}}
|
||||
action={
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (restrictedDomainInput === '') return;
|
||||
setEmailDomainWhitelist([
|
||||
...EmailDomainWhitelist,
|
||||
{
|
||||
key: restrictedDomainInput,
|
||||
text: restrictedDomainInput,
|
||||
value: restrictedDomainInput,
|
||||
},
|
||||
]);
|
||||
setRestrictedDomainInput('');
|
||||
}}
|
||||
>
|
||||
{t('setting.system.email_restriction.buttons.fill')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitEmailDomainWhitelist}>保存邮箱域名白名单设置</Form.Button>
|
||||
<Form.Dropdown
|
||||
label={t('setting.system.email_restriction.allowed_domains')}
|
||||
placeholder={t('setting.system.email_restriction.allowed_domains')}
|
||||
fluid
|
||||
multiple
|
||||
search
|
||||
selection
|
||||
allowAdditions
|
||||
value={EmailDomainWhitelist.map((item) => item.value)}
|
||||
options={EmailDomainWhitelist}
|
||||
onAddItem={(e, { value }) => {
|
||||
setEmailDomainWhitelist([
|
||||
...EmailDomainWhitelist,
|
||||
{
|
||||
key: value,
|
||||
text: value,
|
||||
value: value,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
onChange={(e, { value }) => {
|
||||
let newEmailDomainWhitelist = [];
|
||||
value.forEach((item) => {
|
||||
newEmailDomainWhitelist.push({
|
||||
key: item,
|
||||
text: item,
|
||||
value: item,
|
||||
});
|
||||
});
|
||||
setEmailDomainWhitelist(newEmailDomainWhitelist);
|
||||
}}
|
||||
/>
|
||||
<Form.Button onClick={submitEmailDomainWhitelist}>
|
||||
{t('setting.system.email_restriction.buttons.save')}
|
||||
</Form.Button>
|
||||
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
配置 SMTP
|
||||
<Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
|
||||
</Header>
|
||||
<Header as='h3'>{t('setting.system.smtp.title')}</Header>
|
||||
<Message>{t('setting.system.smtp.subtitle')}</Message>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='SMTP 服务器地址'
|
||||
label={t('setting.system.smtp.server')}
|
||||
placeholder={t('setting.system.smtp.server_placeholder')}
|
||||
name='SMTPServer'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.SMTPServer}
|
||||
placeholder='例如:smtp.qq.com'
|
||||
/>
|
||||
<Form.Input
|
||||
label='SMTP 端口'
|
||||
label={t('setting.system.smtp.port')}
|
||||
placeholder={t('setting.system.smtp.port_placeholder')}
|
||||
name='SMTPPort'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.SMTPPort}
|
||||
placeholder='默认: 587'
|
||||
/>
|
||||
<Form.Input
|
||||
label='SMTP 账户'
|
||||
label={t('setting.system.smtp.account')}
|
||||
placeholder={t('setting.system.smtp.account_placeholder')}
|
||||
name='SMTPAccount'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.SMTPAccount}
|
||||
placeholder='通常是邮箱地址'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='SMTP 发送者邮箱'
|
||||
label={t('setting.system.smtp.from')}
|
||||
placeholder={t('setting.system.smtp.from_placeholder')}
|
||||
name='SMTPFrom'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.SMTPFrom}
|
||||
placeholder='通常和邮箱地址保持一致'
|
||||
/>
|
||||
<Form.Input
|
||||
label='SMTP 访问凭证'
|
||||
label={t('setting.system.smtp.token')}
|
||||
placeholder={t('setting.system.smtp.token_placeholder')}
|
||||
name='SMTPToken'
|
||||
onChange={handleInputChange}
|
||||
type='password'
|
||||
autoComplete='new-password'
|
||||
checked={inputs.RegisterEnabled === 'true'}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
value={inputs.SMTPToken}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitSMTP}>保存 SMTP 设置</Form.Button>
|
||||
<Form.Button onClick={submitSMTP}>
|
||||
{t('setting.system.smtp.buttons.save')}
|
||||
</Form.Button>
|
||||
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
配置 GitHub OAuth App
|
||||
<Header.Subheader>
|
||||
用以支持通过 GitHub 进行登录注册,
|
||||
<a href='https://github.com/settings/developers' target='_blank'>
|
||||
点击此处
|
||||
</a>
|
||||
管理你的 GitHub OAuth App
|
||||
</Header.Subheader>
|
||||
</Header>
|
||||
<Header as='h3'>{t('setting.system.github.title')}</Header>
|
||||
<Message>
|
||||
Homepage URL 填 <code>{inputs.ServerAddress}</code>
|
||||
,Authorization callback URL 填{' '}
|
||||
<code>{`${inputs.ServerAddress}/oauth/github`}</code>
|
||||
{t('setting.system.github.subtitle')}
|
||||
<a href='https://github.com/settings/developers' target='_blank'>
|
||||
{t('setting.system.github.manage_link')}
|
||||
</a>
|
||||
{t('setting.system.github.manage_text')}
|
||||
</Message>
|
||||
<Message>
|
||||
{t('setting.system.github.url_notice', {
|
||||
server_url: originInputs.ServerAddress,
|
||||
callback_url: `${originInputs.ServerAddress}/oauth/github`,
|
||||
})}
|
||||
</Message>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='GitHub Client ID'
|
||||
label={t('setting.system.github.client_id')}
|
||||
placeholder={t('setting.system.github.client_id_placeholder')}
|
||||
name='GitHubClientId'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.GitHubClientId}
|
||||
placeholder='输入你注册的 GitHub OAuth APP 的 ID'
|
||||
/>
|
||||
<Form.Input
|
||||
label='GitHub Client Secret'
|
||||
label={t('setting.system.github.client_secret')}
|
||||
placeholder={t('setting.system.github.client_secret_placeholder')}
|
||||
name='GitHubClientSecret'
|
||||
onChange={handleInputChange}
|
||||
type='password'
|
||||
autoComplete='new-password'
|
||||
value={inputs.GitHubClientSecret}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitGitHubOAuth}>
|
||||
保存 GitHub OAuth 设置
|
||||
{t('setting.system.github.buttons.save')}
|
||||
</Form.Button>
|
||||
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
配置飞书授权登录
|
||||
{t('setting.system.lark.title')}
|
||||
<Header.Subheader>
|
||||
用以支持通过飞书进行登录注册,
|
||||
{t('setting.system.lark.subtitle')}
|
||||
<a href='https://open.feishu.cn/app' target='_blank'>
|
||||
点击此处
|
||||
{t('setting.system.lark.manage_link')}
|
||||
</a>
|
||||
管理你的飞书应用
|
||||
{t('setting.system.lark.manage_text')}
|
||||
</Header.Subheader>
|
||||
</Header>
|
||||
<Message>
|
||||
主页链接填 <code>{inputs.ServerAddress}</code>
|
||||
,重定向 URL 填{' '}
|
||||
<code>{`${inputs.ServerAddress}/oauth/lark`}</code>
|
||||
{t('setting.system.lark.url_notice', {
|
||||
server_url: inputs.ServerAddress,
|
||||
callback_url: `${inputs.ServerAddress}/oauth/lark`,
|
||||
})}
|
||||
</Message>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='App ID'
|
||||
label={t('setting.system.lark.client_id')}
|
||||
name='LarkClientId'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.LarkClientId}
|
||||
placeholder='输入 App ID'
|
||||
placeholder={t('setting.system.lark.client_id_placeholder')}
|
||||
/>
|
||||
<Form.Input
|
||||
label='App Secret'
|
||||
label={t('setting.system.lark.client_secret')}
|
||||
name='LarkClientSecret'
|
||||
onChange={handleInputChange}
|
||||
type='password'
|
||||
autoComplete='new-password'
|
||||
value={inputs.LarkClientSecret}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
placeholder={t('setting.system.lark.client_secret_placeholder')}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitLarkOAuth}>
|
||||
保存飞书 OAuth 设置
|
||||
{t('setting.system.lark.buttons.save')}
|
||||
</Form.Button>
|
||||
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
配置 WeChat Server
|
||||
{t('setting.system.wechat.title')}
|
||||
<Header.Subheader>
|
||||
用以支持通过微信进行登录注册,
|
||||
{t('setting.system.wechat.subtitle')}
|
||||
<a
|
||||
href='https://github.com/songquanpeng/wechat-server'
|
||||
target='_blank'
|
||||
>
|
||||
点击此处
|
||||
{t('setting.system.wechat.learn_more')}
|
||||
</a>
|
||||
了解 WeChat Server
|
||||
</Header.Subheader>
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='WeChat Server 服务器地址'
|
||||
label={t('setting.system.wechat.server_address')}
|
||||
name='WeChatServerAddress'
|
||||
placeholder='例如:https://yourdomain.com'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.WeChatServerAddress}
|
||||
placeholder={t(
|
||||
'setting.system.wechat.server_address_placeholder'
|
||||
)}
|
||||
/>
|
||||
<Form.Input
|
||||
label='WeChat Server 访问凭证'
|
||||
label={t('setting.system.wechat.token')}
|
||||
name='WeChatServerToken'
|
||||
type='password'
|
||||
onChange={handleInputChange}
|
||||
type='password'
|
||||
autoComplete='new-password'
|
||||
value={inputs.WeChatServerToken}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
placeholder={t('setting.system.wechat.token_placeholder')}
|
||||
/>
|
||||
<Form.Input
|
||||
label='微信公众号二维码图片链接'
|
||||
label={t('setting.system.wechat.qrcode')}
|
||||
name='WeChatAccountQRCodeImageURL'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.WeChatAccountQRCodeImageURL}
|
||||
placeholder='输入一个图片链接'
|
||||
placeholder={t('setting.system.wechat.qrcode_placeholder')}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitWeChat}>
|
||||
保存 WeChat Server 设置
|
||||
{t('setting.system.wechat.buttons.save')}
|
||||
</Form.Button>
|
||||
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
配置 Message Pusher
|
||||
{t('setting.system.turnstile.title')}
|
||||
<Header.Subheader>
|
||||
用以推送报警信息,
|
||||
<a
|
||||
href='https://github.com/songquanpeng/message-pusher'
|
||||
target='_blank'
|
||||
>
|
||||
点击此处
|
||||
</a>
|
||||
了解 Message Pusher
|
||||
</Header.Subheader>
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='Message Pusher 推送地址'
|
||||
name='MessagePusherAddress'
|
||||
placeholder='例如:https://msgpusher.com/push/your_username'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.MessagePusherAddress}
|
||||
/>
|
||||
<Form.Input
|
||||
label='Message Pusher 访问凭证'
|
||||
name='MessagePusherToken'
|
||||
type='password'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.MessagePusherToken}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitMessagePusher}>
|
||||
保存 Message Pusher 设置
|
||||
</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
配置 Turnstile
|
||||
<Header.Subheader>
|
||||
用以支持用户校验,
|
||||
{t('setting.system.turnstile.subtitle')}
|
||||
<a href='https://dash.cloudflare.com/' target='_blank'>
|
||||
点击此处
|
||||
{t('setting.system.turnstile.manage_link')}
|
||||
</a>
|
||||
管理你的 Turnstile Sites,推荐选择 Invisible Widget Type
|
||||
{t('setting.system.turnstile.manage_text')}
|
||||
</Header.Subheader>
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='Turnstile Site Key'
|
||||
label={t('setting.system.turnstile.site_key')}
|
||||
name='TurnstileSiteKey'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.TurnstileSiteKey}
|
||||
placeholder='输入你注册的 Turnstile Site Key'
|
||||
placeholder={t('setting.system.turnstile.site_key_placeholder')}
|
||||
/>
|
||||
<Form.Input
|
||||
label='Turnstile Secret Key'
|
||||
label={t('setting.system.turnstile.secret_key')}
|
||||
name='TurnstileSecretKey'
|
||||
onChange={handleInputChange}
|
||||
type='password'
|
||||
autoComplete='new-password'
|
||||
value={inputs.TurnstileSecretKey}
|
||||
placeholder='敏感信息不会发送到前端显示'
|
||||
placeholder={t('setting.system.turnstile.secret_key_placeholder')}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitTurnstile}>
|
||||
保存 Turnstile 设置
|
||||
{t('setting.system.turnstile.buttons.save')}
|
||||
</Form.Button>
|
||||
</Form>
|
||||
</Grid.Column>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
@ -21,64 +22,63 @@ import {
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderQuota } from '../helpers/render';
|
||||
|
||||
const COPY_OPTIONS = [
|
||||
{ key: 'next', text: 'ChatGPT Next Web', value: 'next' },
|
||||
{ key: 'ama', text: 'BotGem', value: 'ama' },
|
||||
{ key: 'opencat', text: 'OpenCat', value: 'opencat' },
|
||||
{ key: 'lobechat', text: 'LobeChat', value: 'lobechat' },
|
||||
];
|
||||
|
||||
const OPEN_LINK_OPTIONS = [
|
||||
{ key: 'next', text: 'ChatGPT Next Web', value: 'next' },
|
||||
{ key: 'ama', text: 'BotGem', value: 'ama' },
|
||||
{ key: 'opencat', text: 'OpenCat', value: 'opencat' },
|
||||
{ key: 'lobechat', text: 'LobeChat', value: 'lobechat' },
|
||||
];
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return <>{timestamp2string(timestamp)}</>;
|
||||
}
|
||||
|
||||
function renderStatus(status) {
|
||||
function renderStatus(status, t) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return (
|
||||
<Label basic color='green'>
|
||||
已启用
|
||||
{t('token.table.status_enabled')}
|
||||
</Label>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Label basic color='red'>
|
||||
{' '}
|
||||
已禁用{' '}
|
||||
{t('token.table.status_disabled')}
|
||||
</Label>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Label basic color='yellow'>
|
||||
{' '}
|
||||
已过期{' '}
|
||||
{t('token.table.status_expired')}
|
||||
</Label>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<Label basic color='grey'>
|
||||
{' '}
|
||||
已耗尽{' '}
|
||||
{t('token.table.status_depleted')}
|
||||
</Label>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Label basic color='black'>
|
||||
{' '}
|
||||
未知状态{' '}
|
||||
{t('token.table.status_unknown')}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const TokensTable = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const COPY_OPTIONS = [
|
||||
{ key: 'raw', text: t('token.copy_options.raw'), value: '' },
|
||||
{ key: 'next', text: t('token.copy_options.next'), value: 'next' },
|
||||
{ key: 'ama', text: t('token.copy_options.ama'), value: 'ama' },
|
||||
{ key: 'opencat', text: t('token.copy_options.opencat'), value: 'opencat' },
|
||||
{ key: 'lobe', text: t('token.copy_options.lobe'), value: 'lobechat' },
|
||||
];
|
||||
|
||||
const OPEN_LINK_OPTIONS = [
|
||||
{ key: 'next', text: t('token.copy_options.next'), value: 'next' },
|
||||
{ key: 'ama', text: t('token.copy_options.ama'), value: 'ama' },
|
||||
{ key: 'opencat', text: t('token.copy_options.opencat'), value: 'opencat' },
|
||||
{ key: 'lobe', text: t('token.copy_options.lobe'), value: 'lobechat' },
|
||||
];
|
||||
|
||||
const [tokens, setTokens] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
@ -161,9 +161,9 @@ const TokensTable = () => {
|
||||
url = `sk-${key}`;
|
||||
}
|
||||
if (await copy(url)) {
|
||||
showSuccess('已复制到剪贴板!');
|
||||
showSuccess(t('token.messages.copy_success'));
|
||||
} else {
|
||||
showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
|
||||
showWarning(t('token.messages.copy_failed'));
|
||||
setSearchKeyword(url);
|
||||
}
|
||||
};
|
||||
@ -237,7 +237,7 @@ const TokensTable = () => {
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('操作成功完成!');
|
||||
showSuccess(t('token.messages.operation_success'));
|
||||
let token = res.data.data;
|
||||
let newTokens = [...tokens];
|
||||
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
|
||||
@ -308,7 +308,7 @@ const TokensTable = () => {
|
||||
icon='search'
|
||||
fluid
|
||||
iconPosition='left'
|
||||
placeholder='搜索令牌的名称 ...'
|
||||
placeholder={t('token.search')}
|
||||
value={searchKeyword}
|
||||
loading={searching}
|
||||
onChange={handleKeywordChange}
|
||||
@ -324,7 +324,7 @@ const TokensTable = () => {
|
||||
sortToken('name');
|
||||
}}
|
||||
>
|
||||
名称
|
||||
{t('token.table.name')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -332,7 +332,7 @@ const TokensTable = () => {
|
||||
sortToken('status');
|
||||
}}
|
||||
>
|
||||
状态
|
||||
{t('token.table.status')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -340,7 +340,7 @@ const TokensTable = () => {
|
||||
sortToken('used_quota');
|
||||
}}
|
||||
>
|
||||
已用额度
|
||||
{t('token.table.used_quota')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -348,7 +348,7 @@ const TokensTable = () => {
|
||||
sortToken('remain_quota');
|
||||
}}
|
||||
>
|
||||
剩余额度
|
||||
{t('token.table.remain_quota')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -356,7 +356,7 @@ const TokensTable = () => {
|
||||
sortToken('created_time');
|
||||
}}
|
||||
>
|
||||
创建时间
|
||||
{t('token.table.created_time')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -364,9 +364,9 @@ const TokensTable = () => {
|
||||
sortToken('expired_time');
|
||||
}}
|
||||
>
|
||||
过期时间
|
||||
{t('token.table.expired_time')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell>操作</Table.HeaderCell>
|
||||
<Table.HeaderCell>{t('token.table.actions')}</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
@ -378,72 +378,77 @@ const TokensTable = () => {
|
||||
)
|
||||
.map((token, idx) => {
|
||||
if (token.deleted) return <></>;
|
||||
|
||||
const copyOptionsWithHandlers = COPY_OPTIONS.map((option) => ({
|
||||
...option,
|
||||
onClick: async () => {
|
||||
await onCopy(option.value, token.key);
|
||||
},
|
||||
}));
|
||||
|
||||
const openLinkOptionsWithHandlers = OPEN_LINK_OPTIONS.map(
|
||||
(option) => ({
|
||||
...option,
|
||||
onClick: async () => {
|
||||
await onOpenLink(option.value, token.key);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<Table.Row key={token.id}>
|
||||
<Table.Cell>{token.name ? token.name : '无'}</Table.Cell>
|
||||
<Table.Cell>{renderStatus(token.status)}</Table.Cell>
|
||||
<Table.Cell>{renderQuota(token.used_quota)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{token.name ? token.name : t('token.table.no_name')}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{renderStatus(token.status, t)}</Table.Cell>
|
||||
<Table.Cell>{renderQuota(token.used_quota, t)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{token.unlimited_quota
|
||||
? '无限制'
|
||||
: renderQuota(token.remain_quota, 2)}
|
||||
? t('token.table.unlimited')
|
||||
: renderQuota(token.remain_quota, t, 2)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{token.expired_time === -1
|
||||
? '永不过期'
|
||||
? t('token.table.never_expire')
|
||||
: renderTimestamp(token.expired_time)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div>
|
||||
<Button.Group color='green' size={'small'}>
|
||||
<Button.Group color='green' size={'tiny'}>
|
||||
<Button
|
||||
size={'small'}
|
||||
size={'tiny'}
|
||||
positive
|
||||
onClick={async () => {
|
||||
await onCopy('', token.key);
|
||||
}}
|
||||
onClick={async () => await onCopy('', token.key)}
|
||||
>
|
||||
复制
|
||||
{t('token.buttons.copy')}
|
||||
</Button>
|
||||
<Dropdown
|
||||
className='button icon'
|
||||
floating
|
||||
options={COPY_OPTIONS.map((option) => ({
|
||||
...option,
|
||||
onClick: async () => {
|
||||
await onCopy(option.value, token.key);
|
||||
},
|
||||
}))}
|
||||
options={copyOptionsWithHandlers}
|
||||
trigger={<></>}
|
||||
/>
|
||||
</Button.Group>{' '}
|
||||
<Button.Group color='blue' size={'small'}>
|
||||
<Button.Group color='olive' size={'tiny'}>
|
||||
<Button
|
||||
size={'small'}
|
||||
size={'tiny'}
|
||||
positive
|
||||
onClick={() => {
|
||||
onOpenLink('', token.key);
|
||||
}}
|
||||
onClick={() => onOpenLink('', token.key)}
|
||||
>
|
||||
聊天
|
||||
{t('token.buttons.chat')}
|
||||
</Button>
|
||||
<Dropdown
|
||||
className='button icon'
|
||||
floating
|
||||
options={OPEN_LINK_OPTIONS.map((option) => ({
|
||||
...option,
|
||||
onClick: async () => {
|
||||
await onOpenLink(option.value, token.key);
|
||||
},
|
||||
}))}
|
||||
options={openLinkOptionsWithHandlers}
|
||||
trigger={<></>}
|
||||
/>
|
||||
</Button.Group>{' '}
|
||||
<Popup
|
||||
trigger={
|
||||
<Button size='small' negative>
|
||||
删除
|
||||
<Button size='mini' negative>
|
||||
{t('token.buttons.delete')}
|
||||
</Button>
|
||||
}
|
||||
on='click'
|
||||
@ -451,16 +456,17 @@ const TokensTable = () => {
|
||||
hoverable
|
||||
>
|
||||
<Button
|
||||
size={'tiny'}
|
||||
negative
|
||||
onClick={() => {
|
||||
manageToken(token.id, 'delete', idx);
|
||||
}}
|
||||
>
|
||||
删除令牌 {token.name}
|
||||
{t('token.buttons.confirm_delete')} {token.name}
|
||||
</Button>
|
||||
</Popup>
|
||||
<Button
|
||||
size={'small'}
|
||||
size={'tiny'}
|
||||
onClick={() => {
|
||||
manageToken(
|
||||
token.id,
|
||||
@ -469,14 +475,16 @@ const TokensTable = () => {
|
||||
);
|
||||
}}
|
||||
>
|
||||
{token.status === 1 ? '禁用' : '启用'}
|
||||
{token.status === 1
|
||||
? t('token.buttons.disable')
|
||||
: t('token.buttons.enable')}
|
||||
</Button>
|
||||
<Button
|
||||
size={'small'}
|
||||
size={'tiny'}
|
||||
as={Link}
|
||||
to={'/token/edit/' + token.id}
|
||||
>
|
||||
编辑
|
||||
{t('token.buttons.edit')}
|
||||
</Button>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
@ -489,24 +497,24 @@ const TokensTable = () => {
|
||||
<Table.Row>
|
||||
<Table.HeaderCell colSpan='7'>
|
||||
<Button size='small' as={Link} to='/token/add' loading={loading}>
|
||||
添加新的令牌
|
||||
{t('token.buttons.add')}
|
||||
</Button>
|
||||
<Button size='small' onClick={refresh} loading={loading}>
|
||||
刷新
|
||||
{t('token.buttons.refresh')}
|
||||
</Button>
|
||||
<Dropdown
|
||||
placeholder='排序方式'
|
||||
placeholder={t('token.sort.placeholder')}
|
||||
selection
|
||||
options={[
|
||||
{ key: '', text: '默认排序', value: '' },
|
||||
{ key: '', text: t('token.sort.default'), value: '' },
|
||||
{
|
||||
key: 'remain_quota',
|
||||
text: '按剩余额度排序',
|
||||
text: t('token.sort.by_remain'),
|
||||
value: 'remain_quota',
|
||||
},
|
||||
{
|
||||
key: 'used_quota',
|
||||
text: '按已用额度排序',
|
||||
text: t('token.sort.by_used'),
|
||||
value: 'used_quota',
|
||||
},
|
||||
]}
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
} from 'semantic-ui-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import {
|
||||
@ -19,20 +20,23 @@ import {
|
||||
renderText,
|
||||
} from '../helpers/render';
|
||||
|
||||
function renderRole(role) {
|
||||
function renderRole(role, t) {
|
||||
switch (role) {
|
||||
case 1:
|
||||
return <Label>普通用户</Label>;
|
||||
return <Label>{t('user.table.role_types.normal')}</Label>;
|
||||
case 10:
|
||||
return <Label color='yellow'>管理员</Label>;
|
||||
return <Label color='yellow'>{t('user.table.role_types.admin')}</Label>;
|
||||
case 100:
|
||||
return <Label color='orange'>超级管理员</Label>;
|
||||
return (
|
||||
<Label color='orange'>{t('user.table.role_types.super_admin')}</Label>
|
||||
);
|
||||
default:
|
||||
return <Label color='red'>未知身份</Label>;
|
||||
return <Label color='red'>{t('user.table.role_types.unknown')}</Label>;
|
||||
}
|
||||
}
|
||||
|
||||
const UsersTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const [users, setUsers] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
@ -83,7 +87,7 @@ const UsersTable = () => {
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('操作成功完成!');
|
||||
showSuccess(t('user.messages.operation_success'));
|
||||
let user = res.data.data;
|
||||
let newUsers = [...users];
|
||||
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
|
||||
@ -103,17 +107,17 @@ const UsersTable = () => {
|
||||
const renderStatus = (status) => {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return <Label basic>已激活</Label>;
|
||||
return <Label basic>{t('user.table.status_types.activated')}</Label>;
|
||||
case 2:
|
||||
return (
|
||||
<Label basic color='red'>
|
||||
已封禁
|
||||
{t('user.table.status_types.banned')}
|
||||
</Label>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Label basic color='grey'>
|
||||
未知状态
|
||||
{t('user.table.status_types.unknown')}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
@ -175,7 +179,7 @@ const UsersTable = () => {
|
||||
icon='search'
|
||||
fluid
|
||||
iconPosition='left'
|
||||
placeholder='搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...'
|
||||
placeholder={t('user.search')}
|
||||
value={searchKeyword}
|
||||
loading={searching}
|
||||
onChange={handleKeywordChange}
|
||||
@ -191,7 +195,7 @@ const UsersTable = () => {
|
||||
sortUser('id');
|
||||
}}
|
||||
>
|
||||
ID
|
||||
{t('user.table.id')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -199,7 +203,7 @@ const UsersTable = () => {
|
||||
sortUser('username');
|
||||
}}
|
||||
>
|
||||
用户名
|
||||
{t('user.table.username')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -207,7 +211,7 @@ const UsersTable = () => {
|
||||
sortUser('group');
|
||||
}}
|
||||
>
|
||||
分组
|
||||
{t('user.table.group')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -215,7 +219,7 @@ const UsersTable = () => {
|
||||
sortUser('quota');
|
||||
}}
|
||||
>
|
||||
统计信息
|
||||
{t('user.table.quota')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -223,7 +227,7 @@ const UsersTable = () => {
|
||||
sortUser('role');
|
||||
}}
|
||||
>
|
||||
用户角色
|
||||
{t('user.table.role_text')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
@ -231,9 +235,9 @@ const UsersTable = () => {
|
||||
sortUser('status');
|
||||
}}
|
||||
>
|
||||
状态
|
||||
{t('user.table.status_text')}
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell>操作</Table.HeaderCell>
|
||||
<Table.HeaderCell>{t('user.table.actions')}</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
@ -265,54 +269,56 @@ const UsersTable = () => {
|
||||
{/*</Table.Cell>*/}
|
||||
<Table.Cell>
|
||||
<Popup
|
||||
content='剩余额度'
|
||||
trigger={<Label basic>{renderQuota(user.quota)}</Label>}
|
||||
/>
|
||||
<Popup
|
||||
content='已用额度'
|
||||
content={t('user.table.remaining_quota')}
|
||||
trigger={
|
||||
<Label basic>{renderQuota(user.used_quota)}</Label>
|
||||
<Label basic>{renderQuota(user.quota, t)}</Label>
|
||||
}
|
||||
/>
|
||||
<Popup
|
||||
content='请求次数'
|
||||
content={t('user.table.used_quota')}
|
||||
trigger={
|
||||
<Label basic>{renderQuota(user.used_quota, t)}</Label>
|
||||
}
|
||||
/>
|
||||
<Popup
|
||||
content={t('user.table.request_count')}
|
||||
trigger={
|
||||
<Label basic>{renderNumber(user.request_count)}</Label>
|
||||
}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>{renderRole(user.role)}</Table.Cell>
|
||||
<Table.Cell>{renderRole(user.role, t)}</Table.Cell>
|
||||
<Table.Cell>{renderStatus(user.status)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div>
|
||||
<Button
|
||||
size={'small'}
|
||||
size={'tiny'}
|
||||
positive
|
||||
onClick={() => {
|
||||
manageUser(user.username, 'promote', idx);
|
||||
}}
|
||||
disabled={user.role === 100}
|
||||
>
|
||||
提升
|
||||
{t('user.buttons.promote')}
|
||||
</Button>
|
||||
<Button
|
||||
size={'small'}
|
||||
size={'tiny'}
|
||||
color={'yellow'}
|
||||
onClick={() => {
|
||||
manageUser(user.username, 'demote', idx);
|
||||
}}
|
||||
disabled={user.role === 100}
|
||||
>
|
||||
降级
|
||||
{t('user.buttons.demote')}
|
||||
</Button>
|
||||
<Popup
|
||||
trigger={
|
||||
<Button
|
||||
size='small'
|
||||
size='tiny'
|
||||
negative
|
||||
disabled={user.role === 100}
|
||||
>
|
||||
删除
|
||||
{t('user.buttons.delete')}
|
||||
</Button>
|
||||
}
|
||||
on='click'
|
||||
@ -321,15 +327,16 @@ const UsersTable = () => {
|
||||
>
|
||||
<Button
|
||||
negative
|
||||
size={'tiny'}
|
||||
onClick={() => {
|
||||
manageUser(user.username, 'delete', idx);
|
||||
}}
|
||||
>
|
||||
删除用户 {user.username}
|
||||
{t('user.buttons.delete_user')} {user.username}
|
||||
</Button>
|
||||
</Popup>
|
||||
<Button
|
||||
size={'small'}
|
||||
size={'tiny'}
|
||||
onClick={() => {
|
||||
manageUser(
|
||||
user.username,
|
||||
@ -339,14 +346,16 @@ const UsersTable = () => {
|
||||
}}
|
||||
disabled={user.role === 100}
|
||||
>
|
||||
{user.status === 1 ? '禁用' : '启用'}
|
||||
{user.status === 1
|
||||
? t('user.buttons.disable')
|
||||
: t('user.buttons.enable')}
|
||||
</Button>
|
||||
<Button
|
||||
size={'small'}
|
||||
size={'tiny'}
|
||||
as={Link}
|
||||
to={'/user/edit/' + user.id}
|
||||
>
|
||||
编辑
|
||||
{t('user.buttons.edit')}
|
||||
</Button>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
@ -359,22 +368,26 @@ const UsersTable = () => {
|
||||
<Table.Row>
|
||||
<Table.HeaderCell colSpan='7'>
|
||||
<Button size='small' as={Link} to='/user/add' loading={loading}>
|
||||
添加新的用户
|
||||
{t('user.buttons.add')}
|
||||
</Button>
|
||||
<Dropdown
|
||||
placeholder='排序方式'
|
||||
placeholder={t('user.table.sort_by')}
|
||||
selection
|
||||
options={[
|
||||
{ key: '', text: '默认排序', value: '' },
|
||||
{ key: 'quota', text: '按剩余额度排序', value: 'quota' },
|
||||
{ key: '', text: t('user.table.sort.default'), value: '' },
|
||||
{
|
||||
key: 'quota',
|
||||
text: t('user.table.sort.by_quota'),
|
||||
value: 'quota',
|
||||
},
|
||||
{
|
||||
key: 'used_quota',
|
||||
text: '按已用额度排序',
|
||||
text: t('user.table.sort.by_used_quota'),
|
||||
value: 'used_quota',
|
||||
},
|
||||
{
|
||||
key: 'request_count',
|
||||
text: '按请求次数排序',
|
||||
text: t('user.table.sort.by_request_count'),
|
||||
value: 'request_count',
|
||||
},
|
||||
]}
|
||||
|
@ -1,48 +1,108 @@
|
||||
export const CHANNEL_OPTIONS = [
|
||||
{ key: 1, text: 'OpenAI', value: 1, color: 'green' },
|
||||
{ key: 14, text: 'Anthropic Claude', value: 14, color: 'black' },
|
||||
{ key: 33, text: 'AWS', value: 33, color: 'black' },
|
||||
{ key: 3, text: 'Azure OpenAI', value: 3, color: 'olive' },
|
||||
{ key: 11, text: 'Google PaLM2', value: 11, color: 'orange' },
|
||||
{ key: 24, text: 'Google Gemini', value: 24, color: 'orange' },
|
||||
{ key: 28, text: 'Mistral AI', value: 28, color: 'orange' },
|
||||
{ key: 41, text: 'Novita', value: 41, color: 'purple' },
|
||||
{ key: 40, text: '字节跳动豆包', value: 40, color: 'blue' },
|
||||
{ key: 15, text: '百度文心千帆', value: 15, color: 'blue' },
|
||||
{ key: 17, text: '阿里通义千问', value: 17, color: 'orange' },
|
||||
{ key: 18, text: '讯飞星火认知', value: 18, color: 'blue' },
|
||||
{ key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' },
|
||||
{ key: 19, text: '360 智脑', value: 19, color: 'blue' },
|
||||
{ key: 25, text: 'Moonshot AI', value: 25, color: 'black' },
|
||||
{ key: 23, text: '腾讯混元', value: 23, color: 'teal' },
|
||||
{ key: 26, text: '百川大模型', value: 26, color: 'orange' },
|
||||
{ key: 27, text: 'MiniMax', value: 27, color: 'red' },
|
||||
{ key: 29, text: 'Groq', value: 29, color: 'orange' },
|
||||
{ key: 30, text: 'Ollama', value: 30, color: 'black' },
|
||||
{ key: 31, text: '零一万物', value: 31, color: 'green' },
|
||||
{ key: 32, text: '阶跃星辰', value: 32, color: 'blue' },
|
||||
{ key: 34, text: 'Coze', value: 34, color: 'blue' },
|
||||
{ key: 35, text: 'Cohere', value: 35, color: 'blue' },
|
||||
{ key: 36, text: 'DeepSeek', value: 36, color: 'black' },
|
||||
{ key: 37, text: 'Cloudflare', value: 37, color: 'orange' },
|
||||
{ key: 38, text: 'DeepL', value: 38, color: 'black' },
|
||||
{ key: 39, text: 'together.ai', value: 39, color: 'blue' },
|
||||
{ key: 42, text: 'VertexAI', value: 42, color: 'blue' },
|
||||
{ key: 43, text: 'Proxy', value: 43, color: 'blue' },
|
||||
{ key: 44, text: 'SiliconFlow', value: 44, color: 'blue' },
|
||||
{ key: 45, text: 'xAI', value: 45, color: 'blue' },
|
||||
{ key: 46, text: 'Replicate', value: 46, color: 'blue' },
|
||||
{ key: 8, text: '自定义渠道', value: 8, color: 'pink' },
|
||||
{ key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' },
|
||||
{ key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' },
|
||||
{ key: 20, text: '代理:OpenRouter', value: 20, color: 'black' },
|
||||
{ key: 2, text: '代理:API2D', value: 2, color: 'blue' },
|
||||
{ key: 5, text: '代理:OpenAI-SB', value: 5, color: 'brown' },
|
||||
{ key: 7, text: '代理:OhMyGPT', value: 7, color: 'purple' },
|
||||
{ key: 10, text: '代理:AI Proxy', value: 10, color: 'purple' },
|
||||
{ key: 4, text: '代理:CloseAI', value: 4, color: 'teal' },
|
||||
{ key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet' },
|
||||
{ key: 9, text: '代理:AI.LS', value: 9, color: 'yellow' },
|
||||
{ key: 12, text: '代理:API2GPT', value: 12, color: 'blue' },
|
||||
{ key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' }
|
||||
{ key: 1, text: 'OpenAI', value: 1, color: 'green' },
|
||||
{
|
||||
key: 50,
|
||||
text: 'OpenAI 兼容',
|
||||
value: 50,
|
||||
color: 'olive',
|
||||
description: 'OpenAI 兼容渠道,支持设置 Base URL',
|
||||
},
|
||||
{key: 14, text: 'Anthropic', value: 14, color: 'black'},
|
||||
{ key: 33, text: 'AWS', value: 33, color: 'black' },
|
||||
{key: 3, text: 'Azure', value: 3, color: 'olive'},
|
||||
{key: 11, text: 'PaLM2', value: 11, color: 'orange'},
|
||||
{key: 24, text: 'Gemini', value: 24, color: 'orange'},
|
||||
{
|
||||
key: 51,
|
||||
text: 'Gemini (OpenAI)',
|
||||
value: 51,
|
||||
color: 'orange',
|
||||
description: 'Gemini OpenAI 兼容格式',
|
||||
},
|
||||
{ key: 28, text: 'Mistral AI', value: 28, color: 'orange' },
|
||||
{ key: 41, text: 'Novita', value: 41, color: 'purple' },
|
||||
{
|
||||
key: 40,
|
||||
text: '字节火山引擎',
|
||||
value: 40,
|
||||
color: 'blue',
|
||||
description: '原字节跳动豆包',
|
||||
},
|
||||
{
|
||||
key: 15,
|
||||
text: '百度文心千帆',
|
||||
value: 15,
|
||||
color: 'blue',
|
||||
tip: '请前往<a href="https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application/v1" target="_blank">此处</a>获取 AK(API Key)以及 SK(Secret Key),注意,V2 版本接口请使用 <strong>百度文心千帆 V2 </strong>渠道类型',
|
||||
},
|
||||
{
|
||||
key: 47,
|
||||
text: '百度文心千帆 V2',
|
||||
value: 47,
|
||||
color: 'blue',
|
||||
tip: '请前往<a href="https://console.bce.baidu.com/iam/#/iam/apikey/list" target="_blank">此处</a>获取 API Key,注意本渠道仅支持<a target="_blank" href="https://cloud.baidu.com/doc/WENXINWORKSHOP/s/em4tsqo3v">推理服务 V2</a>相关模型',
|
||||
},
|
||||
{
|
||||
key: 17,
|
||||
text: '阿里通义千问',
|
||||
value: 17,
|
||||
color: 'orange',
|
||||
tip: '如需使用阿里云百炼,请使用<strong>阿里云百炼</strong>渠道',
|
||||
},
|
||||
{ key: 49, text: '阿里云百炼', value: 49, color: 'orange' },
|
||||
{
|
||||
key: 18,
|
||||
text: '讯飞星火认知',
|
||||
value: 18,
|
||||
color: 'blue',
|
||||
tip: '本渠道基于讯飞 WebSocket 版本 API,如需 HTTP 版本,请使用<strong>讯飞星火认知 V2</strong>渠道',
|
||||
},
|
||||
{
|
||||
key: 48,
|
||||
text: '讯飞星火认知 V2',
|
||||
value: 48,
|
||||
color: 'blue',
|
||||
tip: 'HTTP 版本的讯飞接口,前往<a href="https://console.xfyun.cn/services/cbm" target="_blank">此处</a>获取 HTTP 服务接口认证密钥',
|
||||
},
|
||||
{ key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' },
|
||||
{ key: 19, text: '360 智脑', value: 19, color: 'blue' },
|
||||
{ key: 25, text: 'Moonshot AI', value: 25, color: 'black' },
|
||||
{ key: 23, text: '腾讯混元', value: 23, color: 'teal' },
|
||||
{ key: 26, text: '百川大模型', value: 26, color: 'orange' },
|
||||
{ key: 27, text: 'MiniMax', value: 27, color: 'red' },
|
||||
{ key: 29, text: 'Groq', value: 29, color: 'orange' },
|
||||
{ key: 30, text: 'Ollama', value: 30, color: 'black' },
|
||||
{ key: 31, text: '零一万物', value: 31, color: 'green' },
|
||||
{ key: 32, text: '阶跃星辰', value: 32, color: 'blue' },
|
||||
{ key: 34, text: 'Coze', value: 34, color: 'blue' },
|
||||
{ key: 35, text: 'Cohere', value: 35, color: 'blue' },
|
||||
{ key: 36, text: 'DeepSeek', value: 36, color: 'black' },
|
||||
{ key: 37, text: 'Cloudflare', value: 37, color: 'orange' },
|
||||
{ key: 38, text: 'DeepL', value: 38, color: 'black' },
|
||||
{ key: 39, text: 'together.ai', value: 39, color: 'blue' },
|
||||
{ key: 42, text: 'VertexAI', value: 42, color: 'blue' },
|
||||
{ key: 43, text: 'Proxy', value: 43, color: 'blue' },
|
||||
{ key: 44, text: 'SiliconFlow', value: 44, color: 'blue' },
|
||||
{ key: 45, text: 'xAI', value: 45, color: 'blue' },
|
||||
{ key: 46, text: 'Replicate', value: 46, color: 'blue' },
|
||||
{
|
||||
key: 8,
|
||||
text: '自定义渠道',
|
||||
value: 8,
|
||||
color: 'pink',
|
||||
tip: '不推荐使用,请使用 <strong>OpenAI 兼容</strong>渠道类型。注意,这里所需要填入的代理地址仅会在实际请求时替换域名部分,如果你想填入 OpenAI SDK 中所要求的 Base URL,请使用 OpenAI 兼容渠道类型',
|
||||
description: '不推荐使用,请使用 OpenAI 兼容渠道类型',
|
||||
},
|
||||
{ key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' },
|
||||
{ key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' },
|
||||
{ key: 20, text: 'OpenRouter', value: 20, color: 'black' },
|
||||
{ key: 2, text: '代理:API2D', value: 2, color: 'blue' },
|
||||
{ key: 5, text: '代理:OpenAI-SB', value: 5, color: 'brown' },
|
||||
{ key: 7, text: '代理:OhMyGPT', value: 7, color: 'purple' },
|
||||
{ key: 10, text: '代理:AI Proxy', value: 10, color: 'purple' },
|
||||
{ key: 4, text: '代理:CloseAI', value: 4, color: 'teal' },
|
||||
{ key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet' },
|
||||
{ key: 9, text: '代理:AI.LS', value: 9, color: 'yellow' },
|
||||
{ key: 12, text: '代理:API2GPT', value: 12, color: 'blue' },
|
||||
{ key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' },
|
||||
];
|
||||
|
@ -1,7 +1,7 @@
|
||||
export const toastConstants = {
|
||||
SUCCESS_TIMEOUT: 1500,
|
||||
INFO_TIMEOUT: 3000,
|
||||
ERROR_TIMEOUT: 5000,
|
||||
SUCCESS_TIMEOUT: 5000,
|
||||
INFO_TIMEOUT: 8000,
|
||||
ERROR_TIMEOUT: 10000,
|
||||
WARNING_TIMEOUT: 10000,
|
||||
NOTICE_TIMEOUT: 20000
|
||||
NOTICE_TIMEOUT: 20000,
|
||||
};
|
||||
|
13
web/default/src/helpers/helper.js
Normal file
13
web/default/src/helpers/helper.js
Normal file
@ -0,0 +1,13 @@
|
||||
import {CHANNEL_OPTIONS} from '../constants';
|
||||
|
||||
let channelMap = undefined;
|
||||
|
||||
export function getChannelOption(channelId) {
|
||||
if (channelMap === undefined) {
|
||||
channelMap = {};
|
||||
CHANNEL_OPTIONS.forEach((option) => {
|
||||
channelMap[option.key] = option;
|
||||
});
|
||||
}
|
||||
return channelMap[channelId];
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
import { Label } from 'semantic-ui-react';
|
||||
import { Label, Message } from 'semantic-ui-react';
|
||||
import { getChannelOption } from './helper';
|
||||
import React from 'react';
|
||||
|
||||
export function renderText(text, limit) {
|
||||
if (text.length > limit) {
|
||||
@ -14,7 +16,15 @@ export function renderGroup(group) {
|
||||
let groups = group.split(',');
|
||||
groups.sort();
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: '2px',
|
||||
rowGap: '6px',
|
||||
}}
|
||||
>
|
||||
{groups.map((group) => {
|
||||
if (group === 'vip' || group === 'pro') {
|
||||
return <Label color='yellow'>{group}</Label>;
|
||||
@ -23,7 +33,7 @@ export function renderGroup(group) {
|
||||
}
|
||||
return <Label>{group}</Label>;
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -39,23 +49,33 @@ export function renderNumber(num) {
|
||||
}
|
||||
}
|
||||
|
||||
export function renderQuota(quota, digits = 2) {
|
||||
let quotaPerUnit = localStorage.getItem('quota_per_unit');
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
quotaPerUnit = parseFloat(quotaPerUnit);
|
||||
displayInCurrency = displayInCurrency === 'true';
|
||||
export function renderQuota(quota, t, precision = 2) {
|
||||
const displayInCurrency =
|
||||
localStorage.getItem('display_in_currency') === 'true';
|
||||
const quotaPerUnit = parseFloat(
|
||||
localStorage.getItem('quota_per_unit') || '1'
|
||||
);
|
||||
|
||||
if (displayInCurrency) {
|
||||
return '$' + (quota / quotaPerUnit).toFixed(digits);
|
||||
const amount = (quota / quotaPerUnit).toFixed(precision);
|
||||
return t('common.quota.display_short', { amount });
|
||||
}
|
||||
|
||||
return renderNumber(quota);
|
||||
}
|
||||
|
||||
export function renderQuotaWithPrompt(quota, digits) {
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
displayInCurrency = displayInCurrency === 'true';
|
||||
export function renderQuotaWithPrompt(quota, t) {
|
||||
const displayInCurrency =
|
||||
localStorage.getItem('display_in_currency') === 'true';
|
||||
const quotaPerUnit = parseFloat(
|
||||
localStorage.getItem('quota_per_unit') || '1'
|
||||
);
|
||||
|
||||
if (displayInCurrency) {
|
||||
return `(等价金额:${renderQuota(quota, digits)})`;
|
||||
const amount = (quota / quotaPerUnit).toFixed(2);
|
||||
return ` (${t('common.quota.display', { amount })})`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -87,3 +107,15 @@ export function renderColorLabel(text) {
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderChannelTip(channelId) {
|
||||
let channel = getChannelOption(channelId);
|
||||
if (channel === undefined || channel.tip === undefined) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<Message>
|
||||
<div dangerouslySetInnerHTML={{ __html: channel.tip }}></div>
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { toast } from 'react-toastify';
|
||||
import { toastConstants } from '../constants';
|
||||
import {toast} from 'react-toastify';
|
||||
import {toastConstants} from '../constants';
|
||||
import React from 'react';
|
||||
import { API } from './api';
|
||||
import {API} from './api';
|
||||
|
||||
const HTMLToastContent = ({ htmlContent }) => {
|
||||
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
|
||||
@ -74,6 +74,7 @@ if (isMobile()) {
|
||||
}
|
||||
|
||||
export function showError(error) {
|
||||
if (!error) return;
|
||||
console.error(error);
|
||||
if (error.message) {
|
||||
if (error.name === 'AxiosError') {
|
||||
@ -158,17 +159,7 @@ export function timestamp2string(timestamp) {
|
||||
second = '0' + second;
|
||||
}
|
||||
return (
|
||||
year +
|
||||
'-' +
|
||||
month +
|
||||
'-' +
|
||||
day +
|
||||
' ' +
|
||||
hour +
|
||||
':' +
|
||||
minute +
|
||||
':' +
|
||||
second
|
||||
year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second
|
||||
);
|
||||
}
|
||||
|
||||
@ -193,7 +184,6 @@ export const verifyJSON = (str) => {
|
||||
export function shouldShowPrompt(id) {
|
||||
let prompt = localStorage.getItem(`prompt-${id}`);
|
||||
return !prompt;
|
||||
|
||||
}
|
||||
|
||||
export function setPromptShown(id) {
|
||||
|
28
web/default/src/i18n.js
Normal file
28
web/default/src/i18n.js
Normal file
@ -0,0 +1,28 @@
|
||||
import i18n from 'i18next';
|
||||
import {initReactI18next} from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import zhTranslation from './locales/zh/translation.json';
|
||||
import enTranslation from './locales/en/translation.json';
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: 'zh',
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
||||
resources: {
|
||||
zh: {
|
||||
translation: zhTranslation
|
||||
},
|
||||
en: {
|
||||
translation: enTranslation
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
@ -33,3 +33,85 @@ code {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.ui.container {
|
||||
width: 100% !important;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
|
||||
.ui.card,
|
||||
.ui.cards,
|
||||
.ui.segment {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.ui.table {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏笔记本 (13-14寸) */
|
||||
@media screen and (min-width: 769px) and (max-width: 1366px) {
|
||||
.ui.container {
|
||||
width: auto !important;
|
||||
max-width: 100% !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
padding: 0 24px !important;
|
||||
}
|
||||
|
||||
/* 调整表格显示 */
|
||||
.ui.table {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* 调整卡片布局 */
|
||||
.ui.cards {
|
||||
margin-left: -0.5em !important;
|
||||
margin-right: -0.5em !important;
|
||||
}
|
||||
|
||||
.ui.cards > .card {
|
||||
margin: 0.5em !important;
|
||||
width: calc(50% - 1em) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大屏幕 */
|
||||
@media screen and (min-width: 1367px) {
|
||||
.ui.container {
|
||||
width: 1200px !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 优化 Dashboard 网格布局 */
|
||||
@media screen and (max-width: 1366px) {
|
||||
.charts-grid {
|
||||
margin: 0 -0.5em !important;
|
||||
}
|
||||
|
||||
.charts-grid .column {
|
||||
padding: 0.5em !important;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* 调整字体大小 */
|
||||
.ui.header {
|
||||
font-size: 1.1em !important;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 0.9em !important;
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
831
web/default/src/locales/en/translation.json
Normal file
831
web/default/src/locales/en/translation.json
Normal file
@ -0,0 +1,831 @@
|
||||
{
|
||||
"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",
|
||||
"balance_not_supported": "-"
|
||||
},
|
||||
"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!",
|
||||
"operation_success": "Operation completed successfully!"
|
||||
},
|
||||
"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",
|
||||
"proxy_url": "Proxy",
|
||||
"proxy_url_placeholder": "This is optional and used for API calls via a proxy. Please enter the proxy URL, formatted as: https://domain.com",
|
||||
"base_url": "Base URL",
|
||||
"base_url_placeholder": "The Base URL required by the OpenAPI SDK",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"title": "Token Management",
|
||||
"search": "Search tokens by name ...",
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"used_quota": "Used Quota",
|
||||
"remain_quota": "Remaining Quota",
|
||||
"created_time": "Created Time",
|
||||
"expired_time": "Expiry Time",
|
||||
"actions": "Actions",
|
||||
"no_name": "None",
|
||||
"never_expire": "never",
|
||||
"unlimited": "Unlimited",
|
||||
"status_enabled": "Enabled",
|
||||
"status_disabled": "Disabled",
|
||||
"status_expired": "Expired",
|
||||
"status_depleted": "Depleted",
|
||||
"status_unknown": "Unknown Status"
|
||||
},
|
||||
"buttons": {
|
||||
"copy": "Copy",
|
||||
"chat": "Chat",
|
||||
"delete": "Delete",
|
||||
"confirm_delete": "Delete Token",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"edit": "Edit",
|
||||
"add": "Add New Token",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"edit": {
|
||||
"title_edit": "Update Token Information",
|
||||
"title_create": "Create New Token",
|
||||
"name": "Name",
|
||||
"name_placeholder": "Please enter name",
|
||||
"models": "Model Scope",
|
||||
"models_placeholder": "Please select allowed models, leave empty for no restrictions",
|
||||
"ip_limit": "IP Restriction",
|
||||
"ip_limit_placeholder": "Please enter allowed subnets, e.g.: 192.168.0.0/24, use commas to separate multiple subnets",
|
||||
"expire_time": "Expiry Time",
|
||||
"expire_time_placeholder": "Please enter expiry time in yyyy-MM-dd HH:mm:ss format, -1 for no limit",
|
||||
"quota_notice": "Note: Token quota only limits the maximum usage of the token itself, actual usage is subject to account remaining quota.",
|
||||
"quota": "Quota",
|
||||
"quota_placeholder": "Please enter quota",
|
||||
"buttons": {
|
||||
"never_expire": "Never Expire",
|
||||
"expire_1_month": "Expire in 1 Month",
|
||||
"expire_1_day": "Expire in 1 Day",
|
||||
"expire_1_hour": "Expire in 1 Hour",
|
||||
"expire_1_minute": "Expire in 1 Minute",
|
||||
"unlimited_quota": "Set Unlimited Quota",
|
||||
"cancel_unlimited": "Cancel Unlimited Quota",
|
||||
"submit": "Submit",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"messages": {
|
||||
"update_success": "Token updated successfully!",
|
||||
"create_success": "Token created successfully, please copy it from the list page!",
|
||||
"expire_time_invalid": "Invalid expiry time format!"
|
||||
}
|
||||
},
|
||||
"copy_options": {
|
||||
"raw": "Copy Raw Token",
|
||||
"ama": "Copy AMA Link",
|
||||
"opencat": "Copy OpenCat Link",
|
||||
"next": "Copy NextChat Link",
|
||||
"lobe": "Copy LobeChat Link"
|
||||
},
|
||||
"messages": {
|
||||
"copy_success": "Copied to clipboard!",
|
||||
"copy_failed": "Unable to copy to clipboard, please copy manually. Token has been filled in the search box.",
|
||||
"operation_success": "Operation completed successfully!"
|
||||
},
|
||||
"sort": {
|
||||
"placeholder": "Sort By",
|
||||
"default": "Default Order",
|
||||
"by_remain": "Sort by Remaining Quota",
|
||||
"by_used": "Sort by Used Quota"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"quota": {
|
||||
"display": "Equivalent: ${{amount}}",
|
||||
"display_short": "${{amount}}",
|
||||
"unit": "$"
|
||||
}
|
||||
},
|
||||
"redemption": {
|
||||
"title": "Redemption Management",
|
||||
"search": "Search redemption codes by ID and name ...",
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"name": "Name",
|
||||
"status": "Status",
|
||||
"quota": "Quota",
|
||||
"created_time": "Created Time",
|
||||
"redeemed_time": "Redeemed Time",
|
||||
"actions": "Actions",
|
||||
"no_name": "None",
|
||||
"not_redeemed": "Not Redeemed"
|
||||
},
|
||||
"buttons": {
|
||||
"copy": "Copy",
|
||||
"delete": "Delete",
|
||||
"confirm_delete": "Confirm Delete",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"edit": "Edit",
|
||||
"add": "Add New Code",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"status": {
|
||||
"unused": "Unused",
|
||||
"disabled": "Disabled",
|
||||
"used": "Used",
|
||||
"unknown": "Unknown"
|
||||
},
|
||||
"edit": {
|
||||
"title_edit": "Update Redemption Code",
|
||||
"title_create": "Create New Redemption Code",
|
||||
"name": "Name",
|
||||
"name_placeholder": "Please enter name",
|
||||
"quota": "Quota",
|
||||
"quota_placeholder": "Please enter quota per redemption code",
|
||||
"count": "Generate Count",
|
||||
"count_placeholder": "Please enter number of codes to generate",
|
||||
"buttons": {
|
||||
"submit": "Submit",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"update_success": "Redemption code updated successfully!",
|
||||
"create_success": "Redemption code created successfully!"
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"title": "Operation Log",
|
||||
"search": "Search logs...",
|
||||
"usage_details": "Usage Details",
|
||||
"total_quota": "Total Quota Used",
|
||||
"click_to_view": "Click to View",
|
||||
"type": {
|
||||
"select": "Select Log Type",
|
||||
"all": "All",
|
||||
"topup": "Top Up",
|
||||
"usage": "Usage",
|
||||
"admin": "Admin",
|
||||
"system": "System",
|
||||
"test": "Test"
|
||||
},
|
||||
"table": {
|
||||
"time": "Time",
|
||||
"channel": "Channel",
|
||||
"type": "Type",
|
||||
"model": "Model",
|
||||
"username": "Username",
|
||||
"token_name": "Token Name",
|
||||
"token_name_placeholder": "Optional",
|
||||
"model_name": "Model Name",
|
||||
"model_name_placeholder": "Optional",
|
||||
"start_time": "Start Time",
|
||||
"end_time": "End Time",
|
||||
"channel_id": "Channel ID",
|
||||
"channel_id_placeholder": "Optional",
|
||||
"username_placeholder": "Optional",
|
||||
"prompt_tokens": "Prompt Tokens",
|
||||
"completion_tokens": "Completion Tokens",
|
||||
"quota": "Quota",
|
||||
"detail": "Detail"
|
||||
},
|
||||
"buttons": {
|
||||
"query": "Action",
|
||||
"submit": "Query",
|
||||
"refresh": "Refresh"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"title": "User Management",
|
||||
"edit": {
|
||||
"title": "Update User Information",
|
||||
"username": "Username",
|
||||
"username_placeholder": "Please enter new username",
|
||||
"password": "Password",
|
||||
"password_placeholder": "Please enter new password, minimum 8 characters",
|
||||
"display_name": "Display Name",
|
||||
"display_name_placeholder": "Please enter new display name",
|
||||
"group": "Group",
|
||||
"group_placeholder": "Please select group",
|
||||
"group_addition": "Please edit group multipliers in system settings to add new group:",
|
||||
"quota": "Remaining Quota",
|
||||
"quota_placeholder": "Please enter new remaining quota",
|
||||
"github_id": "Linked GitHub Account",
|
||||
"github_id_placeholder": "Read-only, user must link through personal settings page, cannot be modified directly",
|
||||
"wechat_id": "Linked WeChat Account",
|
||||
"wechat_id_placeholder": "Read-only, user must link through personal settings page, cannot be modified directly",
|
||||
"email": "Linked Email Account",
|
||||
"email_placeholder": "Read-only, user must link through personal settings page, cannot be modified directly",
|
||||
"buttons": {
|
||||
"submit": "Submit",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
},
|
||||
"add": {
|
||||
"title": "Create New User Account"
|
||||
},
|
||||
"messages": {
|
||||
"update_success": "User information updated successfully!",
|
||||
"create_success": "User account created successfully!",
|
||||
"operation_success": "Operation completed successfully!"
|
||||
},
|
||||
"search": "Search users...",
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"username": "Username",
|
||||
"group": "Group",
|
||||
"quota": "Quota",
|
||||
"role_text": "Role",
|
||||
"status_text": "Status",
|
||||
"actions": "Actions",
|
||||
"remaining_quota": "Remaining Quota",
|
||||
"used_quota": "Used Quota",
|
||||
"request_count": "Request Count",
|
||||
"role_types": {
|
||||
"normal": "Normal User",
|
||||
"admin": "Admin",
|
||||
"super_admin": "Super Admin",
|
||||
"unknown": "Unknown Role"
|
||||
},
|
||||
"status_types": {
|
||||
"activated": "Activated",
|
||||
"banned": "Banned",
|
||||
"unknown": "Unknown Status"
|
||||
},
|
||||
"sort": {
|
||||
"default": "Default Order",
|
||||
"by_quota": "Sort by Remaining Quota",
|
||||
"by_used_quota": "Sort by Used Quota",
|
||||
"by_request_count": "Sort by Request Count"
|
||||
},
|
||||
"sort_by": "Sort By"
|
||||
},
|
||||
"buttons": {
|
||||
"add": "Add New User",
|
||||
"delete": "Delete",
|
||||
"delete_user": "Delete User",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"edit": "Edit",
|
||||
"promote": "Promote",
|
||||
"demote": "Demote"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"charts": {
|
||||
"requests": {
|
||||
"title": "Model Request Trend",
|
||||
"tooltip": "Request Count"
|
||||
},
|
||||
"quota": {
|
||||
"title": "Quota Usage Trend",
|
||||
"tooltip": "Quota Used"
|
||||
},
|
||||
"tokens": {
|
||||
"title": "Token Usage Trend",
|
||||
"tooltip": "Token Count"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
"title": "Statistics",
|
||||
"tooltip": {
|
||||
"date": "Date",
|
||||
"value": "Value"
|
||||
}
|
||||
}
|
||||
},
|
||||
"setting": {
|
||||
"title": "System Settings",
|
||||
"tabs": {
|
||||
"personal": "Personal Settings",
|
||||
"operation": "Operation Settings",
|
||||
"system": "System Settings",
|
||||
"other": "Other Settings"
|
||||
},
|
||||
"personal": {
|
||||
"general": {
|
||||
"title": "General Settings",
|
||||
"system_token_notice": "Note: The token generated here is for system management, not for requesting OpenAI related services.",
|
||||
"buttons": {
|
||||
"update_profile": "Update Profile",
|
||||
"generate_token": "Generate System Token",
|
||||
"copy_invite": "Copy Invite Link",
|
||||
"delete_account": "Delete Account"
|
||||
}
|
||||
},
|
||||
"binding": {
|
||||
"title": "Account Binding",
|
||||
"buttons": {
|
||||
"bind_wechat": "Bind WeChat Account",
|
||||
"bind_github": "Bind GitHub Account",
|
||||
"bind_email": "Bind Email Address",
|
||||
"bind_lark": "Bind Lark Account"
|
||||
},
|
||||
"wechat": {
|
||||
"title": "WeChat Binding",
|
||||
"description": "Scan QR code to follow the official account, enter 'verification code' to get the code (valid for 3 minutes)",
|
||||
"verification_code": "Verification Code",
|
||||
"bind": "Bind"
|
||||
},
|
||||
"email": {
|
||||
"title": "Bind Email Address",
|
||||
"email_placeholder": "Enter email address",
|
||||
"code_placeholder": "Verification code",
|
||||
"get_code": "Get Code",
|
||||
"get_code_retry": "Resend({{countdown}})",
|
||||
"bind": "Confirm Binding",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
},
|
||||
"delete_account": {
|
||||
"title": "Dangerous Operation",
|
||||
"warning": "You are deleting your account. All data will be cleared and cannot be recovered",
|
||||
"confirm_placeholder": "Enter your username {{username}} to confirm deletion",
|
||||
"buttons": {
|
||||
"confirm": "Confirm Delete",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"general": {
|
||||
"title": "General Settings",
|
||||
"server_address": "Server Address",
|
||||
"server_address_placeholder": "e.g.: https://yourdomain.com",
|
||||
"buttons": {
|
||||
"update": "Update Server Address"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"title": "Login & Registration Settings",
|
||||
"password_login": "Allow Password Login",
|
||||
"password_register": "Allow Password Registration",
|
||||
"email_verification": "Require Email Verification for Password Registration",
|
||||
"github_oauth": "Allow GitHub OAuth Login & Registration",
|
||||
"wechat_login": "Allow WeChat Login & Registration",
|
||||
"registration": "Allow New User Registration (When disabled, new users cannot register by any means)",
|
||||
"turnstile": "Enable Turnstile User Verification"
|
||||
},
|
||||
"email_restriction": {
|
||||
"title": "Email Domain Whitelist",
|
||||
"subtitle": "Used to prevent malicious users from batch registering using temporary emails",
|
||||
"enable": "Enable Email Domain Whitelist",
|
||||
"allowed_domains": "Allowed Email Domains",
|
||||
"add_domain": "Add New Allowed Email Domain",
|
||||
"add_domain_placeholder": "Enter new allowed email domain",
|
||||
"buttons": {
|
||||
"fill": "Fill",
|
||||
"save": "Save Email Domain Whitelist Settings"
|
||||
}
|
||||
},
|
||||
"smtp": {
|
||||
"title": "SMTP Configuration",
|
||||
"subtitle": "Used to support system email sending",
|
||||
"server": "SMTP Server Address",
|
||||
"server_placeholder": "e.g.: smtp.gmail.com",
|
||||
"port": "SMTP Port",
|
||||
"port_placeholder": "Default: 587",
|
||||
"account": "SMTP Account",
|
||||
"account_placeholder": "Usually your email address",
|
||||
"from": "SMTP Sender Email",
|
||||
"from_placeholder": "Usually same as email address",
|
||||
"token": "SMTP Access Token",
|
||||
"token_placeholder": "Sensitive information will not be sent to frontend",
|
||||
"buttons": {
|
||||
"save": "Save SMTP Settings"
|
||||
}
|
||||
},
|
||||
"github": {
|
||||
"title": "GitHub OAuth App Configuration",
|
||||
"subtitle": "Used to support GitHub login and registration",
|
||||
"manage_link": "Click here",
|
||||
"manage_text": "to manage your GitHub OAuth Apps",
|
||||
"url_notice": "Set Homepage URL to {{server_url}}, and Authorization callback URL to {{callback_url}}",
|
||||
"client_id": "GitHub Client ID",
|
||||
"client_id_placeholder": "Enter your registered GitHub OAuth APP ID",
|
||||
"client_secret": "GitHub Client Secret",
|
||||
"client_secret_placeholder": "Sensitive information will not be sent to frontend",
|
||||
"buttons": {
|
||||
"save": "Save GitHub OAuth Settings"
|
||||
}
|
||||
},
|
||||
"lark": {
|
||||
"title": "Lark OAuth Configuration",
|
||||
"subtitle": "Used to support Lark login and registration",
|
||||
"manage_link": "Click here",
|
||||
"manage_text": "to manage your Lark applications",
|
||||
"url_notice": "Set Homepage URL to {{server_url}}, and Redirect URL to {{callback_url}}",
|
||||
"client_id": "App ID",
|
||||
"client_id_placeholder": "Enter App ID",
|
||||
"client_secret": "App Secret",
|
||||
"client_secret_placeholder": "Sensitive information will not be sent to frontend",
|
||||
"buttons": {
|
||||
"save": "Save Lark OAuth Settings"
|
||||
}
|
||||
},
|
||||
"wechat": {
|
||||
"title": "WeChat Server Configuration",
|
||||
"subtitle": "Used to support WeChat login and registration",
|
||||
"learn_more": "Learn about WeChat Server",
|
||||
"server_address": "WeChat Server Address",
|
||||
"server_address_placeholder": "e.g.: https://yourdomain.com",
|
||||
"token": "WeChat Server Access Token",
|
||||
"token_placeholder": "Sensitive information will not be sent to frontend",
|
||||
"qrcode": "WeChat Official Account QR Code Image URL",
|
||||
"qrcode_placeholder": "Enter an image URL",
|
||||
"buttons": {
|
||||
"save": "Save WeChat Server Settings"
|
||||
},
|
||||
"scan_tip": "Scan QR code to follow WeChat Official Account, enter 'code' to get verification code (valid for 3 minutes)",
|
||||
"code_placeholder": "Verification code"
|
||||
},
|
||||
"turnstile": {
|
||||
"title": "Turnstile Configuration",
|
||||
"subtitle": "Used to support user verification",
|
||||
"manage_link": "Click here",
|
||||
"manage_text": "to manage your Turnstile Sites, Invisible Widget Type recommended",
|
||||
"site_key": "Turnstile Site Key",
|
||||
"site_key_placeholder": "Enter your registered Turnstile Site Key",
|
||||
"secret_key": "Turnstile Secret Key",
|
||||
"secret_key_placeholder": "Sensitive information will not be sent to frontend",
|
||||
"buttons": {
|
||||
"save": "Save Turnstile Settings"
|
||||
}
|
||||
},
|
||||
"password_login": {
|
||||
"warning": {
|
||||
"title": "Warning",
|
||||
"content": "Disabling password login will prevent all users (including administrators) who haven't bound other login methods from logging in via password. Confirm disable?",
|
||||
"buttons": {
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"operation": {
|
||||
"quota": {
|
||||
"title": "Quota Settings",
|
||||
"new_user": "Initial Quota for New Users",
|
||||
"new_user_placeholder": "e.g.: 100",
|
||||
"pre_consume": "Pre-consumed Quota per Request",
|
||||
"pre_consume_placeholder": "Refund or charge difference after request",
|
||||
"inviter_reward": "Reward Quota for Inviter",
|
||||
"inviter_reward_placeholder": "e.g.: 2000",
|
||||
"invitee_reward": "Reward Quota for Using Invite Code",
|
||||
"invitee_reward_placeholder": "e.g.: 1000",
|
||||
"buttons": {
|
||||
"save": "Save Quota Settings"
|
||||
}
|
||||
},
|
||||
"ratio": {
|
||||
"title": "Ratio Settings",
|
||||
"model": {
|
||||
"title": "Model Ratio",
|
||||
"placeholder": "A JSON text where keys are model names and values are ratios"
|
||||
},
|
||||
"completion": {
|
||||
"title": "Completion Ratio",
|
||||
"placeholder": "A JSON text where keys are model names and values are ratios. These ratios are the proportion of completion to prompt ratio, which can override One API's internal ratios"
|
||||
},
|
||||
"group": {
|
||||
"title": "Group Ratio",
|
||||
"placeholder": "A JSON text where keys are group names and values are ratios"
|
||||
},
|
||||
"buttons": {
|
||||
"save": "Save Ratio Settings"
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"title": "Log Settings",
|
||||
"enable_consume": "Enable Quota Consumption Logging",
|
||||
"target_time": "Target Time",
|
||||
"buttons": {
|
||||
"clean": "Clean Historical Logs"
|
||||
}
|
||||
},
|
||||
"monitor": {
|
||||
"title": "Monitor Settings",
|
||||
"max_response_time": "Maximum Response Time",
|
||||
"max_response_time_placeholder": "In seconds, channels exceeding this time during testing will be automatically disabled",
|
||||
"quota_reminder": "Quota Reminder Threshold",
|
||||
"quota_reminder_placeholder": "Users will receive email reminders when quota falls below this value",
|
||||
"auto_disable": "Automatically Disable Channel on Failure",
|
||||
"auto_enable": "Automatically Enable Channel on Success",
|
||||
"buttons": {
|
||||
"save": "Save Monitor Settings"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"title": "General Settings",
|
||||
"topup_link": "Top-up Link",
|
||||
"topup_link_placeholder": "e.g.: Card selling website purchase link",
|
||||
"chat_link": "Chat Page Link",
|
||||
"chat_link_placeholder": "e.g.: ChatGPT Next Web deployment address",
|
||||
"quota_per_unit": "Quota per Dollar",
|
||||
"quota_per_unit_placeholder": "Quota exchangeable per unit of currency",
|
||||
"retry_times": "Retry Times on Failure",
|
||||
"retry_times_placeholder": "Number of retry attempts on failure",
|
||||
"display_in_currency": "Display Quota in Currency Format",
|
||||
"display_token_stat": "Show Token Quota Instead of User Quota in Billing APIs",
|
||||
"approximate_token": "Use Approximate Method to Estimate Token Count",
|
||||
"buttons": {
|
||||
"save": "Save General Settings"
|
||||
}
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"notice": {
|
||||
"title": "Notice Settings",
|
||||
"content": "Notice Content",
|
||||
"content_placeholder": "Enter new notice content here, supports Markdown & HTML code",
|
||||
"buttons": {
|
||||
"save": "Save Notice"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"title": "System Settings",
|
||||
"name": "System Name",
|
||||
"name_placeholder": "Please enter system name",
|
||||
"logo": "Logo Image URL",
|
||||
"logo_placeholder": "Enter Logo image URL here",
|
||||
"theme": {
|
||||
"title": "Theme Name",
|
||||
"link": "Available Themes",
|
||||
"placeholder": "Please enter theme name"
|
||||
},
|
||||
"buttons": {
|
||||
"save_name": "Set System Name",
|
||||
"save_logo": "Set Logo",
|
||||
"save_theme": "Set Theme (Restart Required)"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"title": "Content Settings",
|
||||
"homepage": {
|
||||
"title": "Homepage Content",
|
||||
"placeholder": "Enter homepage content here, supports Markdown & HTML code. Status information will not be shown after setting. If a link is entered, it will be used as the src attribute of an iframe, allowing you to set any webpage as homepage."
|
||||
},
|
||||
"about": {
|
||||
"title": "About System",
|
||||
"description": "You can set about content in settings page, supports HTML & Markdown",
|
||||
"repository": "Project Repository:",
|
||||
"loading_failed": "Failed to load about content..."
|
||||
},
|
||||
"footer": {
|
||||
"title": "Footer",
|
||||
"placeholder": "Enter new footer here, leave empty to use default footer, supports HTML code"
|
||||
},
|
||||
"buttons": {
|
||||
"save_homepage": "Save Homepage Content",
|
||||
"save_about": "Save About",
|
||||
"save_footer": "Set Footer"
|
||||
}
|
||||
},
|
||||
"copyright": {
|
||||
"notice": "Removing One API's copyright notice requires authorization. Project maintenance requires significant effort, if this project is meaningful to you, please actively support it."
|
||||
}
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"built_by": "built by",
|
||||
"built_by_name": "JustSong",
|
||||
"license": ", source code is licensed under the",
|
||||
"mit": "MIT License"
|
||||
},
|
||||
"home": {
|
||||
"welcome": {
|
||||
"title": "Welcome to One API",
|
||||
"description": "One API is a LLM API management and distribution system that helps you better manage and use LLM APIs from various providers.",
|
||||
"login_notice": "To use the service, please login or register first."
|
||||
},
|
||||
"system_status": {
|
||||
"title": "System Status",
|
||||
"info": {
|
||||
"title": "System Information",
|
||||
"name": "Name: ",
|
||||
"version": "Version: ",
|
||||
"source": "Source: ",
|
||||
"source_link": "GitHub Repository",
|
||||
"start_time": "Start Time: "
|
||||
},
|
||||
"config": {
|
||||
"title": "System Configuration",
|
||||
"email_verify": "Email Verification: ",
|
||||
"github_oauth": "GitHub OAuth: ",
|
||||
"wechat_login": "WeChat Login: ",
|
||||
"turnstile": "Turnstile Check: ",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
}
|
||||
},
|
||||
"loading_failed": "Failed to load homepage content..."
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"title": "User Login",
|
||||
"username": "Username / Email",
|
||||
"password": "Password",
|
||||
"button": "Login",
|
||||
"forgot_password": "Forgot password?",
|
||||
"reset_password": "Reset",
|
||||
"no_account": "No account?",
|
||||
"register": "Register",
|
||||
"other_methods": "Other login methods",
|
||||
"wechat": {
|
||||
"scan_tip": "Scan QR code to follow WeChat Official Account, enter 'code' to get verification code (valid for 3 minutes)",
|
||||
"code_placeholder": "Verification code"
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
"title": "New User Registration",
|
||||
"username": "Username (max 12 characters)",
|
||||
"password": "Password (8-20 characters)",
|
||||
"confirm_password": "Confirm password",
|
||||
"email": "Email address",
|
||||
"verification_code": "Verification code",
|
||||
"get_code": "Get code",
|
||||
"get_code_retry": "Retry ({{countdown}})",
|
||||
"button": "Register",
|
||||
"has_account": "Have an account?",
|
||||
"login": "Login"
|
||||
},
|
||||
"reset": {
|
||||
"title": "Password Reset",
|
||||
"email": "Email address",
|
||||
"button": "Submit",
|
||||
"notice": "The system will send an email containing a reset link to your mailbox. Please check your email.",
|
||||
"confirm": {
|
||||
"title": "Password Reset Confirmation",
|
||||
"new_password": "New password",
|
||||
"button": "Submit",
|
||||
"button_disabled": "Password reset completed",
|
||||
"notice": "New password has been generated, please click the password field or button above to copy. Please login and change your password as soon as possible!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"description": "One API is an open-source API management and proxy platform.",
|
||||
"repository": "Repository: ",
|
||||
"loading_failed": "Loading failed"
|
||||
},
|
||||
"messages": {
|
||||
"success": {
|
||||
"login": "Login successful!",
|
||||
"register": "Registration successful!",
|
||||
"verification_code": "Verification code sent, please check your email!",
|
||||
"password_reset": "Reset email sent, please check your inbox!"
|
||||
},
|
||||
"error": {
|
||||
"login_expired": "Not logged in or session expired, please login again!",
|
||||
"password_length": "Password must be at least 8 characters!",
|
||||
"password_mismatch": "Passwords do not match",
|
||||
"turnstile_wait": "Please wait a few seconds, Turnstile is checking the environment!",
|
||||
"root_password": "Please change the default password immediately!"
|
||||
},
|
||||
"notice": {
|
||||
"password_copied": "New password copied to clipboard: {{password}}"
|
||||
}
|
||||
}
|
||||
}
|
827
web/default/src/locales/zh/translation.json
Normal file
827
web/default/src/locales/zh/translation.json
Normal file
@ -0,0 +1,827 @@
|
||||
{
|
||||
"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": "点击更新",
|
||||
"balance_not_supported": "-"
|
||||
},
|
||||
"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": "已更新完毕所有已启用渠道余额!",
|
||||
"operation_success": "操作成功完成!"
|
||||
},
|
||||
"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": "此项可选,用于强制设置给定的系统提示词,请配合自定义模型 & 模型重定向使用,首先创建一个唯一的自定义模型名称并在上面填入,之后将该自定义模型重定向映射到该渠道一个原生支持的模型",
|
||||
"proxy_url": "代理",
|
||||
"proxy_url_placeholder": "此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com。注意,这里所需要填入的代理地址仅会在实际请求时替换域名部分,如果你想填入 OpenAI SDK 中所要求的 Base URL,请使用 OpenAI 兼容渠道类型",
|
||||
"base_url": "Base URL",
|
||||
"base_url_placeholder": "OpenAPI SDK 中所要求的 Base URL",
|
||||
"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`。你可以结合模型重定向功能将其转换为常规的模型名称,例如:doubao-lite-4k -> ep-20240608051426-tkxvl(前者作为 JSON 的 key,后者作为 value)。注意,doubao-lite-4k 和 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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"title": "令牌管理",
|
||||
"search": "搜索令牌的名称 ...",
|
||||
"table": {
|
||||
"name": "名称",
|
||||
"status": "状态",
|
||||
"used_quota": "已用额度",
|
||||
"remain_quota": "剩余额度",
|
||||
"created_time": "创建时间",
|
||||
"expired_time": "过期时间",
|
||||
"actions": "操作",
|
||||
"no_name": "无",
|
||||
"never_expire": "永不过期",
|
||||
"unlimited": "无限制",
|
||||
"status_enabled": "已启用",
|
||||
"status_disabled": "已禁用",
|
||||
"status_expired": "已过期",
|
||||
"status_depleted": "已耗尽",
|
||||
"status_unknown": "未知状态"
|
||||
},
|
||||
"buttons": {
|
||||
"copy": "复制",
|
||||
"chat": "聊天",
|
||||
"delete": "删除",
|
||||
"confirm_delete": "删除令牌",
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
"edit": "编辑",
|
||||
"add": "添加新的令牌",
|
||||
"refresh": "刷新"
|
||||
},
|
||||
"edit": {
|
||||
"title_edit": "更新令牌信息",
|
||||
"title_create": "创建新的令牌",
|
||||
"name": "名称",
|
||||
"name_placeholder": "请输入名称",
|
||||
"models": "模型范围",
|
||||
"models_placeholder": "请选择允许使用的模型,留空则不进行限制",
|
||||
"ip_limit": "IP 限制",
|
||||
"ip_limit_placeholder": "请输入允许访问的网段,例如:192.168.0.0/24,请使用英文逗号分隔多个网段",
|
||||
"expire_time": "过期时间",
|
||||
"expire_time_placeholder": "请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制",
|
||||
"quota_notice": "注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。",
|
||||
"quota": "额度",
|
||||
"quota_placeholder": "请输入额度",
|
||||
"buttons": {
|
||||
"never_expire": "永不过期",
|
||||
"expire_1_month": "一个月后过期",
|
||||
"expire_1_day": "一天后过期",
|
||||
"expire_1_hour": "一小时后过期",
|
||||
"expire_1_minute": "一分钟后过期",
|
||||
"unlimited_quota": "设为无限额度",
|
||||
"cancel_unlimited": "取消无限额度",
|
||||
"submit": "提交",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"messages": {
|
||||
"update_success": "令牌更新成功!",
|
||||
"create_success": "令牌创建成功,请在列表页面点击复制获取令牌!",
|
||||
"expire_time_invalid": "过期时间格式错误!"
|
||||
}
|
||||
},
|
||||
"copy_options": {
|
||||
"raw": "复制原始令牌",
|
||||
"ama": "复制 AMA 链接",
|
||||
"opencat": "复制 OpenCat 链接",
|
||||
"next": "复制 NextChat 链接",
|
||||
"lobe": "复制 LobeChat 链接"
|
||||
},
|
||||
"messages": {
|
||||
"copy_success": "已复制到剪贴板!",
|
||||
"copy_failed": "无法复制到剪贴板,请手动复制,已将令牌填入搜索框。",
|
||||
"operation_success": "操作成功完成!"
|
||||
},
|
||||
"sort": {
|
||||
"placeholder": "排序方式",
|
||||
"default": "默认排序",
|
||||
"by_remain": "按剩余额度排序",
|
||||
"by_used": "按已用额度排序"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"quota": {
|
||||
"display": "等价金额:${{amount}}",
|
||||
"display_short": "${{amount}}",
|
||||
"unit": "$"
|
||||
}
|
||||
},
|
||||
"redemption": {
|
||||
"title": "兑换管理",
|
||||
"search": "搜索兑换码的 ID 和名称 ...",
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"name": "名称",
|
||||
"status": "状态",
|
||||
"quota": "额度",
|
||||
"created_time": "创建时间",
|
||||
"redeemed_time": "兑换时间",
|
||||
"actions": "操作",
|
||||
"no_name": "无",
|
||||
"not_redeemed": "尚未兑换"
|
||||
},
|
||||
"buttons": {
|
||||
"copy": "复制",
|
||||
"delete": "删除",
|
||||
"confirm_delete": "确认删除",
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
"edit": "编辑",
|
||||
"add": "添加新的兑换码",
|
||||
"refresh": "刷新"
|
||||
},
|
||||
"status": {
|
||||
"unused": "未使用",
|
||||
"disabled": "已禁用",
|
||||
"used": "已使用",
|
||||
"unknown": "未知状态"
|
||||
},
|
||||
"edit": {
|
||||
"title_edit": "更新兑换码信息",
|
||||
"title_create": "创建新的兑换码",
|
||||
"name": "名称",
|
||||
"name_placeholder": "请输入名称",
|
||||
"quota": "额度",
|
||||
"quota_placeholder": "请输入单个兑换码中包含的额度",
|
||||
"count": "生成数量",
|
||||
"count_placeholder": "请输入生成数量",
|
||||
"buttons": {
|
||||
"submit": "提交",
|
||||
"cancel": "取消"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"update_success": "兑换码更新成功!",
|
||||
"create_success": "兑换码创建成功!"
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"title": "操作日志",
|
||||
"search": "搜索日志...",
|
||||
"usage_details": "使用明细",
|
||||
"total_quota": "总消耗额度",
|
||||
"click_to_view": "点击查看",
|
||||
"type": {
|
||||
"select": "选择明细分类",
|
||||
"all": "全部",
|
||||
"topup": "充值",
|
||||
"usage": "消费",
|
||||
"admin": "管理",
|
||||
"system": "系统",
|
||||
"test": "测试"
|
||||
},
|
||||
"table": {
|
||||
"time": "时间",
|
||||
"channel": "渠道",
|
||||
"type": "类型",
|
||||
"model": "模型",
|
||||
"username": "用户名",
|
||||
"token_name": "令牌名称",
|
||||
"token_name_placeholder": "可选值",
|
||||
"model_name": "模型名称",
|
||||
"model_name_placeholder": "可选值",
|
||||
"start_time": "起始时间",
|
||||
"end_time": "结束时间",
|
||||
"channel_id": "渠道 ID",
|
||||
"channel_id_placeholder": "可选值",
|
||||
"username_placeholder": "可选值",
|
||||
"prompt_tokens": "提示词消耗",
|
||||
"completion_tokens": "补全消耗",
|
||||
"quota": "额度",
|
||||
"detail": "详情"
|
||||
},
|
||||
"buttons": {
|
||||
"query": "操作",
|
||||
"submit": "查询",
|
||||
"refresh": "刷新"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"title": "用户管理",
|
||||
"edit": {
|
||||
"title": "更新用户信息",
|
||||
"username": "用户名",
|
||||
"username_placeholder": "请输入新的用户名",
|
||||
"password": "密码",
|
||||
"password_placeholder": "请输入新的密码,最短 8 位",
|
||||
"display_name": "显示名称",
|
||||
"display_name_placeholder": "请输入新的显示名称",
|
||||
"group": "分组",
|
||||
"group_placeholder": "请选择分组",
|
||||
"group_addition": "请在系统设置页面编辑分组倍率以添加新的分组:",
|
||||
"quota": "剩余额度",
|
||||
"quota_placeholder": "请输入新的剩余额度",
|
||||
"github_id": "已绑定的 GitHub 账户",
|
||||
"github_id_placeholder": "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改",
|
||||
"wechat_id": "已绑定的微信账户",
|
||||
"wechat_id_placeholder": "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改",
|
||||
"email": "已绑定的邮箱账户",
|
||||
"email_placeholder": "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改",
|
||||
"buttons": {
|
||||
"submit": "提交",
|
||||
"cancel": "取消"
|
||||
}
|
||||
},
|
||||
"add": {
|
||||
"title": "创建新用户账户"
|
||||
},
|
||||
"messages": {
|
||||
"update_success": "用户信息更新成功!",
|
||||
"create_success": "用户账户创建成功!",
|
||||
"operation_success": "操作成功完成!"
|
||||
},
|
||||
"search": "搜索用户...",
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"username": "用户名",
|
||||
"group": "分组",
|
||||
"quota": "额度",
|
||||
"role_text": "角色",
|
||||
"status_text": "状态",
|
||||
"actions": "操作",
|
||||
"remaining_quota": "剩余额度",
|
||||
"used_quota": "已用额度",
|
||||
"request_count": "请求次数",
|
||||
"role_types": {
|
||||
"normal": "普通用户",
|
||||
"admin": "管理员",
|
||||
"super_admin": "超级管理员",
|
||||
"unknown": "未知身份"
|
||||
},
|
||||
"status_types": {
|
||||
"activated": "已激活",
|
||||
"banned": "已封禁",
|
||||
"unknown": "未知状态"
|
||||
},
|
||||
"sort": {
|
||||
"default": "默认排序",
|
||||
"by_quota": "按剩余额度排序",
|
||||
"by_used_quota": "按已用额度排序",
|
||||
"by_request_count": "按请求次数排序"
|
||||
},
|
||||
"sort_by": "排序方式"
|
||||
},
|
||||
"buttons": {
|
||||
"add": "添加新的用户",
|
||||
"delete": "删除",
|
||||
"delete_user": "删除用户",
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
"edit": "编辑",
|
||||
"promote": "提升",
|
||||
"demote": "降级"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"charts": {
|
||||
"requests": {
|
||||
"title": "模型请求趋势",
|
||||
"tooltip": "请求次数"
|
||||
},
|
||||
"quota": {
|
||||
"title": "额度消费趋势",
|
||||
"tooltip": "消费额度"
|
||||
},
|
||||
"tokens": {
|
||||
"title": "Token 消费趋势",
|
||||
"tooltip": "Token 数量"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
"title": "统计",
|
||||
"tooltip": {
|
||||
"date": "日期",
|
||||
"value": "数值"
|
||||
}
|
||||
}
|
||||
},
|
||||
"setting": {
|
||||
"title": "系统设置",
|
||||
"tabs": {
|
||||
"personal": "个人设置",
|
||||
"operation": "运营设置",
|
||||
"system": "系统设置",
|
||||
"other": "其他设置"
|
||||
},
|
||||
"personal": {
|
||||
"general": {
|
||||
"title": "通用设置",
|
||||
"system_token_notice": "注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI 相关的服务,请知悉。",
|
||||
"buttons": {
|
||||
"update_profile": "更新个人信息",
|
||||
"generate_token": "生成系统访问令牌",
|
||||
"copy_invite": "复制邀请链接",
|
||||
"delete_account": "删除个人账户"
|
||||
}
|
||||
},
|
||||
"binding": {
|
||||
"title": "账号绑定",
|
||||
"buttons": {
|
||||
"bind_wechat": "绑定微信账号",
|
||||
"bind_github": "绑定 GitHub 账号",
|
||||
"bind_email": "绑定邮箱地址",
|
||||
"bind_lark": "绑定飞书账号"
|
||||
},
|
||||
"wechat": {
|
||||
"title": "微信绑定",
|
||||
"description": "微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)",
|
||||
"verification_code": "验证码",
|
||||
"bind": "绑定"
|
||||
},
|
||||
"email": {
|
||||
"title": "绑定邮箱地址",
|
||||
"email_placeholder": "输入邮箱地址",
|
||||
"code_placeholder": "验证码",
|
||||
"get_code": "获取验证码",
|
||||
"get_code_retry": "重新发送({{countdown}})",
|
||||
"bind": "确认绑定",
|
||||
"cancel": "取消"
|
||||
}
|
||||
},
|
||||
"delete_account": {
|
||||
"title": "危险操作",
|
||||
"warning": "您正在删除自己的帐户,将清空所有数据且不可恢复",
|
||||
"confirm_placeholder": "输入你的账户名 {{username}} 以确认删除",
|
||||
"buttons": {
|
||||
"confirm": "确认删除",
|
||||
"cancel": "取消"
|
||||
}
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"general": {
|
||||
"title": "通用设置",
|
||||
"server_address": "服务器地址",
|
||||
"server_address_placeholder": "例如:https://yourdomain.com",
|
||||
"buttons": {
|
||||
"update": "更新服务器地址"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"title": "配置登录注册",
|
||||
"password_login": "允许通过密码进行登录",
|
||||
"password_register": "允许通过密码进行注册",
|
||||
"email_verification": "通过密码注册时需要进行邮箱验证",
|
||||
"github_oauth": "允许通过 GitHub 账户登录 & 注册",
|
||||
"wechat_login": "允许通过微信登录 & 注册",
|
||||
"registration": "允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)",
|
||||
"turnstile": "启用 Turnstile 用户校验"
|
||||
},
|
||||
"email_restriction": {
|
||||
"title": "配置邮箱域名白名单",
|
||||
"subtitle": "用以防止恶意用户利用临时邮箱批量注册",
|
||||
"enable": "启用邮箱域名白名单",
|
||||
"allowed_domains": "允许的邮箱域名",
|
||||
"add_domain": "添加新的允许的邮箱域名",
|
||||
"add_domain_placeholder": "输入新的允许的邮箱域名",
|
||||
"buttons": {
|
||||
"fill": "填入",
|
||||
"save": "保存邮箱域名白名单设置"
|
||||
}
|
||||
},
|
||||
"smtp": {
|
||||
"title": "配置 SMTP",
|
||||
"subtitle": "用以支持系统的邮件发送",
|
||||
"server": "SMTP 服务器地址",
|
||||
"server_placeholder": "例如:smtp.qq.com",
|
||||
"port": "SMTP 端口",
|
||||
"port_placeholder": "默认: 587",
|
||||
"account": "SMTP 账户",
|
||||
"account_placeholder": "通常是邮箱地址",
|
||||
"from": "SMTP 发送者邮箱",
|
||||
"from_placeholder": "通常和邮箱地址保持一致",
|
||||
"token": "SMTP 访问凭证",
|
||||
"token_placeholder": "敏感信息不会发送到前端显示",
|
||||
"buttons": {
|
||||
"save": "保存 SMTP 设置"
|
||||
}
|
||||
},
|
||||
"github": {
|
||||
"title": "配置 GitHub OAuth App",
|
||||
"subtitle": "用以支持通过 GitHub 进行登录注册",
|
||||
"manage_link": "点击此处",
|
||||
"manage_text": "管理你的 GitHub OAuth App",
|
||||
"url_notice": "Homepage URL 填 {{server_url}},Authorization callback URL 填 {{callback_url}}",
|
||||
"client_id": "GitHub Client ID",
|
||||
"client_id_placeholder": "输入你注册的 GitHub OAuth APP 的 ID",
|
||||
"client_secret": "GitHub Client Secret",
|
||||
"client_secret_placeholder": "敏感信息不会发送到前端显示",
|
||||
"buttons": {
|
||||
"save": "保存 GitHub OAuth 设置"
|
||||
}
|
||||
},
|
||||
"lark": {
|
||||
"title": "配置飞书授权登录",
|
||||
"subtitle": "用以支持通过飞书进行登录注册",
|
||||
"manage_link": "点击此处",
|
||||
"manage_text": "管理你的飞书应用",
|
||||
"url_notice": "主页链接填 {{server_url}},重定向 URL 填 {{callback_url}}",
|
||||
"client_id": "App ID",
|
||||
"client_id_placeholder": "输入 App ID",
|
||||
"client_secret": "App Secret",
|
||||
"client_secret_placeholder": "敏感信息不会发送到前端显示",
|
||||
"buttons": {
|
||||
"save": "保存飞书 OAuth 设置"
|
||||
}
|
||||
},
|
||||
"wechat": {
|
||||
"title": "配置 WeChat Server",
|
||||
"subtitle": "用以支持通过微信进行登录注册",
|
||||
"learn_more": "了解 WeChat Server",
|
||||
"server_address": "WeChat Server 服务器地址",
|
||||
"server_address_placeholder": "例如:https://yourdomain.com",
|
||||
"token": "WeChat Server 访问凭证",
|
||||
"token_placeholder": "敏感信息不会发送到前端显示",
|
||||
"qrcode": "微信公众号二维码图片链接",
|
||||
"qrcode_placeholder": "输入一个图片链接",
|
||||
"buttons": {
|
||||
"save": "保存 WeChat Server 设置"
|
||||
}
|
||||
},
|
||||
"turnstile": {
|
||||
"title": "配置 Turnstile",
|
||||
"subtitle": "用以支持用户校验",
|
||||
"manage_link": "点击此处",
|
||||
"manage_text": "管理你的 Turnstile Sites,推荐选择 Invisible Widget Type",
|
||||
"site_key": "Turnstile Site Key",
|
||||
"site_key_placeholder": "输入你注册的 Turnstile Site Key",
|
||||
"secret_key": "Turnstile Secret Key",
|
||||
"secret_key_placeholder": "敏感信息不会发送到前端显示",
|
||||
"buttons": {
|
||||
"save": "保存 Turnstile 设置"
|
||||
}
|
||||
},
|
||||
"password_login": {
|
||||
"warning": {
|
||||
"title": "警告",
|
||||
"content": "取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?",
|
||||
"buttons": {
|
||||
"confirm": "确定",
|
||||
"cancel": "取消"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"operation": {
|
||||
"quota": {
|
||||
"title": "额度设置",
|
||||
"new_user": "新用户初始额度",
|
||||
"new_user_placeholder": "例如:100",
|
||||
"pre_consume": "请求预扣费额度",
|
||||
"pre_consume_placeholder": "请求结束后多退少补",
|
||||
"inviter_reward": "邀请新用户奖励额度",
|
||||
"inviter_reward_placeholder": "例如:2000",
|
||||
"invitee_reward": "新用户使用邀请码奖励额度",
|
||||
"invitee_reward_placeholder": "例如:1000",
|
||||
"buttons": {
|
||||
"save": "保存额度设置"
|
||||
}
|
||||
},
|
||||
"ratio": {
|
||||
"title": "倍率设置",
|
||||
"model": {
|
||||
"title": "模型倍率",
|
||||
"placeholder": "为一个 JSON 文本,键为模型名称,值为倍率"
|
||||
},
|
||||
"completion": {
|
||||
"title": "补全倍率",
|
||||
"placeholder": "为一个 JSON 文本,键为模型名称,值为倍率,此处的倍率设置是模型补全倍率相较于提示倍率的比例,使用该设置可强制覆盖 One API 的内部比例"
|
||||
},
|
||||
"group": {
|
||||
"title": "分组倍率",
|
||||
"placeholder": "为一个 JSON 文本,键为分组名称,值为倍率"
|
||||
},
|
||||
"buttons": {
|
||||
"save": "保存倍率设置"
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"title": "日志设置",
|
||||
"enable_consume": "启用额度消费日志记录",
|
||||
"target_time": "目标时间",
|
||||
"buttons": {
|
||||
"clean": "清理历史日志"
|
||||
}
|
||||
},
|
||||
"monitor": {
|
||||
"title": "监控设置",
|
||||
"max_response_time": "最长响应时间",
|
||||
"max_response_time_placeholder": "单位秒,当运行渠道全部测试时,超过此时间将自动禁用渠道",
|
||||
"quota_reminder": "额度提醒阈值",
|
||||
"quota_reminder_placeholder": "低于此额度时将发送邮件提醒用户",
|
||||
"auto_disable": "失败时自动禁用渠道",
|
||||
"auto_enable": "成功时自动启用渠道",
|
||||
"buttons": {
|
||||
"save": "保存监控设置"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"title": "通用设置",
|
||||
"topup_link": "充值链接",
|
||||
"topup_link_placeholder": "例如发卡网站的购买链接",
|
||||
"chat_link": "聊天页面链接",
|
||||
"chat_link_placeholder": "例如 ChatGPT Next Web 的部署地址",
|
||||
"quota_per_unit": "单位美元额度",
|
||||
"quota_per_unit_placeholder": "一单位货币能兑换的额度",
|
||||
"retry_times": "失败重试次数",
|
||||
"retry_times_placeholder": "失败重试次数",
|
||||
"display_in_currency": "以货币形式显示额度",
|
||||
"display_token_stat": "Billing 相关 API 显示令牌额度而非用户额度",
|
||||
"approximate_token": "使用近似的方式估算 token 数以减少计算量",
|
||||
"buttons": {
|
||||
"save": "保存通用设置"
|
||||
}
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
"notice": {
|
||||
"title": "公告设置",
|
||||
"content": "公告内容",
|
||||
"content_placeholder": "在此输入新的公告内容,支持 Markdown & HTML 代码",
|
||||
"buttons": {
|
||||
"save": "保存公告"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"title": "系统设置",
|
||||
"name": "系统名称",
|
||||
"name_placeholder": "请输入系统名称",
|
||||
"logo": "Logo 图片地址",
|
||||
"logo_placeholder": "在此输入 Logo 图片地址",
|
||||
"theme": {
|
||||
"title": "主题名称",
|
||||
"link": "当前可用主题",
|
||||
"placeholder": "请输入主题名称"
|
||||
},
|
||||
"buttons": {
|
||||
"save_name": "设置系统名称",
|
||||
"save_logo": "设置 Logo",
|
||||
"save_theme": "设置主题(重启生效)"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"title": "内容设置",
|
||||
"homepage": {
|
||||
"title": "首页内容",
|
||||
"placeholder": "在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。"
|
||||
},
|
||||
"about": {
|
||||
"title": "关于",
|
||||
"placeholder": "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。"
|
||||
},
|
||||
"footer": {
|
||||
"title": "页脚",
|
||||
"placeholder": "在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码"
|
||||
},
|
||||
"buttons": {
|
||||
"save_homepage": "保存首页内容",
|
||||
"save_about": "保存关于",
|
||||
"save_footer": "设置页脚"
|
||||
}
|
||||
},
|
||||
"copyright": {
|
||||
"notice": "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "关于",
|
||||
"description": "One API 是一个开源的接口管理和代理平台。",
|
||||
"repository": "项目地址:",
|
||||
"loading_failed": "加载失败"
|
||||
},
|
||||
"footer": {
|
||||
"built_by": "由",
|
||||
"built_by_name": "JustSong",
|
||||
"license": "构建,源代码遵循",
|
||||
"mit": "MIT 协议"
|
||||
},
|
||||
"home": {
|
||||
"welcome": {
|
||||
"title": "欢迎使用 One API",
|
||||
"description": "One API 是一个 LLM API 接口管理和分发系统,可以帮助您更好地管理和使用各大厂商的 LLM API。",
|
||||
"login_notice": "如需使用,请先登录或注册。"
|
||||
},
|
||||
"system_status": {
|
||||
"title": "系统状况",
|
||||
"info": {
|
||||
"title": "系统信息",
|
||||
"name": "名称:",
|
||||
"version": "版本:",
|
||||
"source": "源码:",
|
||||
"source_link": "GitHub 仓库",
|
||||
"start_time": "启动时间:"
|
||||
},
|
||||
"config": {
|
||||
"title": "系统配置",
|
||||
"email_verify": "邮箱验证:",
|
||||
"github_oauth": "GitHub 身份验证:",
|
||||
"wechat_login": "微信身份验证:",
|
||||
"turnstile": "Turnstile 校验:",
|
||||
"enabled": "已启用",
|
||||
"disabled": "未启用"
|
||||
}
|
||||
},
|
||||
"loading_failed": "加载首页内容失败..."
|
||||
},
|
||||
"auth": {
|
||||
"login": {
|
||||
"title": "用户登录",
|
||||
"username": "用户名 / 邮箱地址",
|
||||
"password": "密码",
|
||||
"button": "登录",
|
||||
"forgot_password": "忘记密码?",
|
||||
"reset_password": "点击重置",
|
||||
"no_account": "没有账户?",
|
||||
"register": "点击注册",
|
||||
"other_methods": "使用其他方式登录",
|
||||
"wechat": {
|
||||
"scan_tip": "微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)",
|
||||
"code_placeholder": "验证码"
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
"title": "新用户注册",
|
||||
"username": "输入用户名,最长 12 位",
|
||||
"password": "输入密码,最短 8 位,最长 20 位",
|
||||
"confirm_password": "再次输入密码",
|
||||
"email": "输入邮箱地址",
|
||||
"verification_code": "输入验证码",
|
||||
"get_code": "获取验证码",
|
||||
"get_code_retry": "重试 ({{countdown}})",
|
||||
"button": "注册",
|
||||
"has_account": "已有账户?",
|
||||
"login": "点击登录"
|
||||
},
|
||||
"reset": {
|
||||
"title": "密码重置",
|
||||
"email": "邮箱地址",
|
||||
"button": "提交",
|
||||
"notice": "系统将向您的邮箱发送一封包含重置链接的邮件,请注意查收。",
|
||||
"confirm": {
|
||||
"title": "密码重置确认",
|
||||
"new_password": "新密码",
|
||||
"button": "提交",
|
||||
"button_disabled": "密码重置完成",
|
||||
"notice": "新密码已生成,请点击密码框或上方按钮复制。请及时登录并修改密码!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"success": {
|
||||
"login": "登录成功!",
|
||||
"register": "注册成功!",
|
||||
"verification_code": "验证码发送成功,请检查你的邮箱!",
|
||||
"password_reset": "重置邮件发送成功,请检查邮箱!"
|
||||
},
|
||||
"error": {
|
||||
"login_expired": "未登录或登录已过期,请重新登录!",
|
||||
"password_length": "密码长度不得小于 8 位!",
|
||||
"password_mismatch": "两次输入的密码不一致",
|
||||
"turnstile_wait": "请稍后几秒重试,Turnstile 正在检查用户环境!",
|
||||
"root_password": "请立刻修改默认密码!"
|
||||
},
|
||||
"notice": {
|
||||
"password_copied": "新密码已复制到剪贴板:{{password}}"
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user