mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-10 10:33:41 +08:00
Compare commits
11 Commits
v0.4.5-alp
...
v0.4.7-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f55647278c | ||
|
|
03c05bdb5f | ||
|
|
aeb1cad679 | ||
|
|
8a4cd403fd | ||
|
|
9ac5410d06 | ||
|
|
7edc2b5376 | ||
|
|
d4869dfad2 | ||
|
|
4463224f04 | ||
|
|
ad1049b0cf | ||
|
|
d0c454c78e | ||
|
|
fe135fd508 |
57
.github/workflows/docker-image-amd64-en.yml
vendored
Normal file
57
.github/workflows/docker-image-amd64-en.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
name: Publish Docker image (amd64)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
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: Save version info
|
||||||
|
run: |
|
||||||
|
git describe --tags > VERSION
|
||||||
|
|
||||||
|
- name: Translate
|
||||||
|
run: |
|
||||||
|
python ./i18n/translate.py --repository_path . --json_file_path ./i18n/en.json
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
justsong/one-api-en
|
||||||
|
ghcr.io/one-api-en
|
||||||
|
|
||||||
|
- name: Build and push Docker images
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
11
README.md
11
README.md
@@ -41,6 +41,8 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
|
|||||||
·
|
·
|
||||||
<a href="https://github.com/songquanpeng/one-api#常见问题">常见问题</a>
|
<a href="https://github.com/songquanpeng/one-api#常见问题">常见问题</a>
|
||||||
·
|
·
|
||||||
|
<a href="https://github.com/songquanpeng/one-api#相关项目">相关项目</a>
|
||||||
|
·
|
||||||
<a href="https://iamazing.cn/page/reward">赞赏支持</a>
|
<a href="https://iamazing.cn/page/reward">赞赏支持</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -250,6 +252,12 @@ graph LR
|
|||||||
+ 例子:`SYNC_FREQUENCY=60`
|
+ 例子:`SYNC_FREQUENCY=60`
|
||||||
6. `NODE_TYPE`:设置之后将指定节点类型,可选值为 `master` 和 `slave`,未设置则默认为 `master`。
|
6. `NODE_TYPE`:设置之后将指定节点类型,可选值为 `master` 和 `slave`,未设置则默认为 `master`。
|
||||||
+ 例子:`NODE_TYPE=slave`
|
+ 例子:`NODE_TYPE=slave`
|
||||||
|
7. `CHANNEL_UPDATE_FREQUENCY`:设置之后将定期更新渠道余额,单位为分钟,未设置则不进行更新。
|
||||||
|
+ 例子:`CHANNEL_UPDATE_FREQUENCY=1440`
|
||||||
|
8. `CHANNEL_TEST_FREQUENCY`:设置之后将定期检查渠道,单位为分钟,未设置则不进行检查。
|
||||||
|
+ 例子:`CHANNEL_TEST_FREQUENCY=1440`
|
||||||
|
9. `REQUEST_INTERVAL`:批量更新渠道余额以及测试可用性时的请求间隔,单位为秒,默认无间隔。
|
||||||
|
+ 例子:`POLLING_INTERVAL=5`
|
||||||
|
|
||||||
### 命令行参数
|
### 命令行参数
|
||||||
1. `--port <port_number>`: 指定服务器监听的端口号,默认为 `3000`。
|
1. `--port <port_number>`: 指定服务器监听的端口号,默认为 `3000`。
|
||||||
@@ -286,6 +294,9 @@ https://openai.justsong.cn
|
|||||||
+ 部署的时候不要设置 `BASE_URL`。
|
+ 部署的时候不要设置 `BASE_URL`。
|
||||||
+ 检查你的接口地址和 API Key 有没有填对。
|
+ 检查你的接口地址和 API Key 有没有填对。
|
||||||
|
|
||||||
|
## 相关项目
|
||||||
|
[FastGPT](https://github.com/c121914yu/FastGPT): 三分钟搭建 AI 知识库
|
||||||
|
|
||||||
## 注意
|
## 注意
|
||||||
本项目为开源项目,请在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
|
本项目为开源项目,请在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,7 +18,8 @@ var Logo = ""
|
|||||||
var TopUpLink = ""
|
var TopUpLink = ""
|
||||||
var ChatLink = ""
|
var ChatLink = ""
|
||||||
var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
|
var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
|
||||||
var DisplayInCurrencyEnabled = false
|
var DisplayInCurrencyEnabled = true
|
||||||
|
var DisplayTokenStatEnabled = true
|
||||||
|
|
||||||
var UsingSQLite = false
|
var UsingSQLite = false
|
||||||
|
|
||||||
@@ -70,6 +72,9 @@ var RootUserEmail = ""
|
|||||||
|
|
||||||
var IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
|
var IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
|
||||||
|
|
||||||
|
var requestInterval, _ = strconv.Atoi(os.Getenv("REQUEST_INTERVAL"))
|
||||||
|
var RequestInterval = time.Duration(requestInterval) * time.Second
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RoleGuestUser = 0
|
RoleGuestUser = 0
|
||||||
RoleCommonUser = 1
|
RoleCommonUser = 1
|
||||||
|
|||||||
@@ -7,8 +7,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func GetSubscription(c *gin.Context) {
|
func GetSubscription(c *gin.Context) {
|
||||||
userId := c.GetInt("id")
|
var quota int
|
||||||
quota, err := model.GetUserQuota(userId)
|
var err error
|
||||||
|
var token *model.Token
|
||||||
|
if common.DisplayTokenStatEnabled {
|
||||||
|
tokenId := c.GetInt("token_id")
|
||||||
|
token, err = model.GetTokenById(tokenId)
|
||||||
|
quota = token.RemainQuota
|
||||||
|
} else {
|
||||||
|
userId := c.GetInt("id")
|
||||||
|
quota, err = model.GetUserQuota(userId)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
openAIError := OpenAIError{
|
openAIError := OpenAIError{
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
@@ -35,8 +44,17 @@ func GetSubscription(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetUsage(c *gin.Context) {
|
func GetUsage(c *gin.Context) {
|
||||||
userId := c.GetInt("id")
|
var quota int
|
||||||
quota, err := model.GetUserUsedQuota(userId)
|
var err error
|
||||||
|
var token *model.Token
|
||||||
|
if common.DisplayTokenStatEnabled {
|
||||||
|
tokenId := c.GetInt("token_id")
|
||||||
|
token, err = model.GetTokenById(tokenId)
|
||||||
|
quota = token.UsedQuota
|
||||||
|
} else {
|
||||||
|
userId := c.GetInt("id")
|
||||||
|
quota, err = model.GetUserUsedQuota(userId)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
openAIError := OpenAIError{
|
openAIError := OpenAIError{
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ func updateAllChannelsBalance() error {
|
|||||||
disableChannel(channel.Id, channel.Name, "余额不足")
|
disableChannel(channel.Id, channel.Name, "余额不足")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
time.Sleep(common.RequestInterval)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -277,3 +278,12 @@ func UpdateAllChannelsBalance(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AutomaticallyUpdateChannels(frequency int) {
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Duration(frequency) * time.Minute)
|
||||||
|
common.SysLog("updating all channels")
|
||||||
|
_ = updateAllChannelsBalance()
|
||||||
|
common.SysLog("channels update done")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,10 +62,9 @@ func testChannel(channel *model.Channel, request ChatRequest) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTestRequest(c *gin.Context) *ChatRequest {
|
func buildTestRequest() *ChatRequest {
|
||||||
model_ := c.Query("model")
|
|
||||||
testRequest := &ChatRequest{
|
testRequest := &ChatRequest{
|
||||||
Model: model_,
|
Model: "", // this will be set later
|
||||||
MaxTokens: 1,
|
MaxTokens: 1,
|
||||||
}
|
}
|
||||||
testMessage := Message{
|
testMessage := Message{
|
||||||
@@ -93,7 +92,7 @@ func TestChannel(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
testRequest := buildTestRequest(c)
|
testRequest := buildTestRequest()
|
||||||
tik := time.Now()
|
tik := time.Now()
|
||||||
err = testChannel(channel, *testRequest)
|
err = testChannel(channel, *testRequest)
|
||||||
tok := time.Now()
|
tok := time.Now()
|
||||||
@@ -133,7 +132,7 @@ func disableChannel(channelId int, channelName string, reason string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAllChannels(c *gin.Context) error {
|
func testAllChannels(notify bool) error {
|
||||||
if common.RootUserEmail == "" {
|
if common.RootUserEmail == "" {
|
||||||
common.RootUserEmail = model.GetRootUserEmail()
|
common.RootUserEmail = model.GetRootUserEmail()
|
||||||
}
|
}
|
||||||
@@ -146,13 +145,9 @@ func testAllChannels(c *gin.Context) error {
|
|||||||
testAllChannelsLock.Unlock()
|
testAllChannelsLock.Unlock()
|
||||||
channels, err := model.GetAllChannels(0, 0, true)
|
channels, err := model.GetAllChannels(0, 0, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
testRequest := buildTestRequest(c)
|
testRequest := buildTestRequest()
|
||||||
var disableThreshold = int64(common.ChannelDisableThreshold * 1000)
|
var disableThreshold = int64(common.ChannelDisableThreshold * 1000)
|
||||||
if disableThreshold == 0 {
|
if disableThreshold == 0 {
|
||||||
disableThreshold = 10000000 // a impossible value
|
disableThreshold = 10000000 // a impossible value
|
||||||
@@ -173,20 +168,23 @@ func testAllChannels(c *gin.Context) error {
|
|||||||
disableChannel(channel.Id, channel.Name, err.Error())
|
disableChannel(channel.Id, channel.Name, err.Error())
|
||||||
}
|
}
|
||||||
channel.UpdateResponseTime(milliseconds)
|
channel.UpdateResponseTime(milliseconds)
|
||||||
}
|
time.Sleep(common.RequestInterval)
|
||||||
err := common.SendEmail("通道测试完成", common.RootUserEmail, "通道测试完成,如果没有收到禁用通知,说明所有通道都正常")
|
|
||||||
if err != nil {
|
|
||||||
common.SysError(fmt.Sprintf("failed to send email: %s", err.Error()))
|
|
||||||
}
|
}
|
||||||
testAllChannelsLock.Lock()
|
testAllChannelsLock.Lock()
|
||||||
testAllChannelsRunning = false
|
testAllChannelsRunning = false
|
||||||
testAllChannelsLock.Unlock()
|
testAllChannelsLock.Unlock()
|
||||||
|
if notify {
|
||||||
|
err := common.SendEmail("通道测试完成", common.RootUserEmail, "通道测试完成,如果没有收到禁用通知,说明所有通道都正常")
|
||||||
|
if err != nil {
|
||||||
|
common.SysError(fmt.Sprintf("failed to send email: %s", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAllChannels(c *gin.Context) {
|
func TestAllChannels(c *gin.Context) {
|
||||||
err := testAllChannels(c)
|
err := testAllChannels(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -200,3 +198,12 @@ func TestAllChannels(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AutomaticallyTestChannels(frequency int) {
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Duration(frequency) * time.Minute)
|
||||||
|
common.SysLog("testing all channels")
|
||||||
|
_ = testAllChannels(false)
|
||||||
|
common.SysLog("channel test finished")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func GetOptions(c *gin.Context) {
|
|||||||
var options []*model.Option
|
var options []*model.Option
|
||||||
common.OptionMapRWMutex.Lock()
|
common.OptionMapRWMutex.Lock()
|
||||||
for k, v := range common.OptionMap {
|
for k, v := range common.OptionMap {
|
||||||
if strings.Contains(k, "Token") || strings.Contains(k, "Secret") {
|
if strings.HasSuffix(k, "Token") || strings.HasSuffix(k, "Secret") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
options = append(options, &model.Option{
|
options = append(options, &model.Option{
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
preConsumedQuota := int(float64(preConsumedTokens) * ratio)
|
preConsumedQuota := int(float64(preConsumedTokens) * ratio)
|
||||||
userQuota, err := model.CacheGetUserQuota(userId)
|
userQuota, err := model.CacheGetUserQuota(userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "get_user_quota_failed", http.StatusOK)
|
return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
if userQuota > 10*preConsumedQuota {
|
if userQuota > 10*preConsumedQuota {
|
||||||
// in this case, we do not pre-consume quota
|
// in this case, we do not pre-consume quota
|
||||||
@@ -86,12 +86,12 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
if consumeQuota && preConsumedQuota > 0 {
|
if consumeQuota && preConsumedQuota > 0 {
|
||||||
err := model.PreConsumeTokenQuota(tokenId, preConsumedQuota)
|
err := model.PreConsumeTokenQuota(tokenId, preConsumedQuota)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "pre_consume_token_quota_failed", http.StatusOK)
|
return errorWrapper(err, "pre_consume_token_quota_failed", http.StatusForbidden)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest(c.Request.Method, fullRequestURL, c.Request.Body)
|
req, err := http.NewRequest(c.Request.Method, fullRequestURL, c.Request.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "new_request_failed", http.StatusOK)
|
return errorWrapper(err, "new_request_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
if channelType == common.ChannelTypeAzure {
|
if channelType == common.ChannelTypeAzure {
|
||||||
key := c.Request.Header.Get("Authorization")
|
key := c.Request.Header.Get("Authorization")
|
||||||
@@ -106,15 +106,15 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "do_request_failed", http.StatusOK)
|
return errorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
err = req.Body.Close()
|
err = req.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "close_request_body_failed", http.StatusOK)
|
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
err = c.Request.Body.Close()
|
err = c.Request.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "close_request_body_failed", http.StatusOK)
|
return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
var textResponse TextResponse
|
var textResponse TextResponse
|
||||||
isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
|
isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
|
||||||
@@ -224,22 +224,22 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
})
|
})
|
||||||
err = resp.Body.Close()
|
err = resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "close_response_body_failed", http.StatusOK)
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
if consumeQuota {
|
if consumeQuota {
|
||||||
responseBody, err := io.ReadAll(resp.Body)
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "read_response_body_failed", http.StatusOK)
|
return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
err = resp.Body.Close()
|
err = resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "close_response_body_failed", http.StatusOK)
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(responseBody, &textResponse)
|
err = json.Unmarshal(responseBody, &textResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusOK)
|
return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
if textResponse.Error.Type != "" {
|
if textResponse.Error.Type != "" {
|
||||||
return &OpenAIErrorWithStatusCode{
|
return &OpenAIErrorWithStatusCode{
|
||||||
@@ -260,11 +260,11 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
|||||||
c.Writer.WriteHeader(resp.StatusCode)
|
c.Writer.WriteHeader(resp.StatusCode)
|
||||||
_, err = io.Copy(c.Writer, resp.Body)
|
_, err = io.Copy(c.Writer, resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "copy_response_body_failed", http.StatusOK)
|
return errorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
err = resp.Body.Close()
|
err = resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorWrapper(err, "close_response_body_failed", http.StatusOK)
|
return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ func RelayNotImplemented(c *gin.Context) {
|
|||||||
Param: "",
|
Param: "",
|
||||||
Code: "api_not_implemented",
|
Code: "api_not_implemented",
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusNotImplemented, gin.H{
|
||||||
"error": err,
|
"error": err,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -147,7 +147,7 @@ func RelayNotFound(c *gin.Context) {
|
|||||||
Param: "",
|
Param: "",
|
||||||
Code: "api_not_found",
|
Code: "api_not_found",
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
"error": err,
|
"error": err,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
419
i18n/en.json
Normal file
419
i18n/en.json
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
{
|
||||||
|
"$%.6f 额度": "$%.6f quota",
|
||||||
|
"%d 点额度": "%d point quota",
|
||||||
|
"尚未实现": "Not yet implemented",
|
||||||
|
"余额不足": "Insufficient balance",
|
||||||
|
"\"通道「%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, or upgrade your account to improve service quality.",
|
||||||
|
"令牌名称长度必须在1-20之间": "The length of the token name must be between 1-20",
|
||||||
|
"令牌已过期,无法启用,请先修改令牌过期时间": "The token has expired and cannot be enabled. Please modify the token expiration time first",
|
||||||
|
"令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "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",
|
||||||
|
"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",
|
||||||
|
"未测试": "Not tested",
|
||||||
|
"通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test succeeded, time consumed ${time.toFixed(2)} s.",
|
||||||
|
"已成功开始测试所有已启用通道,请刷新页面查看结果。": "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 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",
|
||||||
|
"在此输入新的公告内容": "Enter new announcement content here",
|
||||||
|
"保存公告": "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",
|
||||||
|
"错误": "Error",
|
||||||
|
"错误:未登录或登录已过期,请重新登录": "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 administrator",
|
||||||
|
"本站仅作演示之用,无服务端": "This site is for demonstration purposes only, no server-side",
|
||||||
|
"错误:": "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 ",
|
||||||
|
"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",
|
||||||
|
"由{' '}": "build by{' '}",
|
||||||
|
"构建,源代码遵循{' '}": ", the source code licensed under{' '}",
|
||||||
|
"MIT 协议": "MIT License",
|
||||||
|
"充值额度": "Recharge Quota",
|
||||||
|
"获取兑换码": "Get Redeem Code"
|
||||||
|
}
|
||||||
61
i18n/translate.py
Normal file
61
i18n/translate.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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)
|
||||||
15
main.go
15
main.go
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/gin-contrib/sessions/redis"
|
"github.com/gin-contrib/sessions/redis"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"one-api/controller"
|
||||||
"one-api/middleware"
|
"one-api/middleware"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"one-api/router"
|
"one-api/router"
|
||||||
@@ -59,6 +60,20 @@ func main() {
|
|||||||
go model.SyncChannelCache(frequency)
|
go model.SyncChannelCache(frequency)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if os.Getenv("CHANNEL_UPDATE_FREQUENCY") != "" {
|
||||||
|
frequency, err := strconv.Atoi(os.Getenv("CHANNEL_UPDATE_FREQUENCY"))
|
||||||
|
if err != nil {
|
||||||
|
common.FatalLog("failed to parse CHANNEL_UPDATE_FREQUENCY: " + err.Error())
|
||||||
|
}
|
||||||
|
go controller.AutomaticallyUpdateChannels(frequency)
|
||||||
|
}
|
||||||
|
if os.Getenv("CHANNEL_TEST_FREQUENCY") != "" {
|
||||||
|
frequency, err := strconv.Atoi(os.Getenv("CHANNEL_TEST_FREQUENCY"))
|
||||||
|
if err != nil {
|
||||||
|
common.FatalLog("failed to parse CHANNEL_TEST_FREQUENCY: " + err.Error())
|
||||||
|
}
|
||||||
|
go controller.AutomaticallyTestChannels(frequency)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize HTTP server
|
// Initialize HTTP server
|
||||||
server := gin.Default()
|
server := gin.Default()
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func TokenAuth() func(c *gin.Context) {
|
|||||||
key = parts[0]
|
key = parts[0]
|
||||||
token, err := model.ValidateUserToken(key)
|
token, err := model.ValidateUserToken(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusUnauthorized, gin.H{
|
||||||
"error": gin.H{
|
"error": gin.H{
|
||||||
"message": err.Error(),
|
"message": err.Error(),
|
||||||
"type": "one_api_error",
|
"type": "one_api_error",
|
||||||
@@ -101,7 +101,7 @@ func TokenAuth() func(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !model.CacheIsUserEnabled(token.UserId) {
|
if !model.CacheIsUserEnabled(token.UserId) {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
"error": gin.H{
|
"error": gin.H{
|
||||||
"message": "用户已被封禁",
|
"message": "用户已被封禁",
|
||||||
"type": "one_api_error",
|
"type": "one_api_error",
|
||||||
@@ -123,7 +123,7 @@ func TokenAuth() func(c *gin.Context) {
|
|||||||
if model.IsAdmin(token.UserId) {
|
if model.IsAdmin(token.UserId) {
|
||||||
c.Set("channelId", parts[1])
|
c.Set("channelId", parts[1])
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
"error": gin.H{
|
"error": gin.H{
|
||||||
"message": "普通用户不支持指定渠道",
|
"message": "普通用户不支持指定渠道",
|
||||||
"type": "one_api_error",
|
"type": "one_api_error",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func Distribute() func(c *gin.Context) {
|
|||||||
if ok {
|
if ok {
|
||||||
id, err := strconv.Atoi(channelId.(string))
|
id, err := strconv.Atoi(channelId.(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": gin.H{
|
"error": gin.H{
|
||||||
"message": "无效的渠道 ID",
|
"message": "无效的渠道 ID",
|
||||||
"type": "one_api_error",
|
"type": "one_api_error",
|
||||||
@@ -35,7 +35,7 @@ func Distribute() func(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
channel, err = model.GetChannelById(id, true)
|
channel, err = model.GetChannelById(id, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": gin.H{
|
"error": gin.H{
|
||||||
"message": "无效的渠道 ID",
|
"message": "无效的渠道 ID",
|
||||||
"type": "one_api_error",
|
"type": "one_api_error",
|
||||||
@@ -45,7 +45,7 @@ func Distribute() func(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if channel.Status != common.ChannelStatusEnabled {
|
if channel.Status != common.ChannelStatusEnabled {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
"error": gin.H{
|
"error": gin.H{
|
||||||
"message": "该渠道已被禁用",
|
"message": "该渠道已被禁用",
|
||||||
"type": "one_api_error",
|
"type": "one_api_error",
|
||||||
@@ -59,7 +59,7 @@ func Distribute() func(c *gin.Context) {
|
|||||||
var modelRequest ModelRequest
|
var modelRequest ModelRequest
|
||||||
err := common.UnmarshalBodyReusable(c, &modelRequest)
|
err := common.UnmarshalBodyReusable(c, &modelRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"error": gin.H{
|
"error": gin.H{
|
||||||
"message": "无效的请求",
|
"message": "无效的请求",
|
||||||
"type": "one_api_error",
|
"type": "one_api_error",
|
||||||
@@ -75,7 +75,7 @@ func Distribute() func(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model)
|
channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||||
"error": gin.H{
|
"error": gin.H{
|
||||||
"message": "无可用渠道",
|
"message": "无可用渠道",
|
||||||
"type": "one_api_error",
|
"type": "one_api_error",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
type Channel struct {
|
type Channel struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Type int `json:"type" gorm:"default:0"`
|
Type int `json:"type" gorm:"default:0"`
|
||||||
Key string `json:"key" gorm:"not null"`
|
Key string `json:"key" gorm:"not null;index"`
|
||||||
Status int `json:"status" gorm:"default:1"`
|
Status int `json:"status" gorm:"default:1"`
|
||||||
Name string `json:"name" gorm:"index"`
|
Name string `json:"name" gorm:"index"`
|
||||||
Weight int `json:"weight"`
|
Weight int `json:"weight"`
|
||||||
@@ -36,7 +36,7 @@ func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SearchChannels(keyword string) (channels []*Channel, err error) {
|
func SearchChannels(keyword string) (channels []*Channel, err error) {
|
||||||
err = DB.Omit("key").Where("id = ? or name LIKE ?", keyword, keyword+"%").Find(&channels).Error
|
err = DB.Omit("key").Where("id = ? or name LIKE ? or key = ?", keyword, keyword+"%", keyword).Find(&channels).Error
|
||||||
return channels, err
|
return channels, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
|
common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
|
||||||
common.OptionMap["LogConsumeEnabled"] = strconv.FormatBool(common.LogConsumeEnabled)
|
common.OptionMap["LogConsumeEnabled"] = strconv.FormatBool(common.LogConsumeEnabled)
|
||||||
common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
|
common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
|
||||||
|
common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled)
|
||||||
common.OptionMap["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64)
|
common.OptionMap["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64)
|
||||||
common.OptionMap["SMTPServer"] = ""
|
common.OptionMap["SMTPServer"] = ""
|
||||||
common.OptionMap["SMTPFrom"] = ""
|
common.OptionMap["SMTPFrom"] = ""
|
||||||
@@ -144,6 +145,8 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
common.LogConsumeEnabled = boolValue
|
common.LogConsumeEnabled = boolValue
|
||||||
case "DisplayInCurrencyEnabled":
|
case "DisplayInCurrencyEnabled":
|
||||||
common.DisplayInCurrencyEnabled = boolValue
|
common.DisplayInCurrencyEnabled = boolValue
|
||||||
|
case "DisplayTokenStatEnabled":
|
||||||
|
common.DisplayTokenStatEnabled = boolValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch key {
|
switch key {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type Token struct {
|
|||||||
ExpiredTime int64 `json:"expired_time" gorm:"bigint;default:-1"` // -1 means never expired
|
ExpiredTime int64 `json:"expired_time" gorm:"bigint;default:-1"` // -1 means never expired
|
||||||
RemainQuota int `json:"remain_quota" gorm:"default:0"`
|
RemainQuota int `json:"remain_quota" gorm:"default:0"`
|
||||||
UnlimitedQuota bool `json:"unlimited_quota" gorm:"default:false"`
|
UnlimitedQuota bool `json:"unlimited_quota" gorm:"default:false"`
|
||||||
|
UsedQuota int `json:"used_quota" gorm:"default:0"` // used quota
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) {
|
func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) {
|
||||||
@@ -130,7 +131,12 @@ func IncreaseTokenQuota(id int, quota int) (err error) {
|
|||||||
if quota < 0 {
|
if quota < 0 {
|
||||||
return errors.New("quota 不能为负数!")
|
return errors.New("quota 不能为负数!")
|
||||||
}
|
}
|
||||||
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota + ?", quota)).Error
|
err = DB.Model(&Token{}).Where("id = ?", id).Updates(
|
||||||
|
map[string]interface{}{
|
||||||
|
"remain_quota": gorm.Expr("remain_quota + ?", quota),
|
||||||
|
"used_quota": gorm.Expr("used_quota - ?", quota),
|
||||||
|
},
|
||||||
|
).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +144,12 @@ func DecreaseTokenQuota(id int, quota int) (err error) {
|
|||||||
if quota < 0 {
|
if quota < 0 {
|
||||||
return errors.New("quota 不能为负数!")
|
return errors.New("quota 不能为负数!")
|
||||||
}
|
}
|
||||||
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota - ?", quota)).Error
|
err = DB.Model(&Token{}).Where("id = ?", id).Updates(
|
||||||
|
map[string]interface{}{
|
||||||
|
"remain_quota": gorm.Expr("remain_quota - ?", quota),
|
||||||
|
"used_quota": gorm.Expr("used_quota + ?", quota),
|
||||||
|
},
|
||||||
|
).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ const ChannelsTable = () => {
|
|||||||
icon='search'
|
icon='search'
|
||||||
fluid
|
fluid
|
||||||
iconPosition='left'
|
iconPosition='left'
|
||||||
placeholder='搜索渠道的 ID 和名称 ...'
|
placeholder='搜索渠道的 ID,名称和密钥 ...'
|
||||||
value={searchKeyword}
|
value={searchKeyword}
|
||||||
loading={searching}
|
loading={searching}
|
||||||
onChange={handleKeywordChange}
|
onChange={handleKeywordChange}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ const OperationSetting = () => {
|
|||||||
AutomaticDisableChannelEnabled: '',
|
AutomaticDisableChannelEnabled: '',
|
||||||
ChannelDisableThreshold: 0,
|
ChannelDisableThreshold: 0,
|
||||||
LogConsumeEnabled: '',
|
LogConsumeEnabled: '',
|
||||||
DisplayInCurrencyEnabled: ''
|
DisplayInCurrencyEnabled: '',
|
||||||
|
DisplayTokenStatEnabled: ''
|
||||||
});
|
});
|
||||||
const [originInputs, setOriginInputs] = useState({});
|
const [originInputs, setOriginInputs] = useState({});
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
@@ -177,6 +178,12 @@ const OperationSetting = () => {
|
|||||||
name='DisplayInCurrencyEnabled'
|
name='DisplayInCurrencyEnabled'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.DisplayTokenStatEnabled === 'true'}
|
||||||
|
label='Billing 相关 API 显示令牌额度而非用户额度'
|
||||||
|
name='DisplayTokenStatEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Button onClick={() => {
|
<Form.Button onClick={() => {
|
||||||
submitConfig('general').then();
|
submitConfig('general').then();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
|
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
|
||||||
import { API, copy, showError, showSuccess } from '../helpers';
|
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
const PasswordResetConfirm = () => {
|
const PasswordResetConfirm = () => {
|
||||||
@@ -33,7 +33,7 @@ const PasswordResetConfirm = () => {
|
|||||||
if (success) {
|
if (success) {
|
||||||
let password = res.data.data;
|
let password = res.data.data;
|
||||||
await copy(password);
|
await copy(password);
|
||||||
showSuccess(`密码已重置并已复制到剪贴板:${password}`);
|
showNotice(`密码已重置并已复制到剪贴板:${password}`);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,13 +181,21 @@ const TokensTable = () => {
|
|||||||
>
|
>
|
||||||
状态
|
状态
|
||||||
</Table.HeaderCell>
|
</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => {
|
||||||
|
sortToken('used_quota');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
已用额度
|
||||||
|
</Table.HeaderCell>
|
||||||
<Table.HeaderCell
|
<Table.HeaderCell
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sortToken('remain_quota');
|
sortToken('remain_quota');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
额度
|
剩余额度
|
||||||
</Table.HeaderCell>
|
</Table.HeaderCell>
|
||||||
<Table.HeaderCell
|
<Table.HeaderCell
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
@@ -221,6 +229,7 @@ const TokensTable = () => {
|
|||||||
<Table.Row key={token.id}>
|
<Table.Row key={token.id}>
|
||||||
<Table.Cell>{token.name ? token.name : '无'}</Table.Cell>
|
<Table.Cell>{token.name ? token.name : '无'}</Table.Cell>
|
||||||
<Table.Cell>{renderStatus(token.status)}</Table.Cell>
|
<Table.Cell>{renderStatus(token.status)}</Table.Cell>
|
||||||
|
<Table.Cell>{renderQuota(token.used_quota)}</Table.Cell>
|
||||||
<Table.Cell>{token.unlimited_quota ? '无限制' : renderQuota(token.remain_quota, 2)}</Table.Cell>
|
<Table.Cell>{token.unlimited_quota ? '无限制' : renderQuota(token.remain_quota, 2)}</Table.Cell>
|
||||||
<Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell>
|
<Table.Cell>{renderTimestamp(token.created_time)}</Table.Cell>
|
||||||
<Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
|
<Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
|
||||||
|
|||||||
@@ -32,15 +32,15 @@ const EditChannel = () => {
|
|||||||
let res = await API.get(`/api/channel/${channelId}`);
|
let res = await API.get(`/api/channel/${channelId}`);
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
if (data.models === "") {
|
if (data.models === '') {
|
||||||
data.models = []
|
data.models = [];
|
||||||
} else {
|
} else {
|
||||||
data.models = data.models.split(",")
|
data.models = data.models.split(',');
|
||||||
}
|
}
|
||||||
if (data.group === "") {
|
if (data.group === '') {
|
||||||
data.groups = []
|
data.groups = [];
|
||||||
} else {
|
} else {
|
||||||
data.groups = data.group.split(",")
|
data.groups = data.group.split(',');
|
||||||
}
|
}
|
||||||
setInputs(data);
|
setInputs(data);
|
||||||
} else {
|
} else {
|
||||||
@@ -55,10 +55,10 @@ const EditChannel = () => {
|
|||||||
setModelOptions(res.data.data.map((model) => ({
|
setModelOptions(res.data.data.map((model) => ({
|
||||||
key: model.id,
|
key: model.id,
|
||||||
text: model.id,
|
text: model.id,
|
||||||
value: model.id,
|
value: model.id
|
||||||
})));
|
})));
|
||||||
setFullModels(res.data.data.map((model) => model.id));
|
setFullModels(res.data.data.map((model) => model.id));
|
||||||
setBasicModels(res.data.data.filter((model) => !model.id.startsWith("gpt-4")).map((model) => model.id));
|
setBasicModels(res.data.data.filter((model) => !model.id.startsWith('gpt-4')).map((model) => model.id));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error.message);
|
showError(error.message);
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ const EditChannel = () => {
|
|||||||
setGroupOptions(res.data.data.map((group) => ({
|
setGroupOptions(res.data.data.map((group) => ({
|
||||||
key: group,
|
key: group,
|
||||||
text: group,
|
text: group,
|
||||||
value: group,
|
value: group
|
||||||
})));
|
})));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error.message);
|
showError(error.message);
|
||||||
@@ -90,6 +90,10 @@ const EditChannel = () => {
|
|||||||
showInfo('请填写渠道名称和渠道密钥!');
|
showInfo('请填写渠道名称和渠道密钥!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (inputs.models.length === 0) {
|
||||||
|
showInfo('请至少选择一个模型!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
let localInputs = inputs;
|
let localInputs = inputs;
|
||||||
if (localInputs.base_url.endsWith('/')) {
|
if (localInputs.base_url.endsWith('/')) {
|
||||||
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
|
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
|
||||||
@@ -98,8 +102,8 @@ const EditChannel = () => {
|
|||||||
localInputs.other = '2023-03-15-preview';
|
localInputs.other = '2023-03-15-preview';
|
||||||
}
|
}
|
||||||
let res;
|
let res;
|
||||||
localInputs.models = localInputs.models.join(",")
|
localInputs.models = localInputs.models.join(',');
|
||||||
localInputs.group = localInputs.groups.join(",")
|
localInputs.group = localInputs.groups.join(',');
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId) });
|
res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId) });
|
||||||
} else {
|
} else {
|
||||||
@@ -181,9 +185,9 @@ const EditChannel = () => {
|
|||||||
inputs.type !== 3 && inputs.type !== 8 && (
|
inputs.type !== 3 && inputs.type !== 8 && (
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='Base URL'
|
label='镜像'
|
||||||
name='base_url'
|
name='base_url'
|
||||||
placeholder={'请输入自定义 Base URL,格式为:https://domain.com,可不填,不填使用渠道默认值'}
|
placeholder={'请输入镜像站地址,格式为:https://domain.com,可不填,不填则使用渠道默认值'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.base_url}
|
value={inputs.base_url}
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
@@ -231,28 +235,17 @@ const EditChannel = () => {
|
|||||||
options={modelOptions}
|
options={modelOptions}
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
<div style={{ lineHeight: '40px', marginBottom: '12px'}}>
|
<div style={{ lineHeight: '40px', marginBottom: '12px' }}>
|
||||||
<Button type={'button'} onClick={() => {
|
<Button type={'button'} onClick={() => {
|
||||||
handleInputChange(null, { name: 'models', value: basicModels });
|
handleInputChange(null, { name: 'models', value: basicModels });
|
||||||
}}>填入基础模型</Button>
|
}}>填入基础模型</Button>
|
||||||
<Button type={'button'} onClick={() => {
|
<Button type={'button'} onClick={() => {
|
||||||
handleInputChange(null, { name: 'models', value: fullModels });
|
handleInputChange(null, { name: 'models', value: fullModels });
|
||||||
}}>填入所有模型</Button>
|
}}>填入所有模型</Button>
|
||||||
|
<Button type={'button'} onClick={() => {
|
||||||
|
handleInputChange(null, { name: 'models', value: [] });
|
||||||
|
}}>清除所有模型</Button>
|
||||||
</div>
|
</div>
|
||||||
{
|
|
||||||
inputs.type === 1 && (
|
|
||||||
<Form.Field>
|
|
||||||
<Form.Input
|
|
||||||
label='代理'
|
|
||||||
name='base_url'
|
|
||||||
placeholder={'请输入 OpenAI API 代理地址,如果不需要请留空,格式为:https://api.openai.com'}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
value={inputs.base_url}
|
|
||||||
autoComplete='new-password'
|
|
||||||
/>
|
|
||||||
</Form.Field>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
batch ? <Form.Field>
|
batch ? <Form.Field>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
|
|||||||
Reference in New Issue
Block a user