From 67c64e71c800a76b2fa7a5b6eb51e89a14479b0c Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 20 Dec 2023 21:45:33 +0800 Subject: [PATCH 01/23] fix: fix max_tokens check --- controller/relay-text.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/controller/relay-text.go b/controller/relay-text.go index b53b0caa..c49a2abe 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -58,6 +58,9 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if err != nil { return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest) } + if textRequest.MaxTokens < 0 || textRequest.MaxTokens > math.MaxInt32/2 { + return errorWrapper(errors.New("max_tokens is invalid"), "invalid_max_tokens", http.StatusBadRequest) + } if relayMode == RelayModeModerations && textRequest.Model == "" { textRequest.Model = "text-moderation-latest" } From b7fcb319da622a6de340b34e9255efd5fae6cf19 Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 20 Dec 2023 22:50:50 +0800 Subject: [PATCH 02/23] chore: check if SESSION_SECRET equals to random_string --- common/init.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/init.go b/common/init.go index 1e9c85ce..12df5f51 100644 --- a/common/init.go +++ b/common/init.go @@ -36,7 +36,11 @@ func init() { } if os.Getenv("SESSION_SECRET") != "" { - SessionSecret = os.Getenv("SESSION_SECRET") + if os.Getenv("SESSION_SECRET") == "random_string" { + SysError("SESSION_SECRET is set to an example value, please change it to a random string.") + } else { + SessionSecret = os.Getenv("SESSION_SECRET") + } } if os.Getenv("SQLITE_PATH") != "" { SQLitePath = os.Getenv("SQLITE_PATH") From a763681c2edebca610db7ae91d11d0e9a559a08c Mon Sep 17 00:00:00 2001 From: Buer <42402987+MartialBE@users.noreply.github.com> Date: Sun, 24 Dec 2023 15:35:56 +0800 Subject: [PATCH 03/23] fix: fix base64 image parse error (#858) --- common/image/image.go | 25 +++++++++++++++++++++---- common/image/image_test.go | 17 +++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/common/image/image.go b/common/image/image.go index cbb656ad..93da6a06 100644 --- a/common/image/image.go +++ b/common/image/image.go @@ -1,6 +1,8 @@ package image import ( + "bytes" + "encoding/base64" "image" _ "image/gif" _ "image/jpeg" @@ -8,6 +10,7 @@ import ( "net/http" "regexp" "strings" + "sync" _ "golang.org/x/image/webp" ) @@ -29,13 +32,27 @@ var ( reg = regexp.MustCompile(`data:image/([^;]+);base64,`) ) +var readerPool = sync.Pool{ + New: func() interface{} { + return &bytes.Reader{} + }, +} + func GetImageSizeFromBase64(encoded string) (width int, height int, err error) { - encoded = strings.TrimPrefix(encoded, "data:image/png;base64,") - base64 := strings.NewReader(reg.ReplaceAllString(encoded, "")) - img, _, err := image.DecodeConfig(base64) + decoded, err := base64.StdEncoding.DecodeString(reg.ReplaceAllString(encoded, "")) if err != nil { - return + return 0, 0, err } + + reader := readerPool.Get().(*bytes.Reader) + defer readerPool.Put(reader) + reader.Reset(decoded) + + img, _, err := image.DecodeConfig(reader) + if err != nil { + return 0, 0, err + } + return img.Width, img.Height, nil } diff --git a/common/image/image_test.go b/common/image/image_test.go index 366eda6e..8e47b109 100644 --- a/common/image/image_test.go +++ b/common/image/image_test.go @@ -152,3 +152,20 @@ func TestGetImageSize(t *testing.T) { }) } } + +func TestGetImageSizeFromBase64(t *testing.T) { + for i, c := range cases { + t.Run("Decode:"+strconv.Itoa(i), func(t *testing.T) { + resp, err := http.Get(c.url) + assert.NoError(t, err) + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + encoded := base64.StdEncoding.EncodeToString(data) + width, height, err := img.GetImageSizeFromBase64(encoded) + assert.NoError(t, err) + assert.Equal(t, c.width, width) + assert.Equal(t, c.height, height) + }) + } +} From ee9e746520e57520fd2b7c7f1bc85d4eaed077fd Mon Sep 17 00:00:00 2001 From: moondie <528893699@qq.com> Date: Sun, 24 Dec 2023 16:17:21 +0800 Subject: [PATCH 04/23] feat: update ali stream implementation & enable internet search (#856) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update relay-ali.go: 改进stream模式,添加联网搜索能力 通义千问支持stream的增量模式,不需要每次去掉上次的前缀;实测qwen-max联网模式效果不错,添加了联网模式。如果别的模型有问题可以改为单独给qwen-max开放 * 删除"stream参数" 刚发现原来阿里api没有这个参数,上次误加了。 * refactor: only enable search when specified * fix: remove custom suffix when get model ratio --------- Co-authored-by: JustSong --- common/model-ratio.go | 3 +++ controller/relay-ali.go | 37 +++++++++++++++++----------- web/src/pages/Channel/EditChannel.js | 7 ++++++ 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index d1c96d96..d6b51f84 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -115,6 +115,9 @@ func UpdateModelRatioByJSONString(jsonStr string) error { } func GetModelRatio(name string) float64 { + if strings.HasPrefix(name, "qwen-") && strings.HasSuffix(name, "-internet") { + name = strings.TrimSuffix(name, "-internet") + } ratio, ok := ModelRatio[name] if !ok { SysError("model ratio not found: " + name) diff --git a/controller/relay-ali.go b/controller/relay-ali.go index 65626f6a..7968bfb6 100644 --- a/controller/relay-ali.go +++ b/controller/relay-ali.go @@ -23,10 +23,11 @@ type AliInput struct { } type AliParameters struct { - TopP float64 `json:"top_p,omitempty"` - TopK int `json:"top_k,omitempty"` - Seed uint64 `json:"seed,omitempty"` - EnableSearch bool `json:"enable_search,omitempty"` + TopP float64 `json:"top_p,omitempty"` + TopK int `json:"top_k,omitempty"` + Seed uint64 `json:"seed,omitempty"` + EnableSearch bool `json:"enable_search,omitempty"` + IncrementalOutput bool `json:"incremental_output,omitempty"` } type AliChatRequest struct { @@ -81,6 +82,8 @@ type AliChatResponse struct { AliError } +const AliEnableSearchModelSuffix = "-internet" + func requestOpenAI2Ali(request GeneralOpenAIRequest) *AliChatRequest { messages := make([]AliMessage, 0, len(request.Messages)) for i := 0; i < len(request.Messages); i++ { @@ -90,17 +93,21 @@ func requestOpenAI2Ali(request GeneralOpenAIRequest) *AliChatRequest { Role: strings.ToLower(message.Role), }) } + enableSearch := false + aliModel := request.Model + if strings.HasSuffix(aliModel, AliEnableSearchModelSuffix) { + enableSearch = true + aliModel = strings.TrimSuffix(aliModel, AliEnableSearchModelSuffix) + } return &AliChatRequest{ - Model: request.Model, + Model: aliModel, Input: AliInput{ Messages: messages, }, - //Parameters: AliParameters{ // ChatGPT's parameters are not compatible with Ali's - // TopP: request.TopP, - // TopK: 50, - // //Seed: 0, - // //EnableSearch: false, - //}, + Parameters: AliParameters{ + EnableSearch: enableSearch, + IncrementalOutput: request.Stream, + }, } } @@ -202,7 +209,7 @@ func streamResponseAli2OpenAI(aliResponse *AliChatResponse) *ChatCompletionsStre Id: aliResponse.RequestId, Object: "chat.completion.chunk", Created: common.GetTimestamp(), - Model: "ernie-bot", + Model: "qwen", Choices: []ChatCompletionsStreamResponseChoice{choice}, } return &response @@ -240,7 +247,7 @@ func aliStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStat stopChan <- true }() setEventStreamHeaders(c) - lastResponseText := "" + //lastResponseText := "" c.Stream(func(w io.Writer) bool { select { case data := <-dataChan: @@ -256,8 +263,8 @@ func aliStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStat usage.TotalTokens = aliResponse.Usage.InputTokens + aliResponse.Usage.OutputTokens } response := streamResponseAli2OpenAI(&aliResponse) - response.Choices[0].Delta.Content = strings.TrimPrefix(response.Choices[0].Delta.Content, lastResponseText) - lastResponseText = aliResponse.Output.Text + //response.Choices[0].Delta.Content = strings.TrimPrefix(response.Choices[0].Delta.Content, lastResponseText) + //lastResponseText = aliResponse.Output.Text jsonResponse, err := json.Marshal(response) if err != nil { common.SysError("error marshalling stream response: " + err.Error()) diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 364da69d..b1c7ae62 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -70,6 +70,13 @@ const EditChannel = () => { break; case 17: localModels = ['qwen-turbo', 'qwen-plus', 'qwen-max', 'qwen-max-longcontext', 'text-embedding-v1']; + let withInternetVersion = []; + for (let i = 0; i < localModels.length; i++) { + if (localModels[i].startsWith('qwen-')) { + withInternetVersion.push(localModels[i] + '-internet'); + } + } + localModels = [...localModels, ...withInternetVersion]; break; case 16: localModels = ['chatglm_turbo', 'chatglm_pro', 'chatglm_std', 'chatglm_lite']; From 0699ecd0af9a12e748f1174c3814f0f5c3f49592 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 24 Dec 2023 16:29:48 +0800 Subject: [PATCH 05/23] chore(deps): bump golang.org/x/crypto from 0.14.0 to 0.17.0 (#840) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.14.0 to 0.17.0. - [Commits](https://github.com/golang/crypto/compare/v0.14.0...v0.17.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 1fe5eabc..68dd5eb6 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/pkoukk/tiktoken-go v0.1.5 github.com/stretchr/testify v1.8.3 - golang.org/x/crypto v0.14.0 + golang.org/x/crypto v0.17.0 golang.org/x/image v0.14.0 gorm.io/driver/mysql v1.4.3 gorm.io/driver/postgres v1.5.2 @@ -58,7 +58,7 @@ require ( github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index fb252aa7..21bcddc6 100644 --- a/go.sum +++ b/go.sum @@ -150,8 +150,8 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -164,8 +164,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From 40ceb29e540340572035cdd4348e80433e4d5ab2 Mon Sep 17 00:00:00 2001 From: Bryan Date: Sun, 24 Dec 2023 16:42:00 +0800 Subject: [PATCH 06/23] fix: fix SearchUsers not working if using PostgreSQL (#778) * fix SearchUsers * refactor: using UsingPostgreSQL as condition --------- Co-authored-by: JustSong --- model/user.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/model/user.go b/model/user.go index 7844eb6a..e738b1ba 100644 --- a/model/user.go +++ b/model/user.go @@ -42,7 +42,11 @@ func GetAllUsers(startIdx int, num int) (users []*User, err error) { } func SearchUsers(keyword string) (users []*User, err error) { - err = DB.Omit("password").Where("id = ? or username LIKE ? or email LIKE ? or display_name LIKE ?", keyword, keyword+"%", keyword+"%", keyword+"%").Find(&users).Error + if !common.UsingPostgreSQL { + err = DB.Omit("password").Where("id = ? or username LIKE ? or email LIKE ? or display_name LIKE ?", keyword, keyword+"%", keyword+"%", keyword+"%").Find(&users).Error + } else { + err = DB.Omit("password").Where("username LIKE ? or email LIKE ? or display_name LIKE ?", keyword+"%", keyword+"%", keyword+"%").Find(&users).Error + } return users, err } From f3c07e14511c563b7d2cbe66432338e2ed545586 Mon Sep 17 00:00:00 2001 From: "Laisky.Cai" Date: Sun, 24 Dec 2023 16:58:31 +0800 Subject: [PATCH 07/23] fix: openai response should contains `model` (#841) * fix: openai response should contains `model` - Update model attributes in `claudeHandler` for `relay-claude.go` - Implement model type for fullTextResponse in `relay-gemini.go` - Add new `Model` field to `OpenAITextResponse` struct in `relay.go` * chore: set model name response for models --------- Co-authored-by: JustSong --- controller/relay-ali.go | 1 + controller/relay-baidu.go | 1 + controller/relay-claude.go | 1 + controller/relay-gemini.go | 1 + controller/relay-palm.go | 1 + controller/relay-tencent.go | 1 + controller/relay-zhipu.go | 1 + controller/relay.go | 1 + 8 files changed, 8 insertions(+) diff --git a/controller/relay-ali.go b/controller/relay-ali.go index 7968bfb6..df1cc084 100644 --- a/controller/relay-ali.go +++ b/controller/relay-ali.go @@ -310,6 +310,7 @@ func aliHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode }, nil } fullTextResponse := responseAli2OpenAI(&aliResponse) + fullTextResponse.Model = "qwen" jsonResponse, err := json.Marshal(fullTextResponse) if err != nil { return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil diff --git a/controller/relay-baidu.go b/controller/relay-baidu.go index c75ec09a..dca30da1 100644 --- a/controller/relay-baidu.go +++ b/controller/relay-baidu.go @@ -255,6 +255,7 @@ func baiduHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCo }, nil } fullTextResponse := responseBaidu2OpenAI(&baiduResponse) + fullTextResponse.Model = "ernie-bot" jsonResponse, err := json.Marshal(fullTextResponse) if err != nil { return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil diff --git a/controller/relay-claude.go b/controller/relay-claude.go index 1b72b47d..ca7a701a 100644 --- a/controller/relay-claude.go +++ b/controller/relay-claude.go @@ -204,6 +204,7 @@ func claudeHandler(c *gin.Context, resp *http.Response, promptTokens int, model }, nil } fullTextResponse := responseClaude2OpenAI(&claudeResponse) + fullTextResponse.Model = model completionTokens := countTokenText(claudeResponse.Completion, model) usage := Usage{ PromptTokens: promptTokens, diff --git a/controller/relay-gemini.go b/controller/relay-gemini.go index 2458458e..523018de 100644 --- a/controller/relay-gemini.go +++ b/controller/relay-gemini.go @@ -287,6 +287,7 @@ func geminiChatHandler(c *gin.Context, resp *http.Response, promptTokens int, mo }, nil } fullTextResponse := responseGeminiChat2OpenAI(&geminiResponse) + fullTextResponse.Model = model completionTokens := countTokenText(geminiResponse.GetResponseText(), model) usage := Usage{ PromptTokens: promptTokens, diff --git a/controller/relay-palm.go b/controller/relay-palm.go index 2bd0bcd8..0c1c8af6 100644 --- a/controller/relay-palm.go +++ b/controller/relay-palm.go @@ -187,6 +187,7 @@ func palmHandler(c *gin.Context, resp *http.Response, promptTokens int, model st }, nil } fullTextResponse := responsePaLM2OpenAI(&palmResponse) + fullTextResponse.Model = model completionTokens := countTokenText(palmResponse.Candidates[0].Content, model) usage := Usage{ PromptTokens: promptTokens, diff --git a/controller/relay-tencent.go b/controller/relay-tencent.go index f66bf38f..5930ae89 100644 --- a/controller/relay-tencent.go +++ b/controller/relay-tencent.go @@ -237,6 +237,7 @@ func tencentHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatus }, nil } fullTextResponse := responseTencent2OpenAI(&TencentResponse) + fullTextResponse.Model = "hunyuan" jsonResponse, err := json.Marshal(fullTextResponse) if err != nil { return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil diff --git a/controller/relay-zhipu.go b/controller/relay-zhipu.go index 2e345ab5..cb5a78cf 100644 --- a/controller/relay-zhipu.go +++ b/controller/relay-zhipu.go @@ -290,6 +290,7 @@ func zhipuHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCo }, nil } fullTextResponse := responseZhipu2OpenAI(&zhipuResponse) + fullTextResponse.Model = "chatglm" jsonResponse, err := json.Marshal(fullTextResponse) if err != nil { return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil diff --git a/controller/relay.go b/controller/relay.go index 15021997..b7906d08 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -206,6 +206,7 @@ type OpenAITextResponseChoice struct { type OpenAITextResponse struct { Id string `json:"id"` + Model string `json:"model,omitempty"` Object string `json:"object"` Created int64 `json:"created"` Choices []OpenAITextResponseChoice `json:"choices"` From 1c8922153d6adb94184513e7f0263521f0d29157 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 24 Dec 2023 18:54:32 +0800 Subject: [PATCH 08/23] feat: support gemini-vision-pro --- common/image/image.go | 35 +++++++++++++++++ common/model-ratio.go | 1 + controller/model.go | 9 +++++ controller/relay-gemini.go | 31 +++++++++++++++ controller/relay-text.go | 10 +---- controller/relay.go | 59 +++++++++++++++++++++++++++- middleware/recover.go | 2 + web/src/pages/Channel/EditChannel.js | 2 +- 8 files changed, 139 insertions(+), 10 deletions(-) diff --git a/common/image/image.go b/common/image/image.go index 93da6a06..a602936a 100644 --- a/common/image/image.go +++ b/common/image/image.go @@ -15,7 +15,22 @@ import ( _ "golang.org/x/image/webp" ) +func IsImageUrl(url string) (bool, error) { + resp, err := http.Head(url) + if err != nil { + return false, err + } + if !strings.HasPrefix(resp.Header.Get("Content-Type"), "image/") { + return false, nil + } + return true, nil +} + func GetImageSizeFromUrl(url string) (width int, height int, err error) { + isImage, err := IsImageUrl(url) + if !isImage { + return + } resp, err := http.Get(url) if err != nil { return @@ -28,6 +43,26 @@ func GetImageSizeFromUrl(url string) (width int, height int, err error) { return img.Width, img.Height, nil } +func GetImageFromUrl(url string) (mimeType string, data string, err error) { + isImage, err := IsImageUrl(url) + if !isImage { + return + } + resp, err := http.Get(url) + if err != nil { + return + } + defer resp.Body.Close() + buffer := bytes.NewBuffer(nil) + _, err = buffer.ReadFrom(resp.Body) + if err != nil { + return + } + mimeType = resp.Header.Get("Content-Type") + data = base64.StdEncoding.EncodeToString(buffer.Bytes()) + return +} + var ( reg = regexp.MustCompile(`data:image/([^;]+);base64,`) ) diff --git a/common/model-ratio.go b/common/model-ratio.go index d6b51f84..fa2adaa1 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -84,6 +84,7 @@ var ModelRatio = map[string]float64{ "Embedding-V1": 0.1429, // ¥0.002 / 1k tokens "PaLM-2": 1, "gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens + "gemini-pro-vision": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens "chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens "chatglm_pro": 0.7143, // ¥0.01 / 1k tokens "chatglm_std": 0.3572, // ¥0.005 / 1k tokens diff --git a/controller/model.go b/controller/model.go index 9ae40f5c..6a759b63 100644 --- a/controller/model.go +++ b/controller/model.go @@ -432,6 +432,15 @@ func init() { Root: "gemini-pro", Parent: nil, }, + { + Id: "gemini-pro-vision", + Object: "model", + Created: 1677649963, + OwnedBy: "google", + Permission: permission, + Root: "gemini-pro-vision", + Parent: nil, + }, { Id: "chatglm_turbo", Object: "model", diff --git a/controller/relay-gemini.go b/controller/relay-gemini.go index 523018de..ec55d4b6 100644 --- a/controller/relay-gemini.go +++ b/controller/relay-gemini.go @@ -7,11 +7,18 @@ import ( "io" "net/http" "one-api/common" + "one-api/common/image" "strings" "github.com/gin-gonic/gin" ) +// https://ai.google.dev/docs/gemini_api_overview?hl=zh-cn + +const ( + GeminiVisionMaxImageNum = 16 +) + type GeminiChatRequest struct { Contents []GeminiChatContent `json:"contents"` SafetySettings []GeminiChatSafetySettings `json:"safety_settings,omitempty"` @@ -97,6 +104,30 @@ func requestOpenAI2Gemini(textRequest GeneralOpenAIRequest) *GeminiChatRequest { }, }, } + openaiContent := message.ParseContent() + var parts []GeminiPart + imageNum := 0 + for _, part := range openaiContent { + if part.Type == ContentTypeText { + parts = append(parts, GeminiPart{ + Text: part.Text, + }) + } else if part.Type == ContentTypeImageURL { + imageNum += 1 + if imageNum > GeminiVisionMaxImageNum { + continue + } + mimeType, data, _ := image.GetImageFromUrl(part.ImageURL.Url) + parts = append(parts, GeminiPart{ + InlineData: &GeminiInlineData{ + MimeType: mimeType, + Data: data, + }, + }) + } + } + content.Parts = parts + // there's no assistant role in gemini and API shall vomit if Role is not user or model if content.Role == "assistant" { content.Role = "model" diff --git a/controller/relay-text.go b/controller/relay-text.go index c49a2abe..64338545 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -180,9 +180,6 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if baseURL != "" { fullRequestURL = fmt.Sprintf("%s/v1beta2/models/chat-bison-001:generateMessage", baseURL) } - apiKey := c.Request.Header.Get("Authorization") - apiKey = strings.TrimPrefix(apiKey, "Bearer ") - fullRequestURL += "?key=" + apiKey case APITypeGemini: requestBaseURL := "https://generativelanguage.googleapis.com" if baseURL != "" { @@ -197,9 +194,6 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { action = "streamGenerateContent" } fullRequestURL = fmt.Sprintf("%s/%s/models/%s:%s", requestBaseURL, version, textRequest.Model, action) - apiKey := c.Request.Header.Get("Authorization") - apiKey = strings.TrimPrefix(apiKey, "Bearer ") - fullRequestURL += "?key=" + apiKey case APITypeZhipu: method := "invoke" if textRequest.Stream { @@ -396,9 +390,9 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { case APITypeTencent: req.Header.Set("Authorization", apiKey) case APITypePaLM: - // do not set Authorization header + req.Header.Set("x-goog-api-key", apiKey) case APITypeGemini: - // do not set Authorization header + req.Header.Set("x-goog-api-key", apiKey) default: req.Header.Set("Authorization", "Bearer "+apiKey) } diff --git a/controller/relay.go b/controller/relay.go index b7906d08..e45fd3eb 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -31,6 +31,22 @@ type ImageContent struct { ImageURL *ImageURL `json:"image_url,omitempty"` } +const ( + ContentTypeText = "text" + ContentTypeImageURL = "image_url" +) + +type OpenAIMessageContent struct { + Type string `json:"type,omitempty"` + Text string `json:"text"` + ImageURL *ImageURL `json:"image_url,omitempty"` +} + +func (m Message) IsStringContent() bool { + _, ok := m.Content.(string) + return ok +} + func (m Message) StringContent() string { content, ok := m.Content.(string) if ok { @@ -44,7 +60,7 @@ func (m Message) StringContent() string { if !ok { continue } - if contentMap["type"] == "text" { + if contentMap["type"] == ContentTypeText { if subStr, ok := contentMap["text"].(string); ok { contentStr += subStr } @@ -55,6 +71,47 @@ func (m Message) StringContent() string { return "" } +func (m Message) ParseContent() []OpenAIMessageContent { + var contentList []OpenAIMessageContent + content, ok := m.Content.(string) + if ok { + contentList = append(contentList, OpenAIMessageContent{ + Type: ContentTypeText, + Text: content, + }) + return contentList + } + anyList, ok := m.Content.([]any) + if ok { + for _, contentItem := range anyList { + contentMap, ok := contentItem.(map[string]any) + if !ok { + continue + } + switch contentMap["type"] { + case ContentTypeText: + if subStr, ok := contentMap["text"].(string); ok { + contentList = append(contentList, OpenAIMessageContent{ + Type: ContentTypeText, + Text: subStr, + }) + } + case ContentTypeImageURL: + if subObj, ok := contentMap["image_url"].(map[string]any); ok { + contentList = append(contentList, OpenAIMessageContent{ + Type: ContentTypeImageURL, + ImageURL: &ImageURL{ + Url: subObj["url"].(string), + }, + }) + } + } + } + return contentList + } + return nil +} + const ( RelayModeUnknown = iota RelayModeChatCompletions diff --git a/middleware/recover.go b/middleware/recover.go index c3a3d748..8338a514 100644 --- a/middleware/recover.go +++ b/middleware/recover.go @@ -5,6 +5,7 @@ import ( "github.com/gin-gonic/gin" "net/http" "one-api/common" + "runtime/debug" ) func RelayPanicRecover() gin.HandlerFunc { @@ -12,6 +13,7 @@ func RelayPanicRecover() gin.HandlerFunc { defer func() { if err := recover(); err != nil { common.SysError(fmt.Sprintf("panic detected: %v", err)) + common.SysError(fmt.Sprintf("stacktrace from panic: %s", string(debug.Stack()))) c.JSON(http.StatusInternalServerError, gin.H{ "error": gin.H{ "message": fmt.Sprintf("Panic detected, error: %v. Please submit a issue here: https://github.com/songquanpeng/one-api", err), diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index b1c7ae62..0d4e114d 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -91,7 +91,7 @@ const EditChannel = () => { localModels = ['hunyuan']; break; case 24: - localModels = ['gemini-pro']; + localModels = ['gemini-pro', 'gemini-pro-vision']; break; } setInputs((inputs) => ({ ...inputs, models: localModels })); From f44fbe3fe7e712847930b19a6527a476b2f4a45f Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 24 Dec 2023 19:24:59 +0800 Subject: [PATCH 09/23] docs: update pr template --- pull_request_template.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pull_request_template.md b/pull_request_template.md index bbcd969c..a313004f 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1,3 +1,9 @@ +[//]: # (请按照以下格式关联 issue) +[//]: # (请在提交 PR 前确认所提交的功能可用,附上截图即可,这将有助于项目维护者 review & merge 该 PR,谢谢) +[//]: # (项目维护者一般仅在周末处理 PR,因此如若未能及时回复希望能理解) +[//]: # (开发者交流群:910657413) +[//]: # (请在提交 PR 之前删除上面的注释) + close #issue_number 我已确认该 PR 已自测通过,相关截图如下: \ No newline at end of file From d8029550f758fed626caa1615a8f6d66efa02db4 Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 1 Jan 2024 16:18:50 +0800 Subject: [PATCH 10/23] fix: do not consume user quota if failed (close #881) --- controller/relay-image.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/controller/relay-image.go b/controller/relay-image.go index 7e1fed39..14a2983b 100644 --- a/controller/relay-image.go +++ b/controller/relay-image.go @@ -168,6 +168,9 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode var textResponse ImageResponse defer func(ctx context.Context) { + if resp.StatusCode != http.StatusOK { + return + } err := model.PostConsumeTokenQuota(tokenId, quota) if err != nil { common.SysError("error consuming token remain quota: " + err.Error()) From af8908db540d0ad4650e7595b79b0ccb066a9a38 Mon Sep 17 00:00:00 2001 From: Tisfeng Date: Mon, 1 Jan 2024 16:42:19 +0800 Subject: [PATCH 11/23] feat: able to change gemini safety setting (#867) * perf: adjust gemini safety settings, set BLOCK_NONE by default * feat: able to adjust by env variable --------- Co-authored-by: JustSong --- README.md | 1 + common/constants.go | 2 ++ common/utils.go | 7 +++++++ controller/relay-gemini.go | 36 ++++++++++++++++++------------------ 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index ff9e0bc0..b53936c4 100644 --- a/README.md +++ b/README.md @@ -366,6 +366,7 @@ graph LR + `DATA_GYM_CACHE_DIR`:目前该配置作用与 `TIKTOKEN_CACHE_DIR` 一致,但是优先级没有它高。 15. `RELAY_TIMEOUT`:中继超时设置,单位为秒,默认不设置超时时间。 16. `SQLITE_BUSY_TIMEOUT`:SQLite 锁等待超时设置,单位为毫秒,默认 `3000`。 +17. `GEMINI_SAFETY_SETTING`:Gemini 的安全设置,默认 `BLOCK_NONE`。 ### 命令行参数 1. `--port `: 指定服务器监听的端口号,默认为 `3000`。 diff --git a/common/constants.go b/common/constants.go index 60700ec8..e4cbf8bf 100644 --- a/common/constants.go +++ b/common/constants.go @@ -98,6 +98,8 @@ var BatchUpdateInterval = GetOrDefault("BATCH_UPDATE_INTERVAL", 5) var RelayTimeout = GetOrDefault("RELAY_TIMEOUT", 0) // unit is second +var GeminiSafetySetting = GetOrDefaultString("GEMINI_SAFETY_SETTING", "BLOCK_NONE") + const ( RequestIdKey = "X-Oneapi-Request-Id" ) diff --git a/common/utils.go b/common/utils.go index 21bec8f5..9a7038e2 100644 --- a/common/utils.go +++ b/common/utils.go @@ -196,6 +196,13 @@ func GetOrDefault(env string, defaultValue int) int { return num } +func GetOrDefaultString(env string, defaultValue string) string { + if env == "" || os.Getenv(env) == "" { + return defaultValue + } + return os.Getenv(env) +} + func MessageWithRequestId(message string, id string) string { return fmt.Sprintf("%s (request id: %s)", message, id) } diff --git a/controller/relay-gemini.go b/controller/relay-gemini.go index ec55d4b6..d8ab58d6 100644 --- a/controller/relay-gemini.go +++ b/controller/relay-gemini.go @@ -63,24 +63,24 @@ type GeminiChatGenerationConfig struct { func requestOpenAI2Gemini(textRequest GeneralOpenAIRequest) *GeminiChatRequest { geminiRequest := GeminiChatRequest{ Contents: make([]GeminiChatContent, 0, len(textRequest.Messages)), - //SafetySettings: []GeminiChatSafetySettings{ - // { - // Category: "HARM_CATEGORY_HARASSMENT", - // Threshold: "BLOCK_ONLY_HIGH", - // }, - // { - // Category: "HARM_CATEGORY_HATE_SPEECH", - // Threshold: "BLOCK_ONLY_HIGH", - // }, - // { - // Category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", - // Threshold: "BLOCK_ONLY_HIGH", - // }, - // { - // Category: "HARM_CATEGORY_DANGEROUS_CONTENT", - // Threshold: "BLOCK_ONLY_HIGH", - // }, - //}, + SafetySettings: []GeminiChatSafetySettings{ + { + Category: "HARM_CATEGORY_HARASSMENT", + Threshold: common.GeminiSafetySetting, + }, + { + Category: "HARM_CATEGORY_HATE_SPEECH", + Threshold: common.GeminiSafetySetting, + }, + { + Category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", + Threshold: common.GeminiSafetySetting, + }, + { + Category: "HARM_CATEGORY_DANGEROUS_CONTENT", + Threshold: common.GeminiSafetySetting, + }, + }, GenerationConfig: GeminiChatGenerationConfig{ Temperature: textRequest.Temperature, TopP: textRequest.TopP, From c725cc88429aed1e013729b6c87ced20a040544e Mon Sep 17 00:00:00 2001 From: Zhanliang Liu Date: Mon, 1 Jan 2024 17:00:23 +0800 Subject: [PATCH 12/23] fix: base 64 encoded format support of gemini-pro-vision for field image_url/url (#878) --- common/image/image.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/common/image/image.go b/common/image/image.go index a602936a..27045d28 100644 --- a/common/image/image.go +++ b/common/image/image.go @@ -44,6 +44,18 @@ func GetImageSizeFromUrl(url string) (width int, height int, err error) { } func GetImageFromUrl(url string) (mimeType string, data string, err error) { + // Regex to match data URL pattern + dataURLPattern := regexp.MustCompile(`data:image/([^;]+);base64,(.*)`) + + // Check if the URL is a data URL + matches := dataURLPattern.FindStringSubmatch(url) + if len(matches) == 3 { + // URL is a data URL + mimeType = "image/" + matches[1] + data = matches[2] + return + } + isImage, err := IsImageUrl(url) if !isImage { return From 498dea2dbbd7d68261ef32a900711d555181bb68 Mon Sep 17 00:00:00 2001 From: Tailen <15708073+Tailen@users.noreply.github.com> Date: Mon, 1 Jan 2024 17:06:17 +0800 Subject: [PATCH 13/23] feat: add support for davinci-002 and babbage-002 (#888) --- common/model-ratio.go | 2 ++ controller/model.go | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/common/model-ratio.go b/common/model-ratio.go index fa2adaa1..2908be17 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -52,6 +52,8 @@ var ModelRatio = map[string]float64{ "gpt-3.5-turbo-16k-0613": 1.5, "gpt-3.5-turbo-instruct": 0.75, // $0.0015 / 1K tokens "gpt-3.5-turbo-1106": 0.5, // $0.001 / 1K tokens + "davinci-002" 1, // $0.002 / 1K tokens + "babbage-002" 0.2, // $0.0004 / 1K tokens "text-ada-001": 0.2, "text-babbage-001": 0.25, "text-curie-001": 1, diff --git a/controller/model.go b/controller/model.go index 6a759b63..6cb530db 100644 --- a/controller/model.go +++ b/controller/model.go @@ -342,6 +342,24 @@ func init() { Root: "code-davinci-edit-001", Parent: nil, }, + { + Id: "davinci-002", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "davinci-002", + Parent: nil, + }, + { + Id: "babbage-002", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "babbage-002", + Parent: nil, + }, { Id: "claude-instant-1", Object: "model", From c50c6095651570d01a172c0eb2f166535ca24366 Mon Sep 17 00:00:00 2001 From: Seven Yu <422347121@qq.com> Date: Mon, 1 Jan 2024 17:09:12 +0800 Subject: [PATCH 14/23] fix: fix button copywriting (#880) * feat: rename Channel button * fix: update en.json --------- Co-authored-by: seven.yu Co-authored-by: JustSong --- i18n/en.json | 4 +++- web/src/components/ChannelsTable.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index b0deb83a..7b51909b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -526,5 +526,7 @@ "模型版本": "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!" + "请确保已在 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!", + "测试所有渠道": "Test all channels", + "更新已启用渠道余额": "Update the balance of enabled channels" } diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 5d68e2da..a2adfd32 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -523,10 +523,10 @@ const ChannelsTable = () => { 添加新的渠道 + loading={loading || updatingBalance}>更新已启用渠道余额 From 7772064d87468d62c7f7afee20a033328c07080d Mon Sep 17 00:00:00 2001 From: "Laisky.Cai" Date: Mon, 1 Jan 2024 17:38:35 +0800 Subject: [PATCH 15/23] fix: support base64 encoded image_url (#872) - Add support for base64 encoded image in OpenAI's image_url Co-authored-by: JustSong <39998050+songquanpeng@users.noreply.github.com> --- common/image/image.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/image/image.go b/common/image/image.go index 27045d28..de8fefd3 100644 --- a/common/image/image.go +++ b/common/image/image.go @@ -15,6 +15,9 @@ import ( _ "golang.org/x/image/webp" ) +// Regex to match data URL pattern +var dataURLPattern = regexp.MustCompile(`data:image/([^;]+);base64,(.*)`) + func IsImageUrl(url string) (bool, error) { resp, err := http.Head(url) if err != nil { @@ -44,9 +47,6 @@ func GetImageSizeFromUrl(url string) (width int, height int, err error) { } func GetImageFromUrl(url string) (mimeType string, data string, err error) { - // Regex to match data URL pattern - dataURLPattern := regexp.MustCompile(`data:image/([^;]+);base64,(.*)`) - // Check if the URL is a data URL matches := dataURLPattern.FindStringSubmatch(url) if len(matches) == 3 { From cb5a3df61687bee00eb4a93bd94e8f33786fb4db Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 1 Jan 2024 17:40:10 +0800 Subject: [PATCH 16/23] fix: fix pr error (#888) --- common/model-ratio.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index 2908be17..97cb060d 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -52,8 +52,8 @@ var ModelRatio = map[string]float64{ "gpt-3.5-turbo-16k-0613": 1.5, "gpt-3.5-turbo-instruct": 0.75, // $0.0015 / 1K tokens "gpt-3.5-turbo-1106": 0.5, // $0.001 / 1K tokens - "davinci-002" 1, // $0.002 / 1K tokens - "babbage-002" 0.2, // $0.0004 / 1K tokens + "davinci-002": 1, // $0.002 / 1K tokens + "babbage-002": 0.2, // $0.0004 / 1K tokens "text-ada-001": 0.2, "text-babbage-001": 0.25, "text-curie-001": 1, From 505817ca171121cf6f9d9cf579d6846513a87bda Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 1 Jan 2024 17:46:45 +0800 Subject: [PATCH 17/23] chore: update en.json --- i18n/en.json | 244 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) diff --git a/i18n/en.json b/i18n/en.json index 7b51909b..f67d8665 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -458,6 +458,7 @@ "使用明细(总消耗额度:{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", @@ -527,6 +528,249 @@ "请输入星火大模型版本,注意是接口地址中的版本号,例如: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" } From aa03c89133c5d0f9a35ceedcecb08c6b57206174 Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 1 Jan 2024 18:55:03 +0800 Subject: [PATCH 18/23] feat: able to add more UI theme (#860) --- .github/workflows/linux-release.yml | 6 ++-- .github/workflows/macos-release.yml | 6 ++-- .github/workflows/windows-release.yml | 6 ++-- Dockerfile | 13 ++++++-- common/constants.go | 2 ++ main.go | 9 ++---- router/main.go | 4 +-- router/web-router.go | 10 +++++-- web/README.md | 28 ++++++------------ web/THEMES | 1 + web/build/.gitkeep | 0 web/{ => default}/.gitignore | 0 web/default/README.md | 21 +++++++++++++ web/{ => default}/package.json | 2 +- web/{ => default}/public/favicon.ico | Bin web/{ => default}/public/index.html | 0 web/{ => default}/public/logo.png | Bin web/{ => default}/public/robots.txt | 0 web/{ => default}/src/App.js | 0 .../src/components/ChannelsTable.js | 0 web/{ => default}/src/components/Footer.js | 0 .../src/components/GitHubOAuth.js | 0 web/{ => default}/src/components/Header.js | 0 web/{ => default}/src/components/Loading.js | 0 web/{ => default}/src/components/LoginForm.js | 0 web/{ => default}/src/components/LogsTable.js | 0 .../src/components/OperationSetting.js | 0 .../src/components/OtherSetting.js | 0 .../src/components/PasswordResetConfirm.js | 0 .../src/components/PasswordResetForm.js | 0 .../src/components/PersonalSetting.js | 0 .../src/components/PrivateRoute.js | 0 .../src/components/RedemptionsTable.js | 0 .../src/components/RegisterForm.js | 0 .../src/components/SystemSetting.js | 0 .../src/components/TokensTable.js | 0 .../src/components/UsersTable.js | 0 web/{ => default}/src/components/utils.js | 0 .../src/constants/channel.constants.js | 0 .../src/constants/common.constant.js | 0 web/{ => default}/src/constants/index.js | 0 .../src/constants/toast.constants.js | 0 .../src/constants/user.constants.js | 0 web/{ => default}/src/context/Status/index.js | 0 .../src/context/Status/reducer.js | 0 web/{ => default}/src/context/User/index.js | 0 web/{ => default}/src/context/User/reducer.js | 0 web/{ => default}/src/helpers/api.js | 0 web/{ => default}/src/helpers/auth-header.js | 0 web/{ => default}/src/helpers/history.js | 0 web/{ => default}/src/helpers/index.js | 0 web/{ => default}/src/helpers/render.js | 0 web/{ => default}/src/helpers/utils.js | 0 web/{ => default}/src/index.css | 0 web/{ => default}/src/index.js | 0 web/{ => default}/src/pages/About/index.js | 0 .../src/pages/Channel/EditChannel.js | 0 web/{ => default}/src/pages/Channel/index.js | 0 web/{ => default}/src/pages/Chat/index.js | 0 web/{ => default}/src/pages/Home/index.js | 0 web/{ => default}/src/pages/Log/index.js | 0 web/{ => default}/src/pages/NotFound/index.js | 0 .../src/pages/Redemption/EditRedemption.js | 0 .../src/pages/Redemption/index.js | 0 web/{ => default}/src/pages/Setting/index.js | 0 .../src/pages/Token/EditToken.js | 0 web/{ => default}/src/pages/Token/index.js | 0 web/{ => default}/src/pages/TopUp/index.js | 0 web/{ => default}/src/pages/User/AddUser.js | 0 web/{ => default}/src/pages/User/EditUser.js | 0 web/{ => default}/src/pages/User/index.js | 0 web/{ => default}/vercel.json | 0 72 files changed, 66 insertions(+), 42 deletions(-) create mode 100644 web/THEMES create mode 100644 web/build/.gitkeep rename web/{ => default}/.gitignore (100%) create mode 100644 web/default/README.md rename web/{ => default}/package.json (94%) rename web/{ => default}/public/favicon.ico (100%) rename web/{ => default}/public/index.html (100%) rename web/{ => default}/public/logo.png (100%) rename web/{ => default}/public/robots.txt (100%) rename web/{ => default}/src/App.js (100%) rename web/{ => default}/src/components/ChannelsTable.js (100%) rename web/{ => default}/src/components/Footer.js (100%) rename web/{ => default}/src/components/GitHubOAuth.js (100%) rename web/{ => default}/src/components/Header.js (100%) rename web/{ => default}/src/components/Loading.js (100%) rename web/{ => default}/src/components/LoginForm.js (100%) rename web/{ => default}/src/components/LogsTable.js (100%) rename web/{ => default}/src/components/OperationSetting.js (100%) rename web/{ => default}/src/components/OtherSetting.js (100%) rename web/{ => default}/src/components/PasswordResetConfirm.js (100%) rename web/{ => default}/src/components/PasswordResetForm.js (100%) rename web/{ => default}/src/components/PersonalSetting.js (100%) rename web/{ => default}/src/components/PrivateRoute.js (100%) rename web/{ => default}/src/components/RedemptionsTable.js (100%) rename web/{ => default}/src/components/RegisterForm.js (100%) rename web/{ => default}/src/components/SystemSetting.js (100%) rename web/{ => default}/src/components/TokensTable.js (100%) rename web/{ => default}/src/components/UsersTable.js (100%) rename web/{ => default}/src/components/utils.js (100%) rename web/{ => default}/src/constants/channel.constants.js (100%) rename web/{ => default}/src/constants/common.constant.js (100%) rename web/{ => default}/src/constants/index.js (100%) rename web/{ => default}/src/constants/toast.constants.js (100%) rename web/{ => default}/src/constants/user.constants.js (100%) rename web/{ => default}/src/context/Status/index.js (100%) rename web/{ => default}/src/context/Status/reducer.js (100%) rename web/{ => default}/src/context/User/index.js (100%) rename web/{ => default}/src/context/User/reducer.js (100%) rename web/{ => default}/src/helpers/api.js (100%) rename web/{ => default}/src/helpers/auth-header.js (100%) rename web/{ => default}/src/helpers/history.js (100%) rename web/{ => default}/src/helpers/index.js (100%) rename web/{ => default}/src/helpers/render.js (100%) rename web/{ => default}/src/helpers/utils.js (100%) rename web/{ => default}/src/index.css (100%) rename web/{ => default}/src/index.js (100%) rename web/{ => default}/src/pages/About/index.js (100%) rename web/{ => default}/src/pages/Channel/EditChannel.js (100%) rename web/{ => default}/src/pages/Channel/index.js (100%) rename web/{ => default}/src/pages/Chat/index.js (100%) rename web/{ => default}/src/pages/Home/index.js (100%) rename web/{ => default}/src/pages/Log/index.js (100%) rename web/{ => default}/src/pages/NotFound/index.js (100%) rename web/{ => default}/src/pages/Redemption/EditRedemption.js (100%) rename web/{ => default}/src/pages/Redemption/index.js (100%) rename web/{ => default}/src/pages/Setting/index.js (100%) rename web/{ => default}/src/pages/Token/EditToken.js (100%) rename web/{ => default}/src/pages/Token/index.js (100%) rename web/{ => default}/src/pages/TopUp/index.js (100%) rename web/{ => default}/src/pages/User/AddUser.js (100%) rename web/{ => default}/src/pages/User/EditUser.js (100%) rename web/{ => default}/src/pages/User/index.js (100%) rename web/{ => default}/vercel.json (100%) diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml index 364b83ae..d9375795 100644 --- a/.github/workflows/linux-release.yml +++ b/.github/workflows/linux-release.yml @@ -18,14 +18,14 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 16 - - name: Build Frontend + - name: Build Frontend (theme default) env: CI: "" run: | - cd web + cd web/default npm install REACT_APP_VERSION=$(git describe --tags) npm run build - cd .. + cd ../.. - name: Set up Go uses: actions/setup-go@v3 with: diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml index bdd0d208..69bb93f5 100644 --- a/.github/workflows/macos-release.yml +++ b/.github/workflows/macos-release.yml @@ -18,14 +18,14 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 16 - - name: Build Frontend + - name: Build Frontend (theme default) env: CI: "" run: | - cd web + cd web/default npm install REACT_APP_VERSION=$(git describe --tags) npm run build - cd .. + cd ../.. - name: Set up Go uses: actions/setup-go@v3 with: diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index 33193a89..c08e95d2 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -21,14 +21,14 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 16 - - name: Build Frontend + - name: Build Frontend (theme default) env: CI: "" run: | - cd web + cd web/default npm install REACT_APP_VERSION=$(git describe --tags) npm run build - cd .. + cd ../.. - name: Set up Go uses: actions/setup-go@v3 with: diff --git a/Dockerfile b/Dockerfile index ffb8c21b..56648168 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,18 @@ FROM node:16 as builder WORKDIR /build -COPY web/package.json . -RUN npm install COPY ./web . COPY ./VERSION . -RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build +RUN themes=$(cat THEMES) \ + && IFS=$'\n' \ + && for theme in $themes; do \ + theme_path="web/$theme" \ + && echo "Building theme: $theme" \ + && cd $theme_path \ + && npm install \ + && DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build \ + && cd /app \ + done FROM golang AS builder2 diff --git a/common/constants.go b/common/constants.go index e4cbf8bf..70589041 100644 --- a/common/constants.go +++ b/common/constants.go @@ -100,6 +100,8 @@ var RelayTimeout = GetOrDefault("RELAY_TIMEOUT", 0) // unit is second var GeminiSafetySetting = GetOrDefaultString("GEMINI_SAFETY_SETTING", "BLOCK_NONE") +var Theme = GetOrDefaultString("THEME", "default") + const ( RequestIdKey = "X-Oneapi-Request-Id" ) diff --git a/main.go b/main.go index 88938516..3ab1872c 100644 --- a/main.go +++ b/main.go @@ -15,15 +15,12 @@ import ( "strconv" ) -//go:embed web/build +//go:embed web/build/* var buildFS embed.FS -//go:embed web/build/index.html -var indexPage []byte - func main() { common.SetupLogger() - common.SysLog("One API " + common.Version + " started") + common.SysLog(fmt.Sprintf("One API %s started with theme %s", common.Version, common.Theme)) if os.Getenv("GIN_MODE") != "debug" { gin.SetMode(gin.ReleaseMode) } @@ -95,7 +92,7 @@ func main() { store := cookie.NewStore([]byte(common.SessionSecret)) server.Use(sessions.Sessions("session", store)) - router.SetRouter(server, buildFS, indexPage) + router.SetRouter(server, buildFS) var port = os.Getenv("PORT") if port == "" { port = strconv.Itoa(*common.Port) diff --git a/router/main.go b/router/main.go index b8ac4055..85127a1a 100644 --- a/router/main.go +++ b/router/main.go @@ -10,7 +10,7 @@ import ( "strings" ) -func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { +func SetRouter(router *gin.Engine, buildFS embed.FS) { SetApiRouter(router) SetDashboardRouter(router) SetRelayRouter(router) @@ -20,7 +20,7 @@ func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { common.SysLog("FRONTEND_BASE_URL is ignored on master node") } if frontendBaseUrl == "" { - SetWebRouter(router, buildFS, indexPage) + SetWebRouter(router, buildFS) } else { frontendBaseUrl = strings.TrimSuffix(frontendBaseUrl, "/") router.NoRoute(func(c *gin.Context) { diff --git a/router/web-router.go b/router/web-router.go index 8f9c18a2..2f86db38 100644 --- a/router/web-router.go +++ b/router/web-router.go @@ -2,6 +2,7 @@ package router import ( "embed" + "fmt" "github.com/gin-contrib/gzip" "github.com/gin-contrib/static" "github.com/gin-gonic/gin" @@ -12,17 +13,22 @@ import ( "strings" ) -func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { +func SetWebRouter(router *gin.Engine, buildFS embed.FS) { router.Use(gzip.Gzip(gzip.DefaultCompression)) router.Use(middleware.GlobalWebRateLimit()) router.Use(middleware.Cache()) - router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build"))) + router.Use(static.Serve("/", common.EmbedFolder(buildFS, fmt.Sprintf("web/build/%s", common.Theme)))) router.NoRoute(func(c *gin.Context) { if strings.HasPrefix(c.Request.RequestURI, "/v1") || strings.HasPrefix(c.Request.RequestURI, "/api") { controller.RelayNotFound(c) return } c.Header("Cache-Control", "no-cache") + indexPage, err := buildFS.ReadFile(fmt.Sprintf("web/build/%s/index.html", common.Theme)) + if err != nil { + controller.RelayNotFound(c) + return + } c.Data(http.StatusOK, "text/html; charset=utf-8", indexPage) }) } diff --git a/web/README.md b/web/README.md index 1b1031a3..1454940f 100644 --- a/web/README.md +++ b/web/README.md @@ -1,21 +1,11 @@ -# React Template +# One API 的前端界面 +> 每个文件夹代表一个主题,欢迎提交你的主题 -## Basic Usages +## 提交新的主题 +1. 在 `web` 文件夹下新建一个文件夹,文件夹名为主题名。 +2. 把你的主题文件放到这个文件夹下。 +3. 修改 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv build ../build/default"`,其中 `default` 为你的主题名。 -```shell -# Runs the app in the development mode -npm start - -# Builds the app for production to the `build` folder -npm run build -``` - -If you want to change the default server, please set `REACT_APP_SERVER` environment variables before build, -for example: `REACT_APP_SERVER=http://your.domain.com`. - -Before you start editing, make sure your `Actions on Save` options have `Optimize imports` & `Run Prettier` enabled. - -## Reference - -1. https://github.com/OIerDb-ng/OIerDb -2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example \ No newline at end of file +## 主题列表 +### default +默认主题 \ No newline at end of file diff --git a/web/THEMES b/web/THEMES new file mode 100644 index 00000000..331d858c --- /dev/null +++ b/web/THEMES @@ -0,0 +1 @@ +default \ No newline at end of file diff --git a/web/build/.gitkeep b/web/build/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/web/.gitignore b/web/default/.gitignore similarity index 100% rename from web/.gitignore rename to web/default/.gitignore diff --git a/web/default/README.md b/web/default/README.md new file mode 100644 index 00000000..1b1031a3 --- /dev/null +++ b/web/default/README.md @@ -0,0 +1,21 @@ +# React Template + +## Basic Usages + +```shell +# Runs the app in the development mode +npm start + +# Builds the app for production to the `build` folder +npm run build +``` + +If you want to change the default server, please set `REACT_APP_SERVER` environment variables before build, +for example: `REACT_APP_SERVER=http://your.domain.com`. + +Before you start editing, make sure your `Actions on Save` options have `Optimize imports` & `Run Prettier` enabled. + +## Reference + +1. https://github.com/OIerDb-ng/OIerDb +2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example \ No newline at end of file diff --git a/web/package.json b/web/default/package.json similarity index 94% rename from web/package.json rename to web/default/package.json index a2bf3054..872ad36a 100644 --- a/web/package.json +++ b/web/default/package.json @@ -18,7 +18,7 @@ }, "scripts": { "start": "react-scripts start", - "build": "react-scripts build", + "build": "react-scripts build && mv build ../build/default", "test": "react-scripts test", "eject": "react-scripts eject" }, diff --git a/web/public/favicon.ico b/web/default/public/favicon.ico similarity index 100% rename from web/public/favicon.ico rename to web/default/public/favicon.ico diff --git a/web/public/index.html b/web/default/public/index.html similarity index 100% rename from web/public/index.html rename to web/default/public/index.html diff --git a/web/public/logo.png b/web/default/public/logo.png similarity index 100% rename from web/public/logo.png rename to web/default/public/logo.png diff --git a/web/public/robots.txt b/web/default/public/robots.txt similarity index 100% rename from web/public/robots.txt rename to web/default/public/robots.txt diff --git a/web/src/App.js b/web/default/src/App.js similarity index 100% rename from web/src/App.js rename to web/default/src/App.js diff --git a/web/src/components/ChannelsTable.js b/web/default/src/components/ChannelsTable.js similarity index 100% rename from web/src/components/ChannelsTable.js rename to web/default/src/components/ChannelsTable.js diff --git a/web/src/components/Footer.js b/web/default/src/components/Footer.js similarity index 100% rename from web/src/components/Footer.js rename to web/default/src/components/Footer.js diff --git a/web/src/components/GitHubOAuth.js b/web/default/src/components/GitHubOAuth.js similarity index 100% rename from web/src/components/GitHubOAuth.js rename to web/default/src/components/GitHubOAuth.js diff --git a/web/src/components/Header.js b/web/default/src/components/Header.js similarity index 100% rename from web/src/components/Header.js rename to web/default/src/components/Header.js diff --git a/web/src/components/Loading.js b/web/default/src/components/Loading.js similarity index 100% rename from web/src/components/Loading.js rename to web/default/src/components/Loading.js diff --git a/web/src/components/LoginForm.js b/web/default/src/components/LoginForm.js similarity index 100% rename from web/src/components/LoginForm.js rename to web/default/src/components/LoginForm.js diff --git a/web/src/components/LogsTable.js b/web/default/src/components/LogsTable.js similarity index 100% rename from web/src/components/LogsTable.js rename to web/default/src/components/LogsTable.js diff --git a/web/src/components/OperationSetting.js b/web/default/src/components/OperationSetting.js similarity index 100% rename from web/src/components/OperationSetting.js rename to web/default/src/components/OperationSetting.js diff --git a/web/src/components/OtherSetting.js b/web/default/src/components/OtherSetting.js similarity index 100% rename from web/src/components/OtherSetting.js rename to web/default/src/components/OtherSetting.js diff --git a/web/src/components/PasswordResetConfirm.js b/web/default/src/components/PasswordResetConfirm.js similarity index 100% rename from web/src/components/PasswordResetConfirm.js rename to web/default/src/components/PasswordResetConfirm.js diff --git a/web/src/components/PasswordResetForm.js b/web/default/src/components/PasswordResetForm.js similarity index 100% rename from web/src/components/PasswordResetForm.js rename to web/default/src/components/PasswordResetForm.js diff --git a/web/src/components/PersonalSetting.js b/web/default/src/components/PersonalSetting.js similarity index 100% rename from web/src/components/PersonalSetting.js rename to web/default/src/components/PersonalSetting.js diff --git a/web/src/components/PrivateRoute.js b/web/default/src/components/PrivateRoute.js similarity index 100% rename from web/src/components/PrivateRoute.js rename to web/default/src/components/PrivateRoute.js diff --git a/web/src/components/RedemptionsTable.js b/web/default/src/components/RedemptionsTable.js similarity index 100% rename from web/src/components/RedemptionsTable.js rename to web/default/src/components/RedemptionsTable.js diff --git a/web/src/components/RegisterForm.js b/web/default/src/components/RegisterForm.js similarity index 100% rename from web/src/components/RegisterForm.js rename to web/default/src/components/RegisterForm.js diff --git a/web/src/components/SystemSetting.js b/web/default/src/components/SystemSetting.js similarity index 100% rename from web/src/components/SystemSetting.js rename to web/default/src/components/SystemSetting.js diff --git a/web/src/components/TokensTable.js b/web/default/src/components/TokensTable.js similarity index 100% rename from web/src/components/TokensTable.js rename to web/default/src/components/TokensTable.js diff --git a/web/src/components/UsersTable.js b/web/default/src/components/UsersTable.js similarity index 100% rename from web/src/components/UsersTable.js rename to web/default/src/components/UsersTable.js diff --git a/web/src/components/utils.js b/web/default/src/components/utils.js similarity index 100% rename from web/src/components/utils.js rename to web/default/src/components/utils.js diff --git a/web/src/constants/channel.constants.js b/web/default/src/constants/channel.constants.js similarity index 100% rename from web/src/constants/channel.constants.js rename to web/default/src/constants/channel.constants.js diff --git a/web/src/constants/common.constant.js b/web/default/src/constants/common.constant.js similarity index 100% rename from web/src/constants/common.constant.js rename to web/default/src/constants/common.constant.js diff --git a/web/src/constants/index.js b/web/default/src/constants/index.js similarity index 100% rename from web/src/constants/index.js rename to web/default/src/constants/index.js diff --git a/web/src/constants/toast.constants.js b/web/default/src/constants/toast.constants.js similarity index 100% rename from web/src/constants/toast.constants.js rename to web/default/src/constants/toast.constants.js diff --git a/web/src/constants/user.constants.js b/web/default/src/constants/user.constants.js similarity index 100% rename from web/src/constants/user.constants.js rename to web/default/src/constants/user.constants.js diff --git a/web/src/context/Status/index.js b/web/default/src/context/Status/index.js similarity index 100% rename from web/src/context/Status/index.js rename to web/default/src/context/Status/index.js diff --git a/web/src/context/Status/reducer.js b/web/default/src/context/Status/reducer.js similarity index 100% rename from web/src/context/Status/reducer.js rename to web/default/src/context/Status/reducer.js diff --git a/web/src/context/User/index.js b/web/default/src/context/User/index.js similarity index 100% rename from web/src/context/User/index.js rename to web/default/src/context/User/index.js diff --git a/web/src/context/User/reducer.js b/web/default/src/context/User/reducer.js similarity index 100% rename from web/src/context/User/reducer.js rename to web/default/src/context/User/reducer.js diff --git a/web/src/helpers/api.js b/web/default/src/helpers/api.js similarity index 100% rename from web/src/helpers/api.js rename to web/default/src/helpers/api.js diff --git a/web/src/helpers/auth-header.js b/web/default/src/helpers/auth-header.js similarity index 100% rename from web/src/helpers/auth-header.js rename to web/default/src/helpers/auth-header.js diff --git a/web/src/helpers/history.js b/web/default/src/helpers/history.js similarity index 100% rename from web/src/helpers/history.js rename to web/default/src/helpers/history.js diff --git a/web/src/helpers/index.js b/web/default/src/helpers/index.js similarity index 100% rename from web/src/helpers/index.js rename to web/default/src/helpers/index.js diff --git a/web/src/helpers/render.js b/web/default/src/helpers/render.js similarity index 100% rename from web/src/helpers/render.js rename to web/default/src/helpers/render.js diff --git a/web/src/helpers/utils.js b/web/default/src/helpers/utils.js similarity index 100% rename from web/src/helpers/utils.js rename to web/default/src/helpers/utils.js diff --git a/web/src/index.css b/web/default/src/index.css similarity index 100% rename from web/src/index.css rename to web/default/src/index.css diff --git a/web/src/index.js b/web/default/src/index.js similarity index 100% rename from web/src/index.js rename to web/default/src/index.js diff --git a/web/src/pages/About/index.js b/web/default/src/pages/About/index.js similarity index 100% rename from web/src/pages/About/index.js rename to web/default/src/pages/About/index.js diff --git a/web/src/pages/Channel/EditChannel.js b/web/default/src/pages/Channel/EditChannel.js similarity index 100% rename from web/src/pages/Channel/EditChannel.js rename to web/default/src/pages/Channel/EditChannel.js diff --git a/web/src/pages/Channel/index.js b/web/default/src/pages/Channel/index.js similarity index 100% rename from web/src/pages/Channel/index.js rename to web/default/src/pages/Channel/index.js diff --git a/web/src/pages/Chat/index.js b/web/default/src/pages/Chat/index.js similarity index 100% rename from web/src/pages/Chat/index.js rename to web/default/src/pages/Chat/index.js diff --git a/web/src/pages/Home/index.js b/web/default/src/pages/Home/index.js similarity index 100% rename from web/src/pages/Home/index.js rename to web/default/src/pages/Home/index.js diff --git a/web/src/pages/Log/index.js b/web/default/src/pages/Log/index.js similarity index 100% rename from web/src/pages/Log/index.js rename to web/default/src/pages/Log/index.js diff --git a/web/src/pages/NotFound/index.js b/web/default/src/pages/NotFound/index.js similarity index 100% rename from web/src/pages/NotFound/index.js rename to web/default/src/pages/NotFound/index.js diff --git a/web/src/pages/Redemption/EditRedemption.js b/web/default/src/pages/Redemption/EditRedemption.js similarity index 100% rename from web/src/pages/Redemption/EditRedemption.js rename to web/default/src/pages/Redemption/EditRedemption.js diff --git a/web/src/pages/Redemption/index.js b/web/default/src/pages/Redemption/index.js similarity index 100% rename from web/src/pages/Redemption/index.js rename to web/default/src/pages/Redemption/index.js diff --git a/web/src/pages/Setting/index.js b/web/default/src/pages/Setting/index.js similarity index 100% rename from web/src/pages/Setting/index.js rename to web/default/src/pages/Setting/index.js diff --git a/web/src/pages/Token/EditToken.js b/web/default/src/pages/Token/EditToken.js similarity index 100% rename from web/src/pages/Token/EditToken.js rename to web/default/src/pages/Token/EditToken.js diff --git a/web/src/pages/Token/index.js b/web/default/src/pages/Token/index.js similarity index 100% rename from web/src/pages/Token/index.js rename to web/default/src/pages/Token/index.js diff --git a/web/src/pages/TopUp/index.js b/web/default/src/pages/TopUp/index.js similarity index 100% rename from web/src/pages/TopUp/index.js rename to web/default/src/pages/TopUp/index.js diff --git a/web/src/pages/User/AddUser.js b/web/default/src/pages/User/AddUser.js similarity index 100% rename from web/src/pages/User/AddUser.js rename to web/default/src/pages/User/AddUser.js diff --git a/web/src/pages/User/EditUser.js b/web/default/src/pages/User/EditUser.js similarity index 100% rename from web/src/pages/User/EditUser.js rename to web/default/src/pages/User/EditUser.js diff --git a/web/src/pages/User/index.js b/web/default/src/pages/User/index.js similarity index 100% rename from web/src/pages/User/index.js rename to web/default/src/pages/User/index.js diff --git a/web/vercel.json b/web/default/vercel.json similarity index 100% rename from web/vercel.json rename to web/default/vercel.json From 83f95935de30e74ac012e5b1048a76d9aca9aac2 Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 1 Jan 2024 19:23:46 +0800 Subject: [PATCH 19/23] ci: fix Dockerfile & ci --- .github/workflows/linux-release.yml | 13 +++++++++---- .github/workflows/macos-release.yml | 13 +++++++++---- .github/workflows/windows-release.yml | 5 +++++ Dockerfile | 11 +---------- README.md | 1 + web/build.sh | 13 +++++++++++++ 6 files changed, 38 insertions(+), 18 deletions(-) create mode 100644 web/build.sh diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml index d9375795..d93c70ca 100644 --- a/.github/workflows/linux-release.yml +++ b/.github/workflows/linux-release.yml @@ -7,6 +7,11 @@ on: tags: - '*' - '!*-alpha*' + workflow_dispatch: + inputs: + name: + description: 'reason' + required: false jobs: release: runs-on: ubuntu-latest @@ -22,10 +27,10 @@ jobs: env: CI: "" run: | - cd web/default - npm install - REACT_APP_VERSION=$(git describe --tags) npm run build - cd ../.. + cd web + git describe --tags > VERSION + REACT_APP_VERSION=$(git describe --tags) chmod u+x ./build.sh && ./build.sh + cd .. - name: Set up Go uses: actions/setup-go@v3 with: diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml index 69bb93f5..ce9d1f11 100644 --- a/.github/workflows/macos-release.yml +++ b/.github/workflows/macos-release.yml @@ -7,6 +7,11 @@ on: tags: - '*' - '!*-alpha*' + workflow_dispatch: + inputs: + name: + description: 'reason' + required: false jobs: release: runs-on: macos-latest @@ -22,10 +27,10 @@ jobs: env: CI: "" run: | - cd web/default - npm install - REACT_APP_VERSION=$(git describe --tags) npm run build - cd ../.. + cd web + git describe --tags > VERSION + REACT_APP_VERSION=$(git describe --tags) chmod u+x ./build.sh && ./build.sh + cd .. - name: Set up Go uses: actions/setup-go@v3 with: diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index c08e95d2..9b1f16ba 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -7,6 +7,11 @@ on: tags: - '*' - '!*-alpha*' + workflow_dispatch: + inputs: + name: + description: 'reason' + required: false jobs: release: runs-on: windows-latest diff --git a/Dockerfile b/Dockerfile index 56648168..b21a7b3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,16 +3,7 @@ FROM node:16 as builder WORKDIR /build COPY ./web . COPY ./VERSION . -RUN themes=$(cat THEMES) \ - && IFS=$'\n' \ - && for theme in $themes; do \ - theme_path="web/$theme" \ - && echo "Building theme: $theme" \ - && cd $theme_path \ - && npm install \ - && DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build \ - && cd /app \ - done +RUN chmod u+x ./build.sh && ./build.sh FROM golang AS builder2 diff --git a/README.md b/README.md index b53936c4..28f4e5e6 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 + 邮箱登录注册(支持注册邮箱白名单)以及通过邮箱进行密码重置。 + [GitHub 开放授权](https://github.com/settings/applications/new)。 + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 +23. 支持主题切换,设置环境变量 `THEME` 即可,默认为 `default`,欢迎 PR 更多主题,具体参考[此处](./web/README.md)。 ## 部署 ### 基于 Docker 进行部署 diff --git a/web/build.sh b/web/build.sh new file mode 100644 index 00000000..b3751ff4 --- /dev/null +++ b/web/build.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +version=$(cat VERSION) +themes=$(cat THEMES) +IFS=$'\n' + +for theme in $themes; do + echo "Building theme: $theme" + cd $theme + npm install + DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$version npm run build + cd .. +done From 0c022f17cb28e500c05670f3c89f757fbcc5c864 Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 1 Jan 2024 20:25:53 +0800 Subject: [PATCH 20/23] chore: update theme related code --- router/web-router.go | 8 ++------ web/README.md | 2 +- web/default/package.json | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/router/web-router.go b/router/web-router.go index 2f86db38..7328c7a3 100644 --- a/router/web-router.go +++ b/router/web-router.go @@ -14,6 +14,7 @@ import ( ) func SetWebRouter(router *gin.Engine, buildFS embed.FS) { + indexPageData, _ := buildFS.ReadFile(fmt.Sprintf("web/build/%s/index.html", common.Theme)) router.Use(gzip.Gzip(gzip.DefaultCompression)) router.Use(middleware.GlobalWebRateLimit()) router.Use(middleware.Cache()) @@ -24,11 +25,6 @@ func SetWebRouter(router *gin.Engine, buildFS embed.FS) { return } c.Header("Cache-Control", "no-cache") - indexPage, err := buildFS.ReadFile(fmt.Sprintf("web/build/%s/index.html", common.Theme)) - if err != nil { - controller.RelayNotFound(c) - return - } - c.Data(http.StatusOK, "text/html; charset=utf-8", indexPage) + c.Data(http.StatusOK, "text/html; charset=utf-8", indexPageData) }) } diff --git a/web/README.md b/web/README.md index 1454940f..dad20427 100644 --- a/web/README.md +++ b/web/README.md @@ -4,7 +4,7 @@ ## 提交新的主题 1. 在 `web` 文件夹下新建一个文件夹,文件夹名为主题名。 2. 把你的主题文件放到这个文件夹下。 -3. 修改 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv build ../build/default"`,其中 `default` 为你的主题名。 +3. 修改 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv -f build ../build/default"`,其中 `default` 为你的主题名。 ## 主题列表 ### default diff --git a/web/default/package.json b/web/default/package.json index 872ad36a..438f020c 100644 --- a/web/default/package.json +++ b/web/default/package.json @@ -18,7 +18,7 @@ }, "scripts": { "start": "react-scripts start", - "build": "react-scripts build && mv build ../build/default", + "build": "react-scripts build && mv -f build ../build/default", "test": "react-scripts test", "eject": "react-scripts eject" }, From 92886093ae6c21fe75abf3c81347c8e516c3177f Mon Sep 17 00:00:00 2001 From: JustSong <39998050+songquanpeng@users.noreply.github.com> Date: Mon, 1 Jan 2024 21:10:40 +0800 Subject: [PATCH 21/23] docs: update readme --- web/README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/web/README.md b/web/README.md index dad20427..30846c6f 100644 --- a/web/README.md +++ b/web/README.md @@ -7,5 +7,10 @@ 3. 修改 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv -f build ../build/default"`,其中 `default` 为你的主题名。 ## 主题列表 -### default -默认主题 \ No newline at end of file +### 主题:default +默认主题,由 [JustSong](https://github.com/songquanpeng) 开发。 + +预览: +|![image](https://github.com/songquanpeng/one-api/assets/39998050/ccfbc668-3a7f-4bc1-87da-7eacfd7bf371)|![image](https://github.com/songquanpeng/one-api/assets/39998050/a63ed547-44b9-45db-b43a-ecea07d60840)| +|:---:|:---:| + From 4a96031ce60d88f71a5290e8771b2ba24debd7dc Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 1 Jan 2024 21:14:45 +0800 Subject: [PATCH 22/23] docs: update readme --- README.md | 1 + web/README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 28f4e5e6..edc467aa 100644 --- a/README.md +++ b/README.md @@ -368,6 +368,7 @@ graph LR 15. `RELAY_TIMEOUT`:中继超时设置,单位为秒,默认不设置超时时间。 16. `SQLITE_BUSY_TIMEOUT`:SQLite 锁等待超时设置,单位为毫秒,默认 `3000`。 17. `GEMINI_SAFETY_SETTING`:Gemini 的安全设置,默认 `BLOCK_NONE`。 +18. `THEME`:系统的主题设置,默认为 `default`,具体可选值参考[此处](./web/README.md)。 ### 命令行参数 1. `--port `: 指定服务器监听的端口号,默认为 `3000`。 diff --git a/web/README.md b/web/README.md index 30846c6f..ca73b298 100644 --- a/web/README.md +++ b/web/README.md @@ -2,6 +2,7 @@ > 每个文件夹代表一个主题,欢迎提交你的主题 ## 提交新的主题 +> 欢迎在页面底部保留你和 One API 的版权信息以及指向链接 1. 在 `web` 文件夹下新建一个文件夹,文件夹名为主题名。 2. 把你的主题文件放到这个文件夹下。 3. 修改 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv -f build ../build/default"`,其中 `default` 为你的主题名。 From cbf8f077470fceb38fe9d262c42a64eb4be4eb0c Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 1 Jan 2024 21:19:37 +0800 Subject: [PATCH 23/23] docs: fix logo --- README.en.md | 2 +- README.ja.md | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.en.md b/README.en.md index 82dceb5b..e7f254f7 100644 --- a/README.en.md +++ b/README.en.md @@ -3,7 +3,7 @@

- one-api logo + one-api logo

diff --git a/README.ja.md b/README.ja.md index 089fc2b5..edfd2a28 100644 --- a/README.ja.md +++ b/README.ja.md @@ -3,7 +3,7 @@

- one-api logo + one-api logo

diff --git a/README.md b/README.md index edc467aa..27acfedd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

- one-api logo + one-api logo