Commit c821b0ed by Calcium-Ion Committed by GitHub

Merge branch 'alpha' into fix-balance-unit-sync

parents d30ae820 be71bbb6
...@@ -92,12 +92,12 @@ func RedisDel(key string) error { ...@@ -92,12 +92,12 @@ func RedisDel(key string) error {
return RDB.Del(ctx, key).Err() return RDB.Del(ctx, key).Err()
} }
func RedisHDelObj(key string) error { func RedisDelKey(key string) error {
if DebugEnabled { if DebugEnabled {
SysLog(fmt.Sprintf("Redis HDEL: key=%s", key)) SysLog(fmt.Sprintf("Redis DEL Key: key=%s", key))
} }
ctx := context.Background() ctx := context.Background()
return RDB.HDel(ctx, key).Err() return RDB.Del(ctx, key).Err()
} }
func RedisHSetObj(key string, obj interface{}, expiration time.Duration) error { func RedisHSetObj(key string, obj interface{}, expiration time.Duration) error {
......
...@@ -200,10 +200,10 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest { ...@@ -200,10 +200,10 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
} else { } else {
testRequest.MaxTokens = 10 testRequest.MaxTokens = 10
} }
content, _ := json.Marshal("hi")
testMessage := dto.Message{ testMessage := dto.Message{
Role: "user", Role: "user",
Content: content, Content: "hi",
} }
testRequest.Model = model testRequest.Model = model
testRequest.Messages = append(testRequest.Messages, testMessage) testRequest.Messages = append(testRequest.Messages, testMessage)
......
...@@ -623,3 +623,44 @@ func BatchSetChannelTag(c *gin.Context) { ...@@ -623,3 +623,44 @@ func BatchSetChannelTag(c *gin.Context) {
}) })
return return
} }
func GetTagModels(c *gin.Context) {
tag := c.Query("tag")
if tag == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "tag不能为空",
})
return
}
channels, err := model.GetChannelsByTag(tag, false) // Assuming false for idSort is fine here
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": err.Error(),
})
return
}
var longestModels string
maxLength := 0
// Find the longest models string among all channels with the given tag
for _, channel := range channels {
if channel.Models != "" {
currentModels := strings.Split(channel.Models, ",")
if len(currentModels) > maxLength {
maxLength = len(currentModels)
longestModels = channel.Models
}
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": longestModels,
})
return
}
...@@ -74,6 +74,7 @@ func GetStatus(c *gin.Context) { ...@@ -74,6 +74,7 @@ func GetStatus(c *gin.Context) {
"oidc_client_id": system_setting.GetOIDCSettings().ClientId, "oidc_client_id": system_setting.GetOIDCSettings().ClientId,
"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint, "oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
"setup": constant.Setup, "setup": constant.Setup,
"api_info": setting.GetApiInfo(),
}, },
}) })
return return
......
...@@ -119,7 +119,15 @@ func UpdateOption(c *gin.Context) { ...@@ -119,7 +119,15 @@ func UpdateOption(c *gin.Context) {
}) })
return return
} }
case "ApiInfo":
err = setting.ValidateApiInfo(option.Value)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
} }
err = model.UpdateOption(option.Key, option.Value) err = model.UpdateOption(option.Key, option.Value)
if err != nil { if err != nil {
......
package dto package dto
import "encoding/json" import (
"encoding/json"
"one-api/common"
)
type ClaudeMetadata struct { type ClaudeMetadata struct {
UserId string `json:"user_id"` UserId string `json:"user_id"`
...@@ -23,7 +26,7 @@ type ClaudeMediaMessage struct { ...@@ -23,7 +26,7 @@ type ClaudeMediaMessage struct {
Id string `json:"id,omitempty"` Id string `json:"id,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Input any `json:"input,omitempty"` Input any `json:"input,omitempty"`
Content json.RawMessage `json:"content,omitempty"` Content any `json:"content,omitempty"`
ToolUseId string `json:"tool_use_id,omitempty"` ToolUseId string `json:"tool_use_id,omitempty"`
} }
...@@ -39,15 +42,39 @@ func (c *ClaudeMediaMessage) GetText() string { ...@@ -39,15 +42,39 @@ func (c *ClaudeMediaMessage) GetText() string {
} }
func (c *ClaudeMediaMessage) IsStringContent() bool { func (c *ClaudeMediaMessage) IsStringContent() bool {
var content string if c.Content == nil {
return json.Unmarshal(c.Content, &content) == nil return false
}
_, ok := c.Content.(string)
if ok {
return true
}
return false
} }
func (c *ClaudeMediaMessage) GetStringContent() string { func (c *ClaudeMediaMessage) GetStringContent() string {
var content string if c.Content == nil {
if err := json.Unmarshal(c.Content, &content); err == nil { return ""
return content }
switch c.Content.(type) {
case string:
return c.Content.(string)
case []any:
var contentStr string
for _, contentItem := range c.Content.([]any) {
contentMap, ok := contentItem.(map[string]any)
if !ok {
continue
} }
if contentMap["type"] == ContentTypeText {
if subStr, ok := contentMap["text"].(string); ok {
contentStr += subStr
}
}
}
return contentStr
}
return "" return ""
} }
...@@ -57,16 +84,12 @@ func (c *ClaudeMediaMessage) GetJsonRowString() string { ...@@ -57,16 +84,12 @@ func (c *ClaudeMediaMessage) GetJsonRowString() string {
} }
func (c *ClaudeMediaMessage) SetContent(content any) { func (c *ClaudeMediaMessage) SetContent(content any) {
jsonContent, _ := json.Marshal(content) c.Content = content
c.Content = jsonContent
} }
func (c *ClaudeMediaMessage) ParseMediaContent() []ClaudeMediaMessage { func (c *ClaudeMediaMessage) ParseMediaContent() []ClaudeMediaMessage {
var mediaContent []ClaudeMediaMessage mediaContent, _ := common.Any2Type[[]ClaudeMediaMessage](c.Content)
if err := json.Unmarshal(c.Content, &mediaContent); err == nil {
return mediaContent return mediaContent
}
return make([]ClaudeMediaMessage, 0)
} }
type ClaudeMessageSource struct { type ClaudeMessageSource struct {
...@@ -82,14 +105,36 @@ type ClaudeMessage struct { ...@@ -82,14 +105,36 @@ type ClaudeMessage struct {
} }
func (c *ClaudeMessage) IsStringContent() bool { func (c *ClaudeMessage) IsStringContent() bool {
if c.Content == nil {
return false
}
_, ok := c.Content.(string) _, ok := c.Content.(string)
return ok return ok
} }
func (c *ClaudeMessage) GetStringContent() string { func (c *ClaudeMessage) GetStringContent() string {
if c.IsStringContent() { if c.Content == nil {
return ""
}
switch c.Content.(type) {
case string:
return c.Content.(string) return c.Content.(string)
case []any:
var contentStr string
for _, contentItem := range c.Content.([]any) {
contentMap, ok := contentItem.(map[string]any)
if !ok {
continue
}
if contentMap["type"] == ContentTypeText {
if subStr, ok := contentMap["text"].(string); ok {
contentStr += subStr
}
} }
}
return contentStr
}
return "" return ""
} }
...@@ -98,15 +143,7 @@ func (c *ClaudeMessage) SetStringContent(content string) { ...@@ -98,15 +143,7 @@ func (c *ClaudeMessage) SetStringContent(content string) {
} }
func (c *ClaudeMessage) ParseContent() ([]ClaudeMediaMessage, error) { func (c *ClaudeMessage) ParseContent() ([]ClaudeMediaMessage, error) {
// map content to []ClaudeMediaMessage return common.Any2Type[[]ClaudeMediaMessage](c.Content)
// parse to json
jsonContent, _ := json.Marshal(c.Content)
var contentList []ClaudeMediaMessage
err := json.Unmarshal(jsonContent, &contentList)
if err != nil {
return make([]ClaudeMediaMessage, 0), err
}
return contentList, nil
} }
type Tool struct { type Tool struct {
...@@ -161,14 +198,8 @@ func (c *ClaudeRequest) SetStringSystem(system string) { ...@@ -161,14 +198,8 @@ func (c *ClaudeRequest) SetStringSystem(system string) {
} }
func (c *ClaudeRequest) ParseSystem() []ClaudeMediaMessage { func (c *ClaudeRequest) ParseSystem() []ClaudeMediaMessage {
// map content to []ClaudeMediaMessage mediaContent, _ := common.Any2Type[[]ClaudeMediaMessage](c.System)
// parse to json return mediaContent
jsonContent, _ := json.Marshal(c.System)
var contentList []ClaudeMediaMessage
if err := json.Unmarshal(jsonContent, &contentList); err == nil {
return contentList
}
return make([]ClaudeMediaMessage, 0)
} }
type ClaudeError struct { type ClaudeError struct {
......
...@@ -37,11 +37,11 @@ type GeneralOpenAIRequest struct { ...@@ -37,11 +37,11 @@ type GeneralOpenAIRequest struct {
Input any `json:"input,omitempty"` Input any `json:"input,omitempty"`
Instruction string `json:"instruction,omitempty"` Instruction string `json:"instruction,omitempty"`
Size string `json:"size,omitempty"` Size string `json:"size,omitempty"`
Functions any `json:"functions,omitempty"` Functions json.RawMessage `json:"functions,omitempty"`
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
PresencePenalty float64 `json:"presence_penalty,omitempty"` PresencePenalty float64 `json:"presence_penalty,omitempty"`
ResponseFormat *ResponseFormat `json:"response_format,omitempty"` ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
EncodingFormat any `json:"encoding_format,omitempty"` EncodingFormat json.RawMessage `json:"encoding_format,omitempty"`
Seed float64 `json:"seed,omitempty"` Seed float64 `json:"seed,omitempty"`
ParallelTooCalls *bool `json:"parallel_tool_calls,omitempty"` ParallelTooCalls *bool `json:"parallel_tool_calls,omitempty"`
Tools []ToolCallRequest `json:"tools,omitempty"` Tools []ToolCallRequest `json:"tools,omitempty"`
...@@ -50,10 +50,10 @@ type GeneralOpenAIRequest struct { ...@@ -50,10 +50,10 @@ type GeneralOpenAIRequest struct {
LogProbs bool `json:"logprobs,omitempty"` LogProbs bool `json:"logprobs,omitempty"`
TopLogProbs int `json:"top_logprobs,omitempty"` TopLogProbs int `json:"top_logprobs,omitempty"`
Dimensions int `json:"dimensions,omitempty"` Dimensions int `json:"dimensions,omitempty"`
Modalities any `json:"modalities,omitempty"` Modalities json.RawMessage `json:"modalities,omitempty"`
Audio any `json:"audio,omitempty"` Audio json.RawMessage `json:"audio,omitempty"`
EnableThinking any `json:"enable_thinking,omitempty"` // ali EnableThinking any `json:"enable_thinking,omitempty"` // ali
ExtraBody any `json:"extra_body,omitempty"` ExtraBody json.RawMessage `json:"extra_body,omitempty"`
WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"` WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"`
// OpenRouter Params // OpenRouter Params
Reasoning json.RawMessage `json:"reasoning,omitempty"` Reasoning json.RawMessage `json:"reasoning,omitempty"`
...@@ -108,7 +108,7 @@ func (r *GeneralOpenAIRequest) ParseInput() []string { ...@@ -108,7 +108,7 @@ func (r *GeneralOpenAIRequest) ParseInput() []string {
type Message struct { type Message struct {
Role string `json:"role"` Role string `json:"role"`
Content json.RawMessage `json:"content"` Content any `json:"content"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Prefix *bool `json:"prefix,omitempty"` Prefix *bool `json:"prefix,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"` ReasoningContent string `json:"reasoning_content,omitempty"`
...@@ -116,7 +116,7 @@ type Message struct { ...@@ -116,7 +116,7 @@ type Message struct {
ToolCalls json.RawMessage `json:"tool_calls,omitempty"` ToolCalls json.RawMessage `json:"tool_calls,omitempty"`
ToolCallId string `json:"tool_call_id,omitempty"` ToolCallId string `json:"tool_call_id,omitempty"`
parsedContent []MediaContent parsedContent []MediaContent
parsedStringContent *string //parsedStringContent *string
} }
type MediaContent struct { type MediaContent struct {
...@@ -132,22 +132,51 @@ type MediaContent struct { ...@@ -132,22 +132,51 @@ type MediaContent struct {
func (m *MediaContent) GetImageMedia() *MessageImageUrl { func (m *MediaContent) GetImageMedia() *MessageImageUrl {
if m.ImageUrl != nil { if m.ImageUrl != nil {
if _, ok := m.ImageUrl.(*MessageImageUrl); ok {
return m.ImageUrl.(*MessageImageUrl) return m.ImageUrl.(*MessageImageUrl)
} }
if itemMap, ok := m.ImageUrl.(map[string]any); ok {
out := &MessageImageUrl{
Url: common.Interface2String(itemMap["url"]),
Detail: common.Interface2String(itemMap["detail"]),
MimeType: common.Interface2String(itemMap["mime_type"]),
}
return out
}
}
return nil return nil
} }
func (m *MediaContent) GetInputAudio() *MessageInputAudio { func (m *MediaContent) GetInputAudio() *MessageInputAudio {
if m.InputAudio != nil { if m.InputAudio != nil {
if _, ok := m.InputAudio.(*MessageInputAudio); ok {
return m.InputAudio.(*MessageInputAudio) return m.InputAudio.(*MessageInputAudio)
} }
if itemMap, ok := m.InputAudio.(map[string]any); ok {
out := &MessageInputAudio{
Data: common.Interface2String(itemMap["data"]),
Format: common.Interface2String(itemMap["format"]),
}
return out
}
}
return nil return nil
} }
func (m *MediaContent) GetFile() *MessageFile { func (m *MediaContent) GetFile() *MessageFile {
if m.File != nil { if m.File != nil {
if _, ok := m.File.(*MessageFile); ok {
return m.File.(*MessageFile) return m.File.(*MessageFile)
} }
if itemMap, ok := m.File.(map[string]any); ok {
out := &MessageFile{
FileName: common.Interface2String(itemMap["file_name"]),
FileData: common.Interface2String(itemMap["file_data"]),
FileId: common.Interface2String(itemMap["file_id"]),
}
return out
}
}
return nil return nil
} }
...@@ -212,6 +241,186 @@ func (m *Message) SetToolCalls(toolCalls any) { ...@@ -212,6 +241,186 @@ func (m *Message) SetToolCalls(toolCalls any) {
} }
func (m *Message) StringContent() string { func (m *Message) StringContent() string {
switch m.Content.(type) {
case string:
return m.Content.(string)
case []any:
var contentStr string
for _, contentItem := range m.Content.([]any) {
contentMap, ok := contentItem.(map[string]any)
if !ok {
continue
}
if contentMap["type"] == ContentTypeText {
if subStr, ok := contentMap["text"].(string); ok {
contentStr += subStr
}
}
}
return contentStr
}
return ""
}
func (m *Message) SetNullContent() {
m.Content = nil
m.parsedContent = nil
}
func (m *Message) SetStringContent(content string) {
m.Content = content
m.parsedContent = nil
}
func (m *Message) SetMediaContent(content []MediaContent) {
m.Content = content
m.parsedContent = content
}
func (m *Message) IsStringContent() bool {
_, ok := m.Content.(string)
if ok {
return true
}
return false
}
func (m *Message) ParseContent() []MediaContent {
if m.Content == nil {
return nil
}
if len(m.parsedContent) > 0 {
return m.parsedContent
}
var contentList []MediaContent
// 先尝试解析为字符串
content, ok := m.Content.(string)
if ok {
contentList = []MediaContent{{
Type: ContentTypeText,
Text: content,
}}
m.parsedContent = contentList
return contentList
}
// 尝试解析为数组
//var arrayContent []map[string]interface{}
arrayContent, ok := m.Content.([]any)
if !ok {
return contentList
}
for _, contentItemAny := range arrayContent {
mediaItem, ok := contentItemAny.(MediaContent)
if ok {
contentList = append(contentList, mediaItem)
continue
}
contentItem, ok := contentItemAny.(map[string]any)
if !ok {
continue
}
contentType, ok := contentItem["type"].(string)
if !ok {
continue
}
switch contentType {
case ContentTypeText:
if text, ok := contentItem["text"].(string); ok {
contentList = append(contentList, MediaContent{
Type: ContentTypeText,
Text: text,
})
}
case ContentTypeImageURL:
imageUrl := contentItem["image_url"]
temp := &MessageImageUrl{
Detail: "high",
}
switch v := imageUrl.(type) {
case string:
temp.Url = v
case map[string]interface{}:
url, ok1 := v["url"].(string)
detail, ok2 := v["detail"].(string)
if ok2 {
temp.Detail = detail
}
if ok1 {
temp.Url = url
}
}
contentList = append(contentList, MediaContent{
Type: ContentTypeImageURL,
ImageUrl: temp,
})
case ContentTypeInputAudio:
if audioData, ok := contentItem["input_audio"].(map[string]interface{}); ok {
data, ok1 := audioData["data"].(string)
format, ok2 := audioData["format"].(string)
if ok1 && ok2 {
temp := &MessageInputAudio{
Data: data,
Format: format,
}
contentList = append(contentList, MediaContent{
Type: ContentTypeInputAudio,
InputAudio: temp,
})
}
}
case ContentTypeFile:
if fileData, ok := contentItem["file"].(map[string]interface{}); ok {
fileId, ok3 := fileData["file_id"].(string)
if ok3 {
contentList = append(contentList, MediaContent{
Type: ContentTypeFile,
File: &MessageFile{
FileId: fileId,
},
})
} else {
fileName, ok1 := fileData["filename"].(string)
fileDataStr, ok2 := fileData["file_data"].(string)
if ok1 && ok2 {
contentList = append(contentList, MediaContent{
Type: ContentTypeFile,
File: &MessageFile{
FileName: fileName,
FileData: fileDataStr,
},
})
}
}
}
case ContentTypeVideoUrl:
if videoUrl, ok := contentItem["video_url"].(string); ok {
contentList = append(contentList, MediaContent{
Type: ContentTypeVideoUrl,
VideoUrl: &MessageVideoUrl{
Url: videoUrl,
},
})
}
}
}
if len(contentList) > 0 {
m.parsedContent = contentList
}
return contentList
}
// old code
/*func (m *Message) StringContent() string {
if m.parsedStringContent != nil { if m.parsedStringContent != nil {
return *m.parsedStringContent return *m.parsedStringContent
} }
...@@ -382,7 +591,7 @@ func (m *Message) ParseContent() []MediaContent { ...@@ -382,7 +591,7 @@ func (m *Message) ParseContent() []MediaContent {
m.parsedContent = contentList m.parsedContent = contentList
} }
return contentList return contentList
} }*/
type WebSearchOptions struct { type WebSearchOptions struct {
SearchContextSize string `json:"search_context_size,omitempty"` SearchContextSize string `json:"search_context_size,omitempty"`
......
...@@ -7,7 +7,7 @@ all: build-frontend start-backend ...@@ -7,7 +7,7 @@ all: build-frontend start-backend
build-frontend: build-frontend:
@echo "Building frontend..." @echo "Building frontend..."
@cd $(FRONTEND_DIR) && npm install && DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) npm run build @cd $(FRONTEND_DIR) && bun install && DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
start-backend: start-backend:
@echo "Starting backend dev server..." @echo "Starting backend dev server..."
......
...@@ -122,6 +122,7 @@ func InitOptionMap() { ...@@ -122,6 +122,7 @@ func InitOptionMap() {
common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString() common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength) common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString() common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString()
common.OptionMap["ApiInfo"] = ""
// 自动添加所有注册的模型配置 // 自动添加所有注册的模型配置
modelConfigs := config.GlobalConfig.ExportAllConfigs() modelConfigs := config.GlobalConfig.ExportAllConfigs()
......
...@@ -19,7 +19,7 @@ func cacheSetToken(token Token) error { ...@@ -19,7 +19,7 @@ func cacheSetToken(token Token) error {
func cacheDeleteToken(key string) error { func cacheDeleteToken(key string) error {
key = common.GenerateHMAC(key) key = common.GenerateHMAC(key)
err := common.RedisHDelObj(fmt.Sprintf("token:%s", key)) err := common.RedisDelKey(fmt.Sprintf("token:%s", key))
if err != nil { if err != nil {
return err return err
} }
......
...@@ -3,11 +3,12 @@ package model ...@@ -3,11 +3,12 @@ package model
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gin-gonic/gin"
"one-api/common" "one-api/common"
"one-api/constant" "one-api/constant"
"time" "time"
"github.com/gin-gonic/gin"
"github.com/bytedance/gopkg/util/gopool" "github.com/bytedance/gopkg/util/gopool"
) )
...@@ -57,7 +58,7 @@ func invalidateUserCache(userId int) error { ...@@ -57,7 +58,7 @@ func invalidateUserCache(userId int) error {
if !common.RedisEnabled { if !common.RedisEnabled {
return nil return nil
} }
return common.RedisHDelObj(getUserCacheKey(userId)) return common.RedisDelKey(getUserCacheKey(userId))
} }
// updateUserCache updates all user cache fields using hash // updateUserCache updates all user cache fields using hash
......
...@@ -96,12 +96,11 @@ func embeddingResponseAli2OpenAI(response *AliEmbeddingResponse, model string) * ...@@ -96,12 +96,11 @@ func embeddingResponseAli2OpenAI(response *AliEmbeddingResponse, model string) *
} }
func responseAli2OpenAI(response *AliResponse) *dto.OpenAITextResponse { func responseAli2OpenAI(response *AliResponse) *dto.OpenAITextResponse {
content, _ := json.Marshal(response.Output.Text)
choice := dto.OpenAITextResponseChoice{ choice := dto.OpenAITextResponseChoice{
Index: 0, Index: 0,
Message: dto.Message{ Message: dto.Message{
Role: "assistant", Role: "assistant",
Content: content, Content: response.Output.Text,
}, },
FinishReason: response.Output.FinishReason, FinishReason: response.Output.FinishReason,
} }
......
...@@ -53,12 +53,11 @@ func requestOpenAI2Baidu(request dto.GeneralOpenAIRequest) *BaiduChatRequest { ...@@ -53,12 +53,11 @@ func requestOpenAI2Baidu(request dto.GeneralOpenAIRequest) *BaiduChatRequest {
} }
func responseBaidu2OpenAI(response *BaiduChatResponse) *dto.OpenAITextResponse { func responseBaidu2OpenAI(response *BaiduChatResponse) *dto.OpenAITextResponse {
content, _ := json.Marshal(response.Result)
choice := dto.OpenAITextResponseChoice{ choice := dto.OpenAITextResponseChoice{
Index: 0, Index: 0,
Message: dto.Message{ Message: dto.Message{
Role: "assistant", Role: "assistant",
Content: content, Content: response.Result,
}, },
FinishReason: "stop", FinishReason: "stop",
} }
......
...@@ -48,9 +48,9 @@ func RequestOpenAI2ClaudeComplete(textRequest dto.GeneralOpenAIRequest) *dto.Cla ...@@ -48,9 +48,9 @@ func RequestOpenAI2ClaudeComplete(textRequest dto.GeneralOpenAIRequest) *dto.Cla
prompt := "" prompt := ""
for _, message := range textRequest.Messages { for _, message := range textRequest.Messages {
if message.Role == "user" { if message.Role == "user" {
prompt += fmt.Sprintf("\n\nHuman: %s", message.Content) prompt += fmt.Sprintf("\n\nHuman: %s", message.StringContent())
} else if message.Role == "assistant" { } else if message.Role == "assistant" {
prompt += fmt.Sprintf("\n\nAssistant: %s", message.Content) prompt += fmt.Sprintf("\n\nAssistant: %s", message.StringContent())
} else if message.Role == "system" { } else if message.Role == "system" {
if prompt == "" { if prompt == "" {
prompt = message.StringContent() prompt = message.StringContent()
...@@ -155,15 +155,13 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.Cla ...@@ -155,15 +155,13 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.Cla
} }
if lastMessage.Role == message.Role && lastMessage.Role != "tool" { if lastMessage.Role == message.Role && lastMessage.Role != "tool" {
if lastMessage.IsStringContent() && message.IsStringContent() { if lastMessage.IsStringContent() && message.IsStringContent() {
content, _ := json.Marshal(strings.Trim(fmt.Sprintf("%s %s", lastMessage.StringContent(), message.StringContent()), "\"")) fmtMessage.SetStringContent(strings.Trim(fmt.Sprintf("%s %s", lastMessage.StringContent(), message.StringContent()), "\""))
fmtMessage.Content = content
// delete last message // delete last message
formatMessages = formatMessages[:len(formatMessages)-1] formatMessages = formatMessages[:len(formatMessages)-1]
} }
} }
if fmtMessage.Content == nil { if fmtMessage.Content == nil {
content, _ := json.Marshal("...") fmtMessage.SetStringContent("...")
fmtMessage.Content = content
} }
formatMessages = append(formatMessages, fmtMessage) formatMessages = append(formatMessages, fmtMessage)
lastMessage = fmtMessage lastMessage = fmtMessage
...@@ -397,12 +395,11 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse) *dto ...@@ -397,12 +395,11 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse) *dto
thinkingContent := "" thinkingContent := ""
if reqMode == RequestModeCompletion { if reqMode == RequestModeCompletion {
content, _ := json.Marshal(strings.TrimPrefix(claudeResponse.Completion, " "))
choice := dto.OpenAITextResponseChoice{ choice := dto.OpenAITextResponseChoice{
Index: 0, Index: 0,
Message: dto.Message{ Message: dto.Message{
Role: "assistant", Role: "assistant",
Content: content, Content: strings.TrimPrefix(claudeResponse.Completion, " "),
Name: nil, Name: nil,
}, },
FinishReason: stopReasonClaude2OpenAI(claudeResponse.StopReason), FinishReason: stopReasonClaude2OpenAI(claudeResponse.StopReason),
......
...@@ -195,11 +195,10 @@ func cohereHandler(c *gin.Context, resp *http.Response, modelName string, prompt ...@@ -195,11 +195,10 @@ func cohereHandler(c *gin.Context, resp *http.Response, modelName string, prompt
openaiResp.Model = modelName openaiResp.Model = modelName
openaiResp.Usage = usage openaiResp.Usage = usage
content, _ := json.Marshal(cohereResp.Text)
openaiResp.Choices = []dto.OpenAITextResponseChoice{ openaiResp.Choices = []dto.OpenAITextResponseChoice{
{ {
Index: 0, Index: 0,
Message: dto.Message{Content: content, Role: "assistant"}, Message: dto.Message{Content: cohereResp.Text, Role: "assistant"},
FinishReason: stopReasonCohere2OpenAI(cohereResp.FinishReason), FinishReason: stopReasonCohere2OpenAI(cohereResp.FinishReason),
}, },
} }
......
...@@ -10,7 +10,7 @@ type CozeError struct { ...@@ -10,7 +10,7 @@ type CozeError struct {
type CozeEnterMessage struct { type CozeEnterMessage struct {
Role string `json:"role"` Role string `json:"role"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Content json.RawMessage `json:"content,omitempty"` Content any `json:"content,omitempty"`
MetaData json.RawMessage `json:"meta_data,omitempty"` MetaData json.RawMessage `json:"meta_data,omitempty"`
ContentType string `json:"content_type,omitempty"` ContentType string `json:"content_type,omitempty"`
} }
......
...@@ -278,12 +278,11 @@ func difyHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInf ...@@ -278,12 +278,11 @@ func difyHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInf
Created: common.GetTimestamp(), Created: common.GetTimestamp(),
Usage: difyResponse.MetaData.Usage, Usage: difyResponse.MetaData.Usage,
} }
content, _ := json.Marshal(difyResponse.Answer)
choice := dto.OpenAITextResponseChoice{ choice := dto.OpenAITextResponseChoice{
Index: 0, Index: 0,
Message: dto.Message{ Message: dto.Message{
Role: "assistant", Role: "assistant",
Content: content, Content: difyResponse.Answer,
}, },
FinishReason: "stop", FinishReason: "stop",
} }
......
...@@ -175,12 +175,6 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon ...@@ -175,12 +175,6 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon
// common.SysLog("tools: " + fmt.Sprintf("%+v", geminiRequest.Tools)) // common.SysLog("tools: " + fmt.Sprintf("%+v", geminiRequest.Tools))
// json_data, _ := json.Marshal(geminiRequest.Tools) // json_data, _ := json.Marshal(geminiRequest.Tools)
// common.SysLog("tools_json: " + string(json_data)) // common.SysLog("tools_json: " + string(json_data))
} else if textRequest.Functions != nil {
//geminiRequest.Tools = []GeminiChatTool{
// {
// FunctionDeclarations: textRequest.Functions,
// },
//}
} }
if textRequest.ResponseFormat != nil && (textRequest.ResponseFormat.Type == "json_schema" || textRequest.ResponseFormat.Type == "json_object") { if textRequest.ResponseFormat != nil && (textRequest.ResponseFormat.Type == "json_schema" || textRequest.ResponseFormat.Type == "json_object") {
...@@ -609,14 +603,13 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp ...@@ -609,14 +603,13 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
Created: common.GetTimestamp(), Created: common.GetTimestamp(),
Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.Candidates)), Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.Candidates)),
} }
content, _ := json.Marshal("")
isToolCall := false isToolCall := false
for _, candidate := range response.Candidates { for _, candidate := range response.Candidates {
choice := dto.OpenAITextResponseChoice{ choice := dto.OpenAITextResponseChoice{
Index: int(candidate.Index), Index: int(candidate.Index),
Message: dto.Message{ Message: dto.Message{
Role: "assistant", Role: "assistant",
Content: content, Content: "",
}, },
FinishReason: constant.FinishReasonStop, FinishReason: constant.FinishReasonStop,
} }
......
...@@ -47,7 +47,7 @@ func requestOpenAI2Mistral(request *dto.GeneralOpenAIRequest) *dto.GeneralOpenAI ...@@ -47,7 +47,7 @@ func requestOpenAI2Mistral(request *dto.GeneralOpenAIRequest) *dto.GeneralOpenAI
} }
mediaMessages := message.ParseContent() mediaMessages := message.ParseContent()
if message.Role == "assistant" && message.ToolCalls != nil && string(message.Content) == "null" { if message.Role == "assistant" && message.ToolCalls != nil && message.Content == "" {
mediaMessages = []dto.MediaContent{} mediaMessages = []dto.MediaContent{}
} }
for j, mediaMessage := range mediaMessages { for j, mediaMessage := range mediaMessages {
......
...@@ -45,12 +45,11 @@ func responsePaLM2OpenAI(response *PaLMChatResponse) *dto.OpenAITextResponse { ...@@ -45,12 +45,11 @@ func responsePaLM2OpenAI(response *PaLMChatResponse) *dto.OpenAITextResponse {
Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.Candidates)), Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.Candidates)),
} }
for i, candidate := range response.Candidates { for i, candidate := range response.Candidates {
content, _ := json.Marshal(candidate.Content)
choice := dto.OpenAITextResponseChoice{ choice := dto.OpenAITextResponseChoice{
Index: i, Index: i,
Message: dto.Message{ Message: dto.Message{
Role: "assistant", Role: "assistant",
Content: content, Content: candidate.Content,
}, },
FinishReason: "stop", FinishReason: "stop",
} }
......
...@@ -56,12 +56,11 @@ func responseTencent2OpenAI(response *TencentChatResponse) *dto.OpenAITextRespon ...@@ -56,12 +56,11 @@ func responseTencent2OpenAI(response *TencentChatResponse) *dto.OpenAITextRespon
}, },
} }
if len(response.Choices) > 0 { if len(response.Choices) > 0 {
content, _ := json.Marshal(response.Choices[0].Messages.Content)
choice := dto.OpenAITextResponseChoice{ choice := dto.OpenAITextResponseChoice{
Index: 0, Index: 0,
Message: dto.Message{ Message: dto.Message{
Role: "assistant", Role: "assistant",
Content: content, Content: response.Choices[0].Messages.Content,
}, },
FinishReason: response.Choices[0].FinishReason, FinishReason: response.Choices[0].FinishReason,
} }
......
...@@ -61,12 +61,11 @@ func responseXunfei2OpenAI(response *XunfeiChatResponse) *dto.OpenAITextResponse ...@@ -61,12 +61,11 @@ func responseXunfei2OpenAI(response *XunfeiChatResponse) *dto.OpenAITextResponse
}, },
} }
} }
content, _ := json.Marshal(response.Payload.Choices.Text[0].Content)
choice := dto.OpenAITextResponseChoice{ choice := dto.OpenAITextResponseChoice{
Index: 0, Index: 0,
Message: dto.Message{ Message: dto.Message{
Role: "assistant", Role: "assistant",
Content: content, Content: response.Payload.Choices.Text[0].Content,
}, },
FinishReason: constant.FinishReasonStop, FinishReason: constant.FinishReasonStop,
} }
......
...@@ -108,12 +108,11 @@ func responseZhipu2OpenAI(response *ZhipuResponse) *dto.OpenAITextResponse { ...@@ -108,12 +108,11 @@ func responseZhipu2OpenAI(response *ZhipuResponse) *dto.OpenAITextResponse {
Usage: response.Data.Usage, Usage: response.Data.Usage,
} }
for i, choice := range response.Data.Choices { for i, choice := range response.Data.Choices {
content, _ := json.Marshal(strings.Trim(choice.Content, "\""))
openaiChoice := dto.OpenAITextResponseChoice{ openaiChoice := dto.OpenAITextResponseChoice{
Index: i, Index: i,
Message: dto.Message{ Message: dto.Message{
Role: choice.Role, Role: choice.Role,
Content: content, Content: strings.Trim(choice.Content, "\""),
}, },
FinishReason: "", FinishReason: "",
} }
......
...@@ -105,6 +105,7 @@ func SetApiRouter(router *gin.Engine) { ...@@ -105,6 +105,7 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.GET("/fetch_models/:id", controller.FetchUpstreamModels) channelRoute.GET("/fetch_models/:id", controller.FetchUpstreamModels)
channelRoute.POST("/fetch_models", controller.FetchModels) channelRoute.POST("/fetch_models", controller.FetchModels)
channelRoute.POST("/batch/tag", controller.BatchSetChannelTag) channelRoute.POST("/batch/tag", controller.BatchSetChannelTag)
channelRoute.GET("/tag/models", controller.GetTagModels)
} }
tokenRoute := apiRouter.Group("/token") tokenRoute := apiRouter.Group("/token")
tokenRoute.Use(middleware.UserAuth()) tokenRoute.Use(middleware.UserAuth())
......
...@@ -261,16 +261,20 @@ func CountTokenClaudeMessages(messages []dto.ClaudeMessage, model string, stream ...@@ -261,16 +261,20 @@ func CountTokenClaudeMessages(messages []dto.ClaudeMessage, model string, stream
//} //}
tokenNum += 1000 tokenNum += 1000
case "tool_use": case "tool_use":
if mediaMessage.Input != nil {
tokenNum += getTokenNum(tokenEncoder, mediaMessage.Name) tokenNum += getTokenNum(tokenEncoder, mediaMessage.Name)
inputJSON, _ := json.Marshal(mediaMessage.Input) inputJSON, _ := json.Marshal(mediaMessage.Input)
tokenNum += getTokenNum(tokenEncoder, string(inputJSON)) tokenNum += getTokenNum(tokenEncoder, string(inputJSON))
}
case "tool_result": case "tool_result":
if mediaMessage.Content != nil {
contentJSON, _ := json.Marshal(mediaMessage.Content) contentJSON, _ := json.Marshal(mediaMessage.Content)
tokenNum += getTokenNum(tokenEncoder, string(contentJSON)) tokenNum += getTokenNum(tokenEncoder, string(contentJSON))
} }
} }
} }
} }
}
// Add a constant for message formatting (this may need adjustment based on Claude's exact formatting) // Add a constant for message formatting (this may need adjustment based on Claude's exact formatting)
tokenNum += len(messages) * 2 // Assuming 2 tokens per message for formatting tokenNum += len(messages) * 2 // Assuming 2 tokens per message for formatting
...@@ -386,7 +390,7 @@ func CountTokenMessages(info *relaycommon.RelayInfo, messages []dto.Message, mod ...@@ -386,7 +390,7 @@ func CountTokenMessages(info *relaycommon.RelayInfo, messages []dto.Message, mod
for _, message := range messages { for _, message := range messages {
tokenNum += tokensPerMessage tokenNum += tokensPerMessage
tokenNum += getTokenNum(tokenEncoder, message.Role) tokenNum += getTokenNum(tokenEncoder, message.Role)
if len(message.Content) > 0 { if message.Content != nil {
if message.Name != nil { if message.Name != nil {
tokenNum += tokensPerName tokenNum += tokensPerName
tokenNum += getTokenNum(tokenEncoder, *message.Name) tokenNum += getTokenNum(tokenEncoder, *message.Name)
......
package setting
import (
"encoding/json"
"fmt"
"net/url"
"one-api/common"
"regexp"
"strings"
)
// ValidateApiInfo 验证API信息格式
func ValidateApiInfo(apiInfoStr string) error {
if apiInfoStr == "" {
return nil // 空字符串是合法的
}
var apiInfoList []map[string]interface{}
if err := json.Unmarshal([]byte(apiInfoStr), &apiInfoList); err != nil {
return fmt.Errorf("API信息格式错误:%s", err.Error())
}
// 验证数组长度
if len(apiInfoList) > 50 {
return fmt.Errorf("API信息数量不能超过50个")
}
// 允许的颜色值
validColors := map[string]bool{
"blue": true, "green": true, "cyan": true, "purple": true, "pink": true,
"red": true, "orange": true, "amber": true, "yellow": true, "lime": true,
"light-green": true, "teal": true, "light-blue": true, "indigo": true,
"violet": true, "grey": true,
}
// URL正则表达式
urlRegex := regexp.MustCompile(`^https?://[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(/.*)?$`)
for i, apiInfo := range apiInfoList {
// 检查必填字段
urlStr, ok := apiInfo["url"].(string)
if !ok || urlStr == "" {
return fmt.Errorf("第%d个API信息缺少URL字段", i+1)
}
route, ok := apiInfo["route"].(string)
if !ok || route == "" {
return fmt.Errorf("第%d个API信息缺少线路描述字段", i+1)
}
description, ok := apiInfo["description"].(string)
if !ok || description == "" {
return fmt.Errorf("第%d个API信息缺少说明字段", i+1)
}
color, ok := apiInfo["color"].(string)
if !ok || color == "" {
return fmt.Errorf("第%d个API信息缺少颜色字段", i+1)
}
// 验证URL格式
if !urlRegex.MatchString(urlStr) {
return fmt.Errorf("第%d个API信息的URL格式不正确", i+1)
}
// 验证URL可解析性
if _, err := url.Parse(urlStr); err != nil {
return fmt.Errorf("第%d个API信息的URL无法解析:%s", i+1, err.Error())
}
// 验证字段长度
if len(urlStr) > 500 {
return fmt.Errorf("第%d个API信息的URL长度不能超过500字符", i+1)
}
if len(route) > 100 {
return fmt.Errorf("第%d个API信息的线路描述长度不能超过100字符", i+1)
}
if len(description) > 200 {
return fmt.Errorf("第%d个API信息的说明长度不能超过200字符", i+1)
}
// 验证颜色值
if !validColors[color] {
return fmt.Errorf("第%d个API信息的颜色值不合法", i+1)
}
// 检查并过滤危险字符(防止XSS)
dangerousChars := []string{"<script", "<iframe", "javascript:", "onload=", "onerror=", "onclick="}
for _, dangerous := range dangerousChars {
if strings.Contains(strings.ToLower(description), dangerous) {
return fmt.Errorf("第%d个API信息的说明包含不允许的内容", i+1)
}
if strings.Contains(strings.ToLower(route), dangerous) {
return fmt.Errorf("第%d个API信息的线路描述包含不允许的内容", i+1)
}
}
}
return nil
}
// GetApiInfo 获取API信息列表
func GetApiInfo() []map[string]interface{} {
// 从OptionMap中获取API信息,如果不存在则返回空数组
common.OptionMapRWMutex.RLock()
apiInfoStr, exists := common.OptionMap["ApiInfo"]
common.OptionMapRWMutex.RUnlock()
if !exists || apiInfoStr == "" {
// 如果没有配置,返回空数组
return []map[string]interface{}{}
}
// 解析存储的API信息
var apiInfo []map[string]interface{}
if err := json.Unmarshal([]byte(apiInfoStr), &apiInfo); err != nil {
// 如果解析失败,返回空数组
return []map[string]interface{}{}
}
return apiInfo
}
\ No newline at end of file
...@@ -32,7 +32,6 @@ import OIDCIcon from '../common/logo/OIDCIcon.js'; ...@@ -32,7 +32,6 @@ import OIDCIcon from '../common/logo/OIDCIcon.js';
import WeChatIcon from '../common/logo/WeChatIcon.js'; import WeChatIcon from '../common/logo/WeChatIcon.js';
import LinuxDoIcon from '../common/logo/LinuxDoIcon.js'; import LinuxDoIcon from '../common/logo/LinuxDoIcon.js';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Background from '/example.png';
const LoginForm = () => { const LoginForm = () => {
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
...@@ -266,7 +265,7 @@ const LoginForm = () => { ...@@ -266,7 +265,7 @@ const LoginForm = () => {
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="flex items-center justify-center mb-6 gap-2"> <div className="flex items-center justify-center mb-6 gap-2">
<img src={logo} alt="Logo" className="h-10 rounded-full" /> <img src={logo} alt="Logo" className="h-10 rounded-full" />
<Title heading={3} className='!text-white'>{systemName}</Title> <Title heading={3} className='!text-gray-800'>{systemName}</Title>
</div> </div>
<Card className="shadow-xl border-0 !rounded-2xl overflow-hidden"> <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
...@@ -500,19 +499,8 @@ const LoginForm = () => { ...@@ -500,19 +499,8 @@ const LoginForm = () => {
}; };
return ( return (
<div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden"> <div className="bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
{/* 背景图片容器 - 放大并保持居中 */} <div className="w-full max-w-sm">
<div
className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
style={{
backgroundImage: `url(${Background})`
}}
></div>
{/* 半透明遮罩层 */}
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/30 via-blue-500/30 to-purple-500/30 backdrop-blur-sm z-0"></div>
<div className="w-full max-w-sm relative z-10">
{showEmailLogin || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth) {showEmailLogin || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
? renderEmailLoginForm() ? renderEmailLoginForm()
: renderOAuthOptions()} : renderOAuthOptions()}
......
...@@ -4,7 +4,6 @@ import { useSearchParams, Link } from 'react-router-dom'; ...@@ -4,7 +4,6 @@ import { useSearchParams, Link } from 'react-router-dom';
import { Button, Card, Form, Typography, Banner } from '@douyinfe/semi-ui'; import { Button, Card, Form, Typography, Banner } from '@douyinfe/semi-ui';
import { IconMail, IconLock, IconCopy } from '@douyinfe/semi-icons'; import { IconMail, IconLock, IconCopy } from '@douyinfe/semi-icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Background from '/example.png';
const { Text, Title } = Typography; const { Text, Title } = Typography;
...@@ -79,24 +78,13 @@ const PasswordResetConfirm = () => { ...@@ -79,24 +78,13 @@ const PasswordResetConfirm = () => {
} }
return ( return (
<div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden"> <div className="bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
{/* 背景图片容器 - 放大并保持居中 */} <div className="w-full max-w-sm">
<div
className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
style={{
backgroundImage: `url(${Background})`
}}
></div>
{/* 半透明遮罩层 */}
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/30 via-blue-500/30 to-purple-500/30 backdrop-blur-sm z-0"></div>
<div className="w-full max-w-sm relative z-10">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="flex items-center justify-center mb-6 gap-2"> <div className="flex items-center justify-center mb-6 gap-2">
<img src={logo} alt="Logo" className="h-10 rounded-full" /> <img src={logo} alt="Logo" className="h-10 rounded-full" />
<Title heading={3} className='!text-white'>{systemName}</Title> <Title heading={3} className='!text-gray-800'>{systemName}</Title>
</div> </div>
<Card className="shadow-xl border-0 !rounded-2xl overflow-hidden"> <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
......
...@@ -5,7 +5,6 @@ import { Button, Card, Form, Typography } from '@douyinfe/semi-ui'; ...@@ -5,7 +5,6 @@ import { Button, Card, Form, Typography } from '@douyinfe/semi-ui';
import { IconMail } from '@douyinfe/semi-icons'; import { IconMail } from '@douyinfe/semi-icons';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Background from '/example.png';
const { Text, Title } = Typography; const { Text, Title } = Typography;
...@@ -79,24 +78,13 @@ const PasswordResetForm = () => { ...@@ -79,24 +78,13 @@ const PasswordResetForm = () => {
} }
return ( return (
<div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden"> <div className="bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
{/* 背景图片容器 - 放大并保持居中 */} <div className="w-full max-w-sm">
<div
className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
style={{
backgroundImage: `url(${Background})`
}}
></div>
{/* 半透明遮罩层 */}
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/30 via-blue-500/30 to-purple-500/30 backdrop-blur-sm z-0"></div>
<div className="w-full max-w-sm relative z-10">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="flex items-center justify-center mb-6 gap-2"> <div className="flex items-center justify-center mb-6 gap-2">
<img src={logo} alt="Logo" className="h-10 rounded-full" /> <img src={logo} alt="Logo" className="h-10 rounded-full" />
<Title heading={3} className='!text-white'>{systemName}</Title> <Title heading={3} className='!text-gray-800'>{systemName}</Title>
</div> </div>
<Card className="shadow-xl border-0 !rounded-2xl overflow-hidden"> <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
......
...@@ -33,7 +33,6 @@ import WeChatIcon from '../common/logo/WeChatIcon.js'; ...@@ -33,7 +33,6 @@ import WeChatIcon from '../common/logo/WeChatIcon.js';
import TelegramLoginButton from 'react-telegram-login/src'; import TelegramLoginButton from 'react-telegram-login/src';
import { UserContext } from '../../context/User/index.js'; import { UserContext } from '../../context/User/index.js';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Background from '/example.png';
const RegisterForm = () => { const RegisterForm = () => {
const { t } = useTranslation(); const { t } = useTranslation();
...@@ -272,7 +271,7 @@ const RegisterForm = () => { ...@@ -272,7 +271,7 @@ const RegisterForm = () => {
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="flex items-center justify-center mb-6 gap-2"> <div className="flex items-center justify-center mb-6 gap-2">
<img src={logo} alt="Logo" className="h-10 rounded-full" /> <img src={logo} alt="Logo" className="h-10 rounded-full" />
<Title heading={3} className='!text-white'>{systemName}</Title> <Title heading={3} className='!text-gray-800'>{systemName}</Title>
</div> </div>
<Card className="shadow-xl border-0 !rounded-2xl overflow-hidden"> <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
...@@ -379,7 +378,7 @@ const RegisterForm = () => { ...@@ -379,7 +378,7 @@ const RegisterForm = () => {
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="flex items-center justify-center mb-6 gap-2"> <div className="flex items-center justify-center mb-6 gap-2">
<img src={logo} alt="Logo" className="h-10 rounded-full" /> <img src={logo} alt="Logo" className="h-10 rounded-full" />
<Title heading={3} className='!text-white'>{systemName}</Title> <Title heading={3} className='!text-gray-800'>{systemName}</Title>
</div> </div>
<Card className="shadow-xl border-0 !rounded-2xl overflow-hidden"> <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
...@@ -542,17 +541,8 @@ const RegisterForm = () => { ...@@ -542,17 +541,8 @@ const RegisterForm = () => {
}; };
return ( return (
<div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden"> <div className="bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div <div className="w-full max-w-sm">
className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
style={{
backgroundImage: `url(${Background})`
}}
></div>
<div className="absolute inset-0 bg-gradient-to-br from-teal-500/30 via-blue-500/30 to-purple-500/30 backdrop-blur-sm z-0"></div>
<div className="w-full max-w-sm relative z-10">
{showEmailRegister || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth) {showEmailRegister || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
? renderEmailRegisterForm() ? renderEmailRegisterForm()
: renderOAuthOptions()} : renderOAuthOptions()}
......
import React, { useEffect, useState } from 'react';
import { Card, Spin } from '@douyinfe/semi-ui';
import { API, showError } from '../../helpers';
import SettingsAPIInfo from '../../pages/Setting/Dashboard/SettingsAPIInfo.js';
const DashboardSetting = () => {
let [inputs, setInputs] = useState({
ApiInfo: '',
});
let [loading, setLoading] = useState(false);
const getOptions = async () => {
const res = await API.get('/api/option/');
const { success, message, data } = res.data;
if (success) {
let newInputs = {};
data.forEach((item) => {
if (item.key in inputs) {
newInputs[item.key] = item.value;
}
});
setInputs(newInputs);
} else {
showError(message);
}
};
async function onRefresh() {
try {
setLoading(true);
await getOptions();
} catch (error) {
showError('刷新失败');
console.error(error);
} finally {
setLoading(false);
}
}
useEffect(() => {
onRefresh();
}, []);
return (
<>
<Spin spinning={loading} size='large'>
{/* API信息管理 */}
<Card style={{ marginTop: '10px' }}>
<SettingsAPIInfo options={inputs} refresh={onRefresh} />
</Card>
</Spin>
</>
);
};
export default DashboardSetting;
\ No newline at end of file
...@@ -17,14 +17,19 @@ import { ...@@ -17,14 +17,19 @@ import {
Tabs, Tabs,
TabPane, TabPane,
Dropdown, Dropdown,
Empty
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
IllustrationNoResult,
IllustrationNoResultDark
} from '@douyinfe/semi-illustrations';
import {
IconVerify, IconVerify,
IconHelpCircle, IconHelpCircle,
IconSearch, IconSearch,
IconCopy, IconCopy,
IconInfoCircle, IconInfoCircle,
IconLayers, IconLayers
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
import { UserContext } from '../../context/User/index.js'; import { UserContext } from '../../context/User/index.js';
import { AlertCircle } from 'lucide-react'; import { AlertCircle } from 'lucide-react';
...@@ -489,6 +494,14 @@ const ModelPricing = () => { ...@@ -489,6 +494,14 @@ const ModelPricing = () => {
loading={loading} loading={loading}
rowSelection={rowSelection} rowSelection={rowSelection}
className="custom-table" className="custom-table"
empty={
<Empty
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
description={t('搜索无结果')}
style={{ padding: 30 }}
/>
}
pagination={{ pagination={{
defaultPageSize: 10, defaultPageSize: 10,
pageSize: pageSize, pageSize: pageSize,
......
...@@ -8,21 +8,34 @@ import { ...@@ -8,21 +8,34 @@ import {
renderQuota renderQuota
} from '../../helpers'; } from '../../helpers';
import {
CheckCircle,
XCircle,
Minus,
HelpCircle,
Coins
} from 'lucide-react';
import { ITEMS_PER_PAGE } from '../../constants'; import { ITEMS_PER_PAGE } from '../../constants';
import { import {
Button, Button,
Card, Card,
Divider, Divider,
Dropdown, Dropdown,
Input, Empty,
Form,
Modal, Modal,
Popover, Popover,
Space, Space,
Table, Table,
Tag, Tag,
Typography, Typography
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
IllustrationNoResult,
IllustrationNoResultDark
} from '@douyinfe/semi-illustrations';
import {
IconPlus, IconPlus,
IconCopy, IconCopy,
IconSearch, IconSearch,
...@@ -31,7 +44,7 @@ import { ...@@ -31,7 +44,7 @@ import {
IconDelete, IconDelete,
IconStop, IconStop,
IconPlay, IconPlay,
IconMore, IconMore
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
import EditRedemption from '../../pages/Redemption/EditRedemption'; import EditRedemption from '../../pages/Redemption/EditRedemption';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
...@@ -49,25 +62,25 @@ const RedemptionsTable = () => { ...@@ -49,25 +62,25 @@ const RedemptionsTable = () => {
switch (status) { switch (status) {
case 1: case 1:
return ( return (
<Tag color='green' size='large' shape='circle'> <Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('未使用')} {t('未使用')}
</Tag> </Tag>
); );
case 2: case 2:
return ( return (
<Tag color='red' size='large' shape='circle'> <Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
{t('已禁用')} {t('已禁用')}
</Tag> </Tag>
); );
case 3: case 3:
return ( return (
<Tag color='grey' size='large' shape='circle'> <Tag color='grey' size='large' shape='circle' prefixIcon={<Minus size={14} />}>
{t('已使用')} {t('已使用')}
</Tag> </Tag>
); );
default: default:
return ( return (
<Tag color='black' size='large' shape='circle'> <Tag color='black' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知状态')} {t('未知状态')}
</Tag> </Tag>
); );
...@@ -95,7 +108,13 @@ const RedemptionsTable = () => { ...@@ -95,7 +108,13 @@ const RedemptionsTable = () => {
title: t('额度'), title: t('额度'),
dataIndex: 'quota', dataIndex: 'quota',
render: (text, record, index) => { render: (text, record, index) => {
return <div>{renderQuota(parseInt(text))}</div>; return (
<div>
<Tag size={'large'} color={'grey'} shape='circle' prefixIcon={<Coins size={14} />}>
{renderQuota(parseInt(text))}
</Tag>
</div>
);
}, },
}, },
{ {
...@@ -223,7 +242,6 @@ const RedemptionsTable = () => { ...@@ -223,7 +242,6 @@ const RedemptionsTable = () => {
const [redemptions, setRedemptions] = useState([]); const [redemptions, setRedemptions] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(1); const [activePage, setActivePage] = useState(1);
const [searchKeyword, setSearchKeyword] = useState('');
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE); const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE);
const [selectedKeys, setSelectedKeys] = useState([]); const [selectedKeys, setSelectedKeys] = useState([]);
...@@ -233,6 +251,22 @@ const RedemptionsTable = () => { ...@@ -233,6 +251,22 @@ const RedemptionsTable = () => {
}); });
const [showEdit, setShowEdit] = useState(false); const [showEdit, setShowEdit] = useState(false);
// Form 初始值
const formInitValues = {
searchKeyword: '',
};
// Form API 引用
const [formApi, setFormApi] = useState(null);
// 获取表单值的辅助函数
const getFormValues = () => {
const formValues = formApi ? formApi.getValues() : {};
return {
searchKeyword: formValues.searchKeyword || '',
};
};
const closeEdit = () => { const closeEdit = () => {
setShowEdit(false); setShowEdit(false);
setTimeout(() => { setTimeout(() => {
...@@ -340,8 +374,14 @@ const RedemptionsTable = () => { ...@@ -340,8 +374,14 @@ const RedemptionsTable = () => {
setLoading(false); setLoading(false);
}; };
const searchRedemptions = async (keyword, page, pageSize) => { const searchRedemptions = async (keyword = null, page, pageSize) => {
if (searchKeyword === '') { // 如果没有传递keyword参数,从表单获取值
if (keyword === null) {
const formValues = getFormValues();
keyword = formValues.searchKeyword;
}
if (keyword === '') {
await loadRedemptions(page, pageSize); await loadRedemptions(page, pageSize);
return; return;
} }
...@@ -361,10 +401,6 @@ const RedemptionsTable = () => { ...@@ -361,10 +401,6 @@ const RedemptionsTable = () => {
setSearching(false); setSearching(false);
}; };
const handleKeywordChange = async (value) => {
setSearchKeyword(value.trim());
};
const sortRedemption = (key) => { const sortRedemption = (key) => {
if (redemptions.length === 0) return; if (redemptions.length === 0) return;
setLoading(true); setLoading(true);
...@@ -381,6 +417,7 @@ const RedemptionsTable = () => { ...@@ -381,6 +417,7 @@ const RedemptionsTable = () => {
const handlePageChange = (page) => { const handlePageChange = (page) => {
setActivePage(page); setActivePage(page);
const { searchKeyword } = getFormValues();
if (searchKeyword === '') { if (searchKeyword === '') {
loadRedemptions(page, pageSize).then(); loadRedemptions(page, pageSize).then();
} else { } else {
...@@ -457,29 +494,60 @@ const RedemptionsTable = () => { ...@@ -457,29 +494,60 @@ const RedemptionsTable = () => {
</Button> </Button>
</div> </div>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2"> <Form
initValues={formInitValues}
getFormApi={(api) => setFormApi(api)}
onSubmit={() => {
setActivePage(1);
searchRedemptions(null, 1, pageSize);
}}
allowEmpty={true}
autoComplete="off"
layout="horizontal"
trigger="change"
stopValidateWithError={false}
className="w-full md:w-auto order-1 md:order-2"
>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<div className="relative w-full md:w-64"> <div className="relative w-full md:w-64">
<Input <Form.Input
field="searchKeyword"
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder={t('关键字(id或者名称)')} placeholder={t('关键字(id或者名称)')}
value={searchKeyword}
onChange={handleKeywordChange}
className="!rounded-full" className="!rounded-full"
showClear showClear
pure
/> />
</div> </div>
<div className="flex gap-2 w-full md:w-auto">
<Button <Button
type="primary" type="primary"
htmlType="submit"
loading={loading || searching}
className="!rounded-full flex-1 md:flex-initial md:w-auto"
>
{t('查询')}
</Button>
<Button
theme="light"
onClick={() => { onClick={() => {
searchRedemptions(searchKeyword, 1, pageSize).then(); if (formApi) {
formApi.reset();
// 重置后立即查询,使用setTimeout确保表单重置完成
setTimeout(() => {
setActivePage(1);
loadRedemptions(1, pageSize);
}, 100);
}
}} }}
loading={searching} className="!rounded-full flex-1 md:flex-initial md:w-auto"
className="!rounded-full w-full md:w-auto"
> >
{t('查询')} {t('重置')}
</Button> </Button>
</div> </div>
</div> </div>
</Form>
</div>
</div> </div>
); );
...@@ -517,6 +585,7 @@ const RedemptionsTable = () => { ...@@ -517,6 +585,7 @@ const RedemptionsTable = () => {
onPageSizeChange: (size) => { onPageSizeChange: (size) => {
setPageSize(size); setPageSize(size);
setActivePage(1); setActivePage(1);
const { searchKeyword } = getFormValues();
if (searchKeyword === '') { if (searchKeyword === '') {
loadRedemptions(1, size).then(); loadRedemptions(1, size).then();
} else { } else {
...@@ -528,6 +597,14 @@ const RedemptionsTable = () => { ...@@ -528,6 +597,14 @@ const RedemptionsTable = () => {
loading={loading} loading={loading}
rowSelection={rowSelection} rowSelection={rowSelection}
onRow={handleRow} onRow={handleRow}
empty={
<Empty
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
description={t('搜索无结果')}
style={{ padding: 30 }}
/>
}
className="rounded-xl overflow-hidden" className="rounded-xl overflow-hidden"
size="middle" size="middle"
></Table> ></Table>
......
...@@ -6,7 +6,8 @@ import { ...@@ -6,7 +6,8 @@ import {
showSuccess, showSuccess,
timestamp2string, timestamp2string,
renderGroup, renderGroup,
renderQuota renderQuota,
getQuotaPerUnit
} from '../../helpers'; } from '../../helpers';
import { ITEMS_PER_PAGE } from '../../constants'; import { ITEMS_PER_PAGE } from '../../constants';
...@@ -14,13 +15,29 @@ import { ...@@ -14,13 +15,29 @@ import {
Button, Button,
Card, Card,
Dropdown, Dropdown,
Empty,
Form,
Modal, Modal,
Space, Space,
SplitButtonGroup, SplitButtonGroup,
Table, Table,
Tag, Tag
Input,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import {
IllustrationNoResult,
IllustrationNoResultDark
} from '@douyinfe/semi-illustrations';
import {
CheckCircle,
Shield,
XCircle,
Clock,
Gauge,
HelpCircle,
Infinity,
Coins
} from 'lucide-react';
import { import {
IconPlus, IconPlus,
...@@ -32,7 +49,7 @@ import { ...@@ -32,7 +49,7 @@ import {
IconDelete, IconDelete,
IconStop, IconStop,
IconPlay, IconPlay,
IconMore, IconMore
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
import EditToken from '../../pages/Token/EditToken'; import EditToken from '../../pages/Token/EditToken';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
...@@ -49,38 +66,38 @@ const TokensTable = () => { ...@@ -49,38 +66,38 @@ const TokensTable = () => {
case 1: case 1:
if (model_limits_enabled) { if (model_limits_enabled) {
return ( return (
<Tag color='green' size='large' shape='circle'> <Tag color='green' size='large' shape='circle' prefixIcon={<Shield size={14} />}>
{t('已启用:限制模型')} {t('已启用:限制模型')}
</Tag> </Tag>
); );
} else { } else {
return ( return (
<Tag color='green' size='large' shape='circle'> <Tag color='green' size='large' shape='circle' prefixIcon={<CheckCircle size={14} />}>
{t('已启用')} {t('已启用')}
</Tag> </Tag>
); );
} }
case 2: case 2:
return ( return (
<Tag color='red' size='large' shape='circle'> <Tag color='red' size='large' shape='circle' prefixIcon={<XCircle size={14} />}>
{t('已禁用')} {t('已禁用')}
</Tag> </Tag>
); );
case 3: case 3:
return ( return (
<Tag color='yellow' size='large' shape='circle'> <Tag color='yellow' size='large' shape='circle' prefixIcon={<Clock size={14} />}>
{t('已过期')} {t('已过期')}
</Tag> </Tag>
); );
case 4: case 4:
return ( return (
<Tag color='grey' size='large' shape='circle'> <Tag color='grey' size='large' shape='circle' prefixIcon={<Gauge size={14} />}>
{t('已耗尽')} {t('已耗尽')}
</Tag> </Tag>
); );
default: default:
return ( return (
<Tag color='black' size='large' shape='circle'> <Tag color='black' size='large' shape='circle' prefixIcon={<HelpCircle size={14} />}>
{t('未知状态')} {t('未知状态')}
</Tag> </Tag>
); );
...@@ -111,21 +128,45 @@ const TokensTable = () => { ...@@ -111,21 +128,45 @@ const TokensTable = () => {
title: t('已用额度'), title: t('已用额度'),
dataIndex: 'used_quota', dataIndex: 'used_quota',
render: (text, record, index) => { render: (text, record, index) => {
return <div>{renderQuota(parseInt(text))}</div>; return (
<div>
<Tag size={'large'} color={'grey'} shape='circle' prefixIcon={<Coins size={14} />}>
{renderQuota(parseInt(text))}
</Tag>
</div>
);
}, },
}, },
{ {
title: t('剩余额度'), title: t('剩余额度'),
dataIndex: 'remain_quota', dataIndex: 'remain_quota',
render: (text, record, index) => { render: (text, record, index) => {
const getQuotaColor = (quotaValue) => {
const quotaPerUnit = getQuotaPerUnit();
const dollarAmount = quotaValue / quotaPerUnit;
if (dollarAmount <= 0) {
return 'red';
} else if (dollarAmount <= 100) {
return 'yellow';
} else {
return 'green';
}
};
return ( return (
<div> <div>
{record.unlimited_quota ? ( {record.unlimited_quota ? (
<Tag size={'large'} color={'white'} shape='circle'> <Tag size={'large'} color={'white'} shape='circle' prefixIcon={<Infinity size={14} />}>
{t('无限制')} {t('无限制')}
</Tag> </Tag>
) : ( ) : (
<Tag size={'large'} color={'light-blue'} shape='circle'> <Tag
size={'large'}
color={getQuotaColor(parseInt(text))}
shape='circle'
prefixIcon={<Coins size={14} />}
>
{renderQuota(parseInt(text))} {renderQuota(parseInt(text))}
</Tag> </Tag>
)} )}
...@@ -335,14 +376,29 @@ const TokensTable = () => { ...@@ -335,14 +376,29 @@ const TokensTable = () => {
const [tokenCount, setTokenCount] = useState(pageSize); const [tokenCount, setTokenCount] = useState(pageSize);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(1); const [activePage, setActivePage] = useState(1);
const [searchKeyword, setSearchKeyword] = useState('');
const [searchToken, setSearchToken] = useState('');
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
const [chats, setChats] = useState([]);
const [editingToken, setEditingToken] = useState({ const [editingToken, setEditingToken] = useState({
id: undefined, id: undefined,
}); });
// Form 初始值
const formInitValues = {
searchKeyword: '',
searchToken: '',
};
// Form API 引用
const [formApi, setFormApi] = useState(null);
// 获取表单值的辅助函数
const getFormValues = () => {
const formValues = formApi ? formApi.getValues() : {};
return {
searchKeyword: formValues.searchKeyword || '',
searchToken: formValues.searchToken || '',
};
};
const closeEdit = () => { const closeEdit = () => {
setShowEdit(false); setShowEdit(false);
setTimeout(() => { setTimeout(() => {
...@@ -416,8 +472,6 @@ const TokensTable = () => { ...@@ -416,8 +472,6 @@ const TokensTable = () => {
window.open(url, '_blank'); window.open(url, '_blank');
}; };
useEffect(() => { useEffect(() => {
loadTokens(0) loadTokens(0)
.then() .then()
...@@ -472,6 +526,7 @@ const TokensTable = () => { ...@@ -472,6 +526,7 @@ const TokensTable = () => {
}; };
const searchTokens = async () => { const searchTokens = async () => {
const { searchKeyword, searchToken } = getFormValues();
if (searchKeyword === '' && searchToken === '') { if (searchKeyword === '' && searchToken === '') {
await loadTokens(0); await loadTokens(0);
setActivePage(1); setActivePage(1);
...@@ -491,14 +546,6 @@ const TokensTable = () => { ...@@ -491,14 +546,6 @@ const TokensTable = () => {
setSearching(false); setSearching(false);
}; };
const handleKeywordChange = async (value) => {
setSearchKeyword(value.trim());
};
const handleSearchTokenChange = async (value) => {
setSearchToken(value.trim());
};
const sortToken = (key) => { const sortToken = (key) => {
if (tokens.length === 0) return; if (tokens.length === 0) return;
setLoading(true); setLoading(true);
...@@ -580,36 +627,65 @@ const TokensTable = () => { ...@@ -580,36 +627,65 @@ const TokensTable = () => {
</Button> </Button>
</div> </div>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2"> <Form
initValues={formInitValues}
getFormApi={(api) => setFormApi(api)}
onSubmit={searchTokens}
allowEmpty={true}
autoComplete="off"
layout="horizontal"
trigger="change"
stopValidateWithError={false}
className="w-full md:w-auto order-1 md:order-2"
>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<div className="relative w-full md:w-56"> <div className="relative w-full md:w-56">
<Input <Form.Input
field="searchKeyword"
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder={t('搜索关键字')} placeholder={t('搜索关键字')}
value={searchKeyword}
onChange={handleKeywordChange}
className="!rounded-full" className="!rounded-full"
showClear showClear
pure
/> />
</div> </div>
<div className="relative w-full md:w-56"> <div className="relative w-full md:w-56">
<Input <Form.Input
field="searchToken"
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder={t('密钥')} placeholder={t('密钥')}
value={searchToken}
onChange={handleSearchTokenChange}
className="!rounded-full" className="!rounded-full"
showClear showClear
pure
/> />
</div> </div>
<div className="flex gap-2 w-full md:w-auto">
<Button <Button
type="primary" type="primary"
onClick={searchTokens} htmlType="submit"
loading={searching} loading={searching}
className="!rounded-full w-full md:w-auto" className="!rounded-full flex-1 md:flex-initial md:w-auto"
> >
{t('查询')} {t('查询')}
</Button> </Button>
<Button
theme="light"
onClick={() => {
if (formApi) {
formApi.reset();
// 重置后立即查询,使用setTimeout确保表单重置完成
setTimeout(() => {
searchTokens();
}, 100);
}
}}
className="!rounded-full flex-1 md:flex-initial md:w-auto"
>
{t('重置')}
</Button>
</div>
</div> </div>
</Form>
</div> </div>
</div> </div>
); );
...@@ -654,6 +730,14 @@ const TokensTable = () => { ...@@ -654,6 +730,14 @@ const TokensTable = () => {
loading={loading} loading={loading}
rowSelection={rowSelection} rowSelection={rowSelection}
onRow={handleRow} onRow={handleRow}
empty={
<Empty
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
description={t('搜索无结果')}
style={{ padding: 30 }}
/>
}
className="rounded-xl overflow-hidden" className="rounded-xl overflow-hidden"
size="middle" size="middle"
></Table> ></Table>
......
...@@ -24,6 +24,13 @@ import { ...@@ -24,6 +24,13 @@ import {
XAI, XAI,
Ollama, Ollama,
Doubao, Doubao,
Suno,
Xinference,
OpenRouter,
Dify,
Coze,
SiliconCloud,
FastGPT
} from '@lobehub/icons'; } from '@lobehub/icons';
import { import {
...@@ -40,6 +47,7 @@ import { ...@@ -40,6 +47,7 @@ import {
User, User,
Settings, Settings,
CircleUser, CircleUser,
Users
} from 'lucide-react'; } from 'lucide-react';
// 侧边栏图标颜色映射 // 侧边栏图标颜色映射
...@@ -308,6 +316,88 @@ export const getModelCategories = (() => { ...@@ -308,6 +316,88 @@ export const getModelCategories = (() => {
}; };
})(); })();
/**
* 根据渠道类型返回对应的厂商图标
* @param {number} channelType - 渠道类型值
* @returns {JSX.Element|null} - 对应的厂商图标组件
*/
export function getChannelIcon(channelType) {
const iconSize = 14;
switch (channelType) {
case 1: // OpenAI
case 3: // Azure OpenAI
return <OpenAI size={iconSize} />;
case 2: // Midjourney Proxy
case 5: // Midjourney Proxy Plus
return <Midjourney size={iconSize} />;
case 36: // Suno API
return <Suno size={iconSize} />;
case 4: // Ollama
return <Ollama size={iconSize} />;
case 14: // Anthropic Claude
case 33: // AWS Claude
return <Claude.Color size={iconSize} />;
case 41: // Vertex AI
return <Gemini.Color size={iconSize} />;
case 34: // Cohere
return <Cohere.Color size={iconSize} />;
case 39: // Cloudflare
return <Cloudflare.Color size={iconSize} />;
case 43: // DeepSeek
return <DeepSeek.Color size={iconSize} />;
case 15: // 百度文心千帆
case 46: // 百度文心千帆V2
return <Wenxin.Color size={iconSize} />;
case 17: // 阿里通义千问
return <Qwen.Color size={iconSize} />;
case 18: // 讯飞星火认知
return <Spark.Color size={iconSize} />;
case 16: // 智谱 ChatGLM
case 26: // 智谱 GLM-4V
return <Zhipu.Color size={iconSize} />;
case 24: // Google Gemini
case 11: // Google PaLM2
return <Gemini.Color size={iconSize} />;
case 47: // Xinference
return <Xinference.Color size={iconSize} />;
case 25: // Moonshot
return <Moonshot size={iconSize} />;
case 20: // OpenRouter
return <OpenRouter size={iconSize} />;
case 19: // 360 智脑
return <Ai360.Color size={iconSize} />;
case 23: // 腾讯混元
return <Hunyuan.Color size={iconSize} />;
case 31: // 零一万物
return <Yi.Color size={iconSize} />;
case 35: // MiniMax
return <Minimax.Color size={iconSize} />;
case 37: // Dify
return <Dify.Color size={iconSize} />;
case 38: // Jina
return <Jina size={iconSize} />;
case 40: // SiliconCloud
return <SiliconCloud.Color size={iconSize} />;
case 42: // Mistral AI
return <Mistral.Color size={iconSize} />;
case 45: // 字节火山方舟、豆包通用
return <Doubao.Color size={iconSize} />;
case 48: // xAI
return <XAI size={iconSize} />;
case 49: // Coze
return <Coze size={iconSize} />;
case 8: // 自定义渠道
case 22: // 知识库:FastGPT
return <FastGPT.Color size={iconSize} />;
case 21: // 知识库:AI Proxy
case 44: // 嵌入模型:MokaAI M3E
default:
return null; // 未知类型或自定义渠道不显示图标
}
}
// 颜色列表 // 颜色列表
const colors = [ const colors = [
'amber', 'amber',
...@@ -519,7 +609,7 @@ export function renderGroup(group) { ...@@ -519,7 +609,7 @@ export function renderGroup(group) {
showSuccess(i18next.t('已复制:') + group); showSuccess(i18next.t('已复制:') + group);
} else { } else {
Modal.error({ Modal.error({
title: t('无法复制到剪贴板,请手动复制'), title: i18next.t('无法复制到剪贴板,请手动复制'),
content: group, content: group,
}); });
} }
......
...@@ -1567,5 +1567,24 @@ ...@@ -1567,5 +1567,24 @@
"使用统计": "Usage Statistics", "使用统计": "Usage Statistics",
"资源消耗": "Resource Consumption", "资源消耗": "Resource Consumption",
"性能指标": "Performance Indicators", "性能指标": "Performance Indicators",
"模型数据分析": "Model Data Analysis" "模型数据分析": "Model Data Analysis",
"搜索无结果": "No results found",
"仪表盘配置": "Dashboard Configuration",
"API信息管理,可以配置多个API地址用于状态展示和负载均衡": "API information management, you can configure multiple API addresses for status display and load balancing",
"线路描述": "Route description",
"颜色": "Color",
"标识颜色": "Identifier color",
"添加API": "Add API",
"保存配置": "Save Configuration",
"API信息": "API Information",
"暂无API信息配置": "No API information configured",
"暂无API信息": "No API information",
"请输入API地址": "Please enter the API address",
"请输入线路描述": "Please enter the route description",
"如:大带宽批量分析图片推荐": "e.g. Large bandwidth batch analysis of image recommendations",
"请输入说明": "Please enter the description",
"如:香港线路": "e.g. Hong Kong line",
"请联系管理员在系统设置中配置API信息": "Please contact the administrator to configure API information in the system settings.",
"确定要删除此API信息吗?": "Are you sure you want to delete this API information?",
"测速": "Speed Test"
} }
\ No newline at end of file
...@@ -73,6 +73,10 @@ code { ...@@ -73,6 +73,10 @@ code {
.semi-page-item, .semi-page-item,
.semi-navigation-item, .semi-navigation-item,
.semi-tag-closable, .semi-tag-closable,
.semi-input-wrapper,
.semi-tabs-tab-button,
.semi-select,
.semi-button,
.semi-datepicker-range-input { .semi-datepicker-range-input {
border-radius: 9999px !important; border-radius: 9999px !important;
} }
...@@ -322,6 +326,24 @@ code { ...@@ -322,6 +326,24 @@ code {
font-size: 1.1em; font-size: 1.1em;
} }
/* API信息卡片样式 */
.api-info-container {
position: relative;
}
.api-info-fade-indicator {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 30px;
background: linear-gradient(transparent, var(--semi-color-bg-1));
pointer-events: none;
z-index: 1;
opacity: 0;
transition: opacity 0.3s ease;
}
/* ==================== 调试面板特定样式 ==================== */ /* ==================== 调试面板特定样式 ==================== */
.debug-panel .semi-tabs { .debug-panel .semi-tabs {
height: 100% !important; height: 100% !important;
...@@ -378,6 +400,7 @@ code { ...@@ -378,6 +400,7 @@ code {
} }
/* 隐藏模型设置区域的滚动条 */ /* 隐藏模型设置区域的滚动条 */
.api-info-scroll::-webkit-scrollbar,
.model-settings-scroll::-webkit-scrollbar, .model-settings-scroll::-webkit-scrollbar,
.thinking-content-scroll::-webkit-scrollbar, .thinking-content-scroll::-webkit-scrollbar,
.custom-request-textarea .semi-input::-webkit-scrollbar, .custom-request-textarea .semi-input::-webkit-scrollbar,
...@@ -385,6 +408,7 @@ code { ...@@ -385,6 +408,7 @@ code {
display: none; display: none;
} }
.api-info-scroll,
.model-settings-scroll, .model-settings-scroll,
.thinking-content-scroll, .thinking-content-scroll,
.custom-request-textarea .semi-input, .custom-request-textarea .semi-input,
......
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import '@douyinfe/semi-ui/dist/css/semi.css';
import { UserProvider } from './context/User'; import { UserProvider } from './context/User';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import { StatusProvider } from './context/Status'; import { StatusProvider } from './context/Status';
......
...@@ -194,6 +194,24 @@ const EditTagModal = (props) => { ...@@ -194,6 +194,24 @@ const EditTagModal = (props) => {
}, [originModelOptions, inputs.models]); }, [originModelOptions, inputs.models]);
useEffect(() => { useEffect(() => {
const fetchTagModels = async () => {
if (!tag) return;
setLoading(true);
try {
const res = await API.get(`/api/channel/tag/models?tag=${tag}`);
if (res?.data?.success) {
const models = res.data.data ? res.data.data.split(',') : [];
setInputs((inputs) => ({ ...inputs, models: models }));
} else {
showError(res.data.message);
}
} catch (error) {
showError(error.message);
} finally {
setLoading(false);
}
};
setInputs({ setInputs({
...originInputs, ...originInputs,
tag: tag, tag: tag,
...@@ -201,7 +219,8 @@ const EditTagModal = (props) => { ...@@ -201,7 +219,8 @@ const EditTagModal = (props) => {
}); });
fetchModels().then(); fetchModels().then();
fetchGroups().then(); fetchGroups().then();
}, [visible]); fetchTagModels().then(); // Call the new function
}, [visible, tag]); // Add tag to dependency array
const addCustomModels = () => { const addCustomModels = () => {
if (customModel.trim() === '') return; if (customModel.trim() === '') return;
...@@ -347,6 +366,11 @@ const EditTagModal = (props) => { ...@@ -347,6 +366,11 @@ const EditTagModal = (props) => {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<Text strong className="block mb-2">{t('模型')}</Text> <Text strong className="block mb-2">{t('模型')}</Text>
<Banner
type="info"
description={t('当前模型列表为该标签下所有渠道模型列表最长的一个,并非所有渠道的并集,请注意可能导致某些渠道模型丢失。')}
className="!rounded-lg mb-4"
/>
<Select <Select
placeholder={t('请选择该渠道所支持的模型,留空则不更改')} placeholder={t('请选择该渠道所支持的模型,留空则不更改')}
name='models' name='models'
...@@ -388,19 +412,19 @@ const EditTagModal = (props) => { ...@@ -388,19 +412,19 @@ const EditTagModal = (props) => {
/> />
<Space className="mt-2"> <Space className="mt-2">
<Text <Text
className="text-blue-500 cursor-pointer" className="!text-semi-color-primary cursor-pointer"
onClick={() => handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))} onClick={() => handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))}
> >
{t('填入模板')} {t('填入模板')}
</Text> </Text>
<Text <Text
className="text-blue-500 cursor-pointer" className="!text-semi-color-primary cursor-pointer"
onClick={() => handleInputChange('model_mapping', JSON.stringify({}, null, 2))} onClick={() => handleInputChange('model_mapping', JSON.stringify({}, null, 2))}
> >
{t('清空重定向')} {t('清空重定向')}
</Text> </Text>
<Text <Text
className="text-blue-500 cursor-pointer" className="!text-semi-color-primary cursor-pointer"
onClick={() => handleInputChange('model_mapping', '')} onClick={() => handleInputChange('model_mapping', '')}
> >
{t('不更改')} {t('不更改')}
......
...@@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next'; ...@@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next';
import SystemSetting from '../../components/settings/SystemSetting.js'; import SystemSetting from '../../components/settings/SystemSetting.js';
import { isRoot } from '../../helpers'; import { isRoot } from '../../helpers';
import OtherSetting from '../../components/settings/OtherSetting'; import OtherSetting from '../../components/settings/OtherSetting';
import PersonalSetting from '../../components/settings/PersonalSetting.js';
import OperationSetting from '../../components/settings/OperationSetting.js'; import OperationSetting from '../../components/settings/OperationSetting.js';
import RateLimitSetting from '../../components/settings/RateLimitSetting.js'; import RateLimitSetting from '../../components/settings/RateLimitSetting.js';
import ModelSetting from '../../components/settings/ModelSetting.js'; import ModelSetting from '../../components/settings/ModelSetting.js';
import DashboardSetting from '../../components/settings/DashboardSetting.js';
const Setting = () => { const Setting = () => {
const { t } = useTranslation(); const { t } = useTranslation();
...@@ -44,6 +44,11 @@ const Setting = () => { ...@@ -44,6 +44,11 @@ const Setting = () => {
content: <OtherSetting />, content: <OtherSetting />,
itemKey: 'other', itemKey: 'other',
}); });
panes.push({
tab: t('仪表盘配置'),
content: <DashboardSetting />,
itemKey: 'dashboard',
});
} }
const onChangeTab = (key) => { const onChangeTab = (key) => {
setTabActiveKey(key); setTabActiveKey(key);
......
...@@ -133,7 +133,7 @@ const Setup = () => { ...@@ -133,7 +133,7 @@ const Setup = () => {
}; };
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="bg-gray-50">
<Layout> <Layout>
<Layout.Content> <Layout.Content>
<div className="flex justify-center px-4 py-8"> <div className="flex justify-center px-4 py-8">
......
...@@ -219,9 +219,15 @@ const EditToken = (props) => { ...@@ -219,9 +219,15 @@ const EditToken = (props) => {
let successCount = 0; // 记录成功创建的令牌数量 let successCount = 0; // 记录成功创建的令牌数量
for (let i = 0; i < tokenCount; i++) { for (let i = 0; i < tokenCount; i++) {
let localInputs = { ...inputs }; let localInputs = { ...inputs };
if (i !== 0) {
// 如果用户想要创建多个令牌,则给每个令牌一个序号后缀 // 检查用户是否填写了令牌名称
localInputs.name = `${inputs.name}-${generateRandomSuffix()}`; const baseName = inputs.name.trim() === '' ? 'default' : inputs.name;
if (i !== 0 || inputs.name.trim() === '') {
// 如果创建多个令牌(i !== 0)或者用户没有填写名称,则添加随机后缀
localInputs.name = `${baseName}-${generateRandomSuffix()}`;
} else {
localInputs.name = baseName;
} }
localInputs.remain_quota = parseInt(localInputs.remain_quota); localInputs.remain_quota = parseInt(localInputs.remain_quota);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment