From 191f5219261e5a0d653aa0080bf4244d097e8979 Mon Sep 17 00:00:00 2001 From: RedwindA Date: Thu, 5 Jun 2025 20:42:56 +0800 Subject: [PATCH 01/35] fix: change RedisHDelObj to use Del instead of HDel --- common/redis.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/redis.go b/common/redis.go index 49d3ec78..50030d2a 100644 --- a/common/redis.go +++ b/common/redis.go @@ -97,7 +97,7 @@ func RedisHDelObj(key string) error { SysLog(fmt.Sprintf("Redis HDEL: key=%s", key)) } 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 { From eff9ce117f149e0d760d51bc1b0920f3d1c38456 Mon Sep 17 00:00:00 2001 From: RedwindA Date: Thu, 5 Jun 2025 21:17:57 +0800 Subject: [PATCH 02/35] refactor: rename RedisHDelObj to RedisDelKey and update references --- common/redis.go | 4 ++-- model/token_cache.go | 2 +- model/user_cache.go | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/common/redis.go b/common/redis.go index 50030d2a..ba35331a 100644 --- a/common/redis.go +++ b/common/redis.go @@ -92,9 +92,9 @@ func RedisDel(key string) error { return RDB.Del(ctx, key).Err() } -func RedisHDelObj(key string) error { +func RedisDelKey(key string) error { if DebugEnabled { - SysLog(fmt.Sprintf("Redis HDEL: key=%s", key)) + SysLog(fmt.Sprintf("Redis DEL Key: key=%s", key)) } ctx := context.Background() return RDB.Del(ctx, key).Err() diff --git a/model/token_cache.go b/model/token_cache.go index 0fe02fea..b2e0c951 100644 --- a/model/token_cache.go +++ b/model/token_cache.go @@ -19,7 +19,7 @@ func cacheSetToken(token Token) error { func cacheDeleteToken(key string) error { key = common.GenerateHMAC(key) - err := common.RedisHDelObj(fmt.Sprintf("token:%s", key)) + err := common.RedisDelKey(fmt.Sprintf("token:%s", key)) if err != nil { return err } diff --git a/model/user_cache.go b/model/user_cache.go index bc412e77..d74877bd 100644 --- a/model/user_cache.go +++ b/model/user_cache.go @@ -3,11 +3,12 @@ package model import ( "encoding/json" "fmt" - "github.com/gin-gonic/gin" "one-api/common" "one-api/constant" "time" + "github.com/gin-gonic/gin" + "github.com/bytedance/gopkg/util/gopool" ) @@ -57,7 +58,7 @@ func invalidateUserCache(userId int) error { if !common.RedisEnabled { return nil } - return common.RedisHDelObj(getUserCacheKey(userId)) + return common.RedisDelKey(getUserCacheKey(userId)) } // updateUserCache updates all user cache fields using hash From b778cd2b2357b506998a27cc9e023869a3d238da Mon Sep 17 00:00:00 2001 From: Xyfacai Date: Sat, 7 Jun 2025 23:05:01 +0800 Subject: [PATCH 03/35] =?UTF-8?q?refactor:=20message=20content=20=E6=94=B9?= =?UTF-8?q?=E6=88=90=20any?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor: message content 改成 any --- controller/channel-test.go | 4 +- dto/claude.go | 105 ++++++---- dto/openai_request.go | 270 ++++++++++++++++++++----- main.go | 4 +- relay/channel/ali/text.go | 3 +- relay/channel/baidu/relay-baidu.go | 3 +- relay/channel/claude/relay-claude.go | 13 +- relay/channel/cohere/relay-cohere.go | 3 +- relay/channel/coze/dto.go | 2 +- relay/channel/dify/relay-dify.go | 3 +- relay/channel/gemini/relay-gemini.go | 3 +- relay/channel/palm/relay-palm.go | 3 +- relay/channel/tencent/relay-tencent.go | 3 +- relay/channel/xunfei/relay-xunfei.go | 3 +- relay/channel/zhipu/relay-zhipu.go | 3 +- service/token_counter.go | 16 +- 16 files changed, 319 insertions(+), 122 deletions(-) diff --git a/controller/channel-test.go b/controller/channel-test.go index d1cb4093..970c1768 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -200,10 +200,10 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest { } else { testRequest.MaxTokens = 10 } - content, _ := json.Marshal("hi") + testMessage := dto.Message{ Role: "user", - Content: content, + Content: "hi", } testRequest.Model = model testRequest.Messages = append(testRequest.Messages, testMessage) diff --git a/dto/claude.go b/dto/claude.go index 36dfc02e..4d24bc70 100644 --- a/dto/claude.go +++ b/dto/claude.go @@ -1,6 +1,9 @@ package dto -import "encoding/json" +import ( + "encoding/json" + "one-api/common" +) type ClaudeMetadata struct { UserId string `json:"user_id"` @@ -20,11 +23,11 @@ type ClaudeMediaMessage struct { Delta string `json:"delta,omitempty"` CacheControl json.RawMessage `json:"cache_control,omitempty"` // tool_calls - Id string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Input any `json:"input,omitempty"` - Content json.RawMessage `json:"content,omitempty"` - ToolUseId string `json:"tool_use_id,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Input any `json:"input,omitempty"` + Content any `json:"content,omitempty"` + ToolUseId string `json:"tool_use_id,omitempty"` } func (c *ClaudeMediaMessage) SetText(s string) { @@ -39,15 +42,39 @@ func (c *ClaudeMediaMessage) GetText() string { } func (c *ClaudeMediaMessage) IsStringContent() bool { - var content string - return json.Unmarshal(c.Content, &content) == nil + if c.Content == nil { + return false + } + _, ok := c.Content.(string) + if ok { + return true + } + return false } func (c *ClaudeMediaMessage) GetStringContent() string { - var content string - if err := json.Unmarshal(c.Content, &content); err == nil { - return content + if c.Content == nil { + return "" } + 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 "" } @@ -57,16 +84,12 @@ func (c *ClaudeMediaMessage) GetJsonRowString() string { } func (c *ClaudeMediaMessage) SetContent(content any) { - jsonContent, _ := json.Marshal(content) - c.Content = jsonContent + c.Content = content } func (c *ClaudeMediaMessage) ParseMediaContent() []ClaudeMediaMessage { - var mediaContent []ClaudeMediaMessage - if err := json.Unmarshal(c.Content, &mediaContent); err == nil { - return mediaContent - } - return make([]ClaudeMediaMessage, 0) + mediaContent, _ := common.Any2Type[[]ClaudeMediaMessage](c.Content) + return mediaContent } type ClaudeMessageSource struct { @@ -82,14 +105,36 @@ type ClaudeMessage struct { } func (c *ClaudeMessage) IsStringContent() bool { + if c.Content == nil { + return false + } _, ok := c.Content.(string) return ok } func (c *ClaudeMessage) GetStringContent() string { - if c.IsStringContent() { - return c.Content.(string) + if c.Content == nil { + return "" } + 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 "" } @@ -98,15 +143,7 @@ func (c *ClaudeMessage) SetStringContent(content string) { } func (c *ClaudeMessage) ParseContent() ([]ClaudeMediaMessage, error) { - // map content to []ClaudeMediaMessage - // 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 + return common.Any2Type[[]ClaudeMediaMessage](c.Content) } type Tool struct { @@ -161,14 +198,8 @@ func (c *ClaudeRequest) SetStringSystem(system string) { } func (c *ClaudeRequest) ParseSystem() []ClaudeMediaMessage { - // map content to []ClaudeMediaMessage - // parse to json - jsonContent, _ := json.Marshal(c.System) - var contentList []ClaudeMediaMessage - if err := json.Unmarshal(jsonContent, &contentList); err == nil { - return contentList - } - return make([]ClaudeMediaMessage, 0) + mediaContent, _ := common.Any2Type[[]ClaudeMediaMessage](c.System) + return mediaContent } type ClaudeError struct { diff --git a/dto/openai_request.go b/dto/openai_request.go index a7325fe8..fcb0fe36 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -19,43 +19,43 @@ type FormatJsonSchema struct { } type GeneralOpenAIRequest struct { - Model string `json:"model,omitempty"` - Messages []Message `json:"messages,omitempty"` - Prompt any `json:"prompt,omitempty"` - Prefix any `json:"prefix,omitempty"` - Suffix any `json:"suffix,omitempty"` - Stream bool `json:"stream,omitempty"` - StreamOptions *StreamOptions `json:"stream_options,omitempty"` - MaxTokens uint `json:"max_tokens,omitempty"` - MaxCompletionTokens uint `json:"max_completion_tokens,omitempty"` - ReasoningEffort string `json:"reasoning_effort,omitempty"` - Temperature *float64 `json:"temperature,omitempty"` - TopP float64 `json:"top_p,omitempty"` - TopK int `json:"top_k,omitempty"` - Stop any `json:"stop,omitempty"` - N int `json:"n,omitempty"` - Input any `json:"input,omitempty"` - Instruction string `json:"instruction,omitempty"` - Size string `json:"size,omitempty"` - Functions any `json:"functions,omitempty"` - FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` - PresencePenalty float64 `json:"presence_penalty,omitempty"` - ResponseFormat *ResponseFormat `json:"response_format,omitempty"` - EncodingFormat any `json:"encoding_format,omitempty"` - Seed float64 `json:"seed,omitempty"` - ParallelTooCalls *bool `json:"parallel_tool_calls,omitempty"` - Tools []ToolCallRequest `json:"tools,omitempty"` - ToolChoice any `json:"tool_choice,omitempty"` - User string `json:"user,omitempty"` - LogProbs bool `json:"logprobs,omitempty"` - TopLogProbs int `json:"top_logprobs,omitempty"` - Dimensions int `json:"dimensions,omitempty"` - Modalities any `json:"modalities,omitempty"` - Audio any `json:"audio,omitempty"` - EnableThinking any `json:"enable_thinking,omitempty"` // ali - ExtraBody any `json:"extra_body,omitempty"` - WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"` - // OpenRouter Params + Model string `json:"model,omitempty"` + Messages []Message `json:"messages,omitempty"` + Prompt any `json:"prompt,omitempty"` + Prefix any `json:"prefix,omitempty"` + Suffix any `json:"suffix,omitempty"` + Stream bool `json:"stream,omitempty"` + StreamOptions *StreamOptions `json:"stream_options,omitempty"` + MaxTokens uint `json:"max_tokens,omitempty"` + MaxCompletionTokens uint `json:"max_completion_tokens,omitempty"` + ReasoningEffort string `json:"reasoning_effort,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + TopP float64 `json:"top_p,omitempty"` + TopK int `json:"top_k,omitempty"` + Stop any `json:"stop,omitempty"` + N int `json:"n,omitempty"` + Input any `json:"input,omitempty"` + Instruction string `json:"instruction,omitempty"` + Size string `json:"size,omitempty"` + Functions any `json:"functions,omitempty"` + FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` + PresencePenalty float64 `json:"presence_penalty,omitempty"` + ResponseFormat *ResponseFormat `json:"response_format,omitempty"` + EncodingFormat any `json:"encoding_format,omitempty"` + Seed float64 `json:"seed,omitempty"` + ParallelTooCalls *bool `json:"parallel_tool_calls,omitempty"` + Tools []ToolCallRequest `json:"tools,omitempty"` + ToolChoice any `json:"tool_choice,omitempty"` + User string `json:"user,omitempty"` + LogProbs bool `json:"logprobs,omitempty"` + TopLogProbs int `json:"top_logprobs,omitempty"` + Dimensions int `json:"dimensions,omitempty"` + Modalities any `json:"modalities,omitempty"` + Audio any `json:"audio,omitempty"` + EnableThinking any `json:"enable_thinking,omitempty"` // ali + ExtraBody any `json:"extra_body,omitempty"` + WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"` + // OpenRouter Params Reasoning json.RawMessage `json:"reasoning,omitempty"` } @@ -107,16 +107,16 @@ func (r *GeneralOpenAIRequest) ParseInput() []string { } type Message struct { - Role string `json:"role"` - Content json.RawMessage `json:"content"` - Name *string `json:"name,omitempty"` - Prefix *bool `json:"prefix,omitempty"` - ReasoningContent string `json:"reasoning_content,omitempty"` - Reasoning string `json:"reasoning,omitempty"` - ToolCalls json.RawMessage `json:"tool_calls,omitempty"` - ToolCallId string `json:"tool_call_id,omitempty"` - parsedContent []MediaContent - parsedStringContent *string + Role string `json:"role"` + Content any `json:"content"` + Name *string `json:"name,omitempty"` + Prefix *bool `json:"prefix,omitempty"` + ReasoningContent string `json:"reasoning_content,omitempty"` + Reasoning string `json:"reasoning,omitempty"` + ToolCalls json.RawMessage `json:"tool_calls,omitempty"` + ToolCallId string `json:"tool_call_id,omitempty"` + parsedContent []MediaContent + //parsedStringContent *string } type MediaContent struct { @@ -212,6 +212,180 @@ func (m *Message) SetToolCalls(toolCalls any) { } 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 { + 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 { return *m.parsedStringContent } @@ -382,7 +556,7 @@ func (m *Message) ParseContent() []MediaContent { m.parsedContent = contentList } return contentList -} +}*/ type WebSearchOptions struct { SearchContextSize string `json:"search_context_size,omitempty"` diff --git a/main.go b/main.go index c286650f..26c39e5f 100644 --- a/main.go +++ b/main.go @@ -25,10 +25,10 @@ import ( _ "net/http/pprof" ) -//go:embed web/dist +// go:embed web/dist var buildFS embed.FS -//go:embed web/dist/index.html +// go:embed web/dist/index.html var indexPage []byte func main() { diff --git a/relay/channel/ali/text.go b/relay/channel/ali/text.go index 3fe893b3..bc70be89 100644 --- a/relay/channel/ali/text.go +++ b/relay/channel/ali/text.go @@ -94,12 +94,11 @@ func embeddingResponseAli2OpenAI(response *AliEmbeddingResponse) *dto.OpenAIEmbe } func responseAli2OpenAI(response *AliResponse) *dto.OpenAITextResponse { - content, _ := json.Marshal(response.Output.Text) choice := dto.OpenAITextResponseChoice{ Index: 0, Message: dto.Message{ Role: "assistant", - Content: content, + Content: response.Output.Text, }, FinishReason: response.Output.FinishReason, } diff --git a/relay/channel/baidu/relay-baidu.go b/relay/channel/baidu/relay-baidu.go index 62b06413..55b6c137 100644 --- a/relay/channel/baidu/relay-baidu.go +++ b/relay/channel/baidu/relay-baidu.go @@ -53,12 +53,11 @@ func requestOpenAI2Baidu(request dto.GeneralOpenAIRequest) *BaiduChatRequest { } func responseBaidu2OpenAI(response *BaiduChatResponse) *dto.OpenAITextResponse { - content, _ := json.Marshal(response.Result) choice := dto.OpenAITextResponseChoice{ Index: 0, Message: dto.Message{ Role: "assistant", - Content: content, + Content: response.Result, }, FinishReason: "stop", } diff --git a/relay/channel/claude/relay-claude.go b/relay/channel/claude/relay-claude.go index 95e7c4be..cb2c75b1 100644 --- a/relay/channel/claude/relay-claude.go +++ b/relay/channel/claude/relay-claude.go @@ -48,9 +48,9 @@ func RequestOpenAI2ClaudeComplete(textRequest dto.GeneralOpenAIRequest) *dto.Cla prompt := "" for _, message := range textRequest.Messages { 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" { - prompt += fmt.Sprintf("\n\nAssistant: %s", message.Content) + prompt += fmt.Sprintf("\n\nAssistant: %s", message.StringContent()) } else if message.Role == "system" { if prompt == "" { prompt = message.StringContent() @@ -155,15 +155,13 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.Cla } if lastMessage.Role == message.Role && lastMessage.Role != "tool" { if lastMessage.IsStringContent() && message.IsStringContent() { - content, _ := json.Marshal(strings.Trim(fmt.Sprintf("%s %s", lastMessage.StringContent(), message.StringContent()), "\"")) - fmtMessage.Content = content + fmtMessage.SetStringContent(strings.Trim(fmt.Sprintf("%s %s", lastMessage.StringContent(), message.StringContent()), "\"")) // delete last message formatMessages = formatMessages[:len(formatMessages)-1] } } if fmtMessage.Content == nil { - content, _ := json.Marshal("...") - fmtMessage.Content = content + fmtMessage.SetStringContent("...") } formatMessages = append(formatMessages, fmtMessage) lastMessage = fmtMessage @@ -397,12 +395,11 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse) *dto thinkingContent := "" if reqMode == RequestModeCompletion { - content, _ := json.Marshal(strings.TrimPrefix(claudeResponse.Completion, " ")) choice := dto.OpenAITextResponseChoice{ Index: 0, Message: dto.Message{ Role: "assistant", - Content: content, + Content: strings.TrimPrefix(claudeResponse.Completion, " "), Name: nil, }, FinishReason: stopReasonClaude2OpenAI(claudeResponse.StopReason), diff --git a/relay/channel/cohere/relay-cohere.go b/relay/channel/cohere/relay-cohere.go index 17b58dbc..10c4328b 100644 --- a/relay/channel/cohere/relay-cohere.go +++ b/relay/channel/cohere/relay-cohere.go @@ -195,11 +195,10 @@ func cohereHandler(c *gin.Context, resp *http.Response, modelName string, prompt openaiResp.Model = modelName openaiResp.Usage = usage - content, _ := json.Marshal(cohereResp.Text) openaiResp.Choices = []dto.OpenAITextResponseChoice{ { Index: 0, - Message: dto.Message{Content: content, Role: "assistant"}, + Message: dto.Message{Content: cohereResp.Text, Role: "assistant"}, FinishReason: stopReasonCohere2OpenAI(cohereResp.FinishReason), }, } diff --git a/relay/channel/coze/dto.go b/relay/channel/coze/dto.go index 4e9afa23..d5dc9a81 100644 --- a/relay/channel/coze/dto.go +++ b/relay/channel/coze/dto.go @@ -10,7 +10,7 @@ type CozeError struct { type CozeEnterMessage struct { Role string `json:"role"` Type string `json:"type,omitempty"` - Content json.RawMessage `json:"content,omitempty"` + Content any `json:"content,omitempty"` MetaData json.RawMessage `json:"meta_data,omitempty"` ContentType string `json:"content_type,omitempty"` } diff --git a/relay/channel/dify/relay-dify.go b/relay/channel/dify/relay-dify.go index b58fbe53..93e3e8d6 100644 --- a/relay/channel/dify/relay-dify.go +++ b/relay/channel/dify/relay-dify.go @@ -278,12 +278,11 @@ func difyHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInf Created: common.GetTimestamp(), Usage: difyResponse.MetaData.Usage, } - content, _ := json.Marshal(difyResponse.Answer) choice := dto.OpenAITextResponseChoice{ Index: 0, Message: dto.Message{ Role: "assistant", - Content: content, + Content: difyResponse.Answer, }, FinishReason: "stop", } diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index 4022c9b0..b8bec6c2 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -609,14 +609,13 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp Created: common.GetTimestamp(), Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.Candidates)), } - content, _ := json.Marshal("") isToolCall := false for _, candidate := range response.Candidates { choice := dto.OpenAITextResponseChoice{ Index: int(candidate.Index), Message: dto.Message{ Role: "assistant", - Content: content, + Content: "", }, FinishReason: constant.FinishReasonStop, } diff --git a/relay/channel/palm/relay-palm.go b/relay/channel/palm/relay-palm.go index c8e337de..5c398b5e 100644 --- a/relay/channel/palm/relay-palm.go +++ b/relay/channel/palm/relay-palm.go @@ -45,12 +45,11 @@ func responsePaLM2OpenAI(response *PaLMChatResponse) *dto.OpenAITextResponse { Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.Candidates)), } for i, candidate := range response.Candidates { - content, _ := json.Marshal(candidate.Content) choice := dto.OpenAITextResponseChoice{ Index: i, Message: dto.Message{ Role: "assistant", - Content: content, + Content: candidate.Content, }, FinishReason: "stop", } diff --git a/relay/channel/tencent/relay-tencent.go b/relay/channel/tencent/relay-tencent.go index 5630650f..1446e06e 100644 --- a/relay/channel/tencent/relay-tencent.go +++ b/relay/channel/tencent/relay-tencent.go @@ -56,12 +56,11 @@ func responseTencent2OpenAI(response *TencentChatResponse) *dto.OpenAITextRespon }, } if len(response.Choices) > 0 { - content, _ := json.Marshal(response.Choices[0].Messages.Content) choice := dto.OpenAITextResponseChoice{ Index: 0, Message: dto.Message{ Role: "assistant", - Content: content, + Content: response.Choices[0].Messages.Content, }, FinishReason: response.Choices[0].FinishReason, } diff --git a/relay/channel/xunfei/relay-xunfei.go b/relay/channel/xunfei/relay-xunfei.go index 15d33510..c6ef722c 100644 --- a/relay/channel/xunfei/relay-xunfei.go +++ b/relay/channel/xunfei/relay-xunfei.go @@ -61,12 +61,11 @@ func responseXunfei2OpenAI(response *XunfeiChatResponse) *dto.OpenAITextResponse }, } } - content, _ := json.Marshal(response.Payload.Choices.Text[0].Content) choice := dto.OpenAITextResponseChoice{ Index: 0, Message: dto.Message{ Role: "assistant", - Content: content, + Content: response.Payload.Choices.Text[0].Content, }, FinishReason: constant.FinishReasonStop, } diff --git a/relay/channel/zhipu/relay-zhipu.go b/relay/channel/zhipu/relay-zhipu.go index b0cac858..744538e3 100644 --- a/relay/channel/zhipu/relay-zhipu.go +++ b/relay/channel/zhipu/relay-zhipu.go @@ -108,12 +108,11 @@ func responseZhipu2OpenAI(response *ZhipuResponse) *dto.OpenAITextResponse { Usage: response.Data.Usage, } for i, choice := range response.Data.Choices { - content, _ := json.Marshal(strings.Trim(choice.Content, "\"")) openaiChoice := dto.OpenAITextResponseChoice{ Index: i, Message: dto.Message{ Role: choice.Role, - Content: content, + Content: strings.Trim(choice.Content, "\""), }, FinishReason: "", } diff --git a/service/token_counter.go b/service/token_counter.go index d63b54ad..e1722013 100644 --- a/service/token_counter.go +++ b/service/token_counter.go @@ -261,12 +261,16 @@ func CountTokenClaudeMessages(messages []dto.ClaudeMessage, model string, stream //} tokenNum += 1000 case "tool_use": - tokenNum += getTokenNum(tokenEncoder, mediaMessage.Name) - inputJSON, _ := json.Marshal(mediaMessage.Input) - tokenNum += getTokenNum(tokenEncoder, string(inputJSON)) + if mediaMessage.Input != nil { + tokenNum += getTokenNum(tokenEncoder, mediaMessage.Name) + inputJSON, _ := json.Marshal(mediaMessage.Input) + tokenNum += getTokenNum(tokenEncoder, string(inputJSON)) + } case "tool_result": - contentJSON, _ := json.Marshal(mediaMessage.Content) - tokenNum += getTokenNum(tokenEncoder, string(contentJSON)) + if mediaMessage.Content != nil { + contentJSON, _ := json.Marshal(mediaMessage.Content) + tokenNum += getTokenNum(tokenEncoder, string(contentJSON)) + } } } } @@ -386,7 +390,7 @@ func CountTokenMessages(info *relaycommon.RelayInfo, messages []dto.Message, mod for _, message := range messages { tokenNum += tokensPerMessage tokenNum += getTokenNum(tokenEncoder, message.Role) - if len(message.Content) > 0 { + if message.Content != nil { if message.Name != nil { tokenNum += tokensPerName tokenNum += getTokenNum(tokenEncoder, *message.Name) From b7c742166a728c4c82d6df1f78ac1e054ad90be5 Mon Sep 17 00:00:00 2001 From: RedwindA Date: Sun, 8 Jun 2025 01:16:27 +0800 Subject: [PATCH 04/35] =?UTF-8?q?=F0=9F=8E=A8=20feat(channel):=20add=20end?= =?UTF-8?q?point=20to=20retrieve=20models=20by=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/channel.go | 41 +++++++++++++++++++++++++++++++++++++++++ router/api-router.go | 1 + 2 files changed, 42 insertions(+) diff --git a/controller/channel.go b/controller/channel.go index a31e1f47..a4ef87c3 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -623,3 +623,44 @@ func BatchSetChannelTag(c *gin.Context) { }) 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 +} diff --git a/router/api-router.go b/router/api-router.go index 1720ff57..6251c8a2 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -105,6 +105,7 @@ func SetApiRouter(router *gin.Engine) { channelRoute.GET("/fetch_models/:id", controller.FetchUpstreamModels) channelRoute.POST("/fetch_models", controller.FetchModels) channelRoute.POST("/batch/tag", controller.BatchSetChannelTag) + channelRoute.GET("/tag/models", controller.GetTagModels) } tokenRoute := apiRouter.Group("/token") tokenRoute.Use(middleware.UserAuth()) From 49898928309ef72d5f2cbbb7c7c6a153b69904c8 Mon Sep 17 00:00:00 2001 From: RedwindA Date: Sun, 8 Jun 2025 01:16:39 +0800 Subject: [PATCH 05/35] =?UTF-8?q?=F0=9F=90=9B=20fix(EditTagModal):=20add?= =?UTF-8?q?=20fetchTagModels=20function=20to=20retrieve=20models=20based?= =?UTF-8?q?=20on=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/pages/Channel/EditTagModal.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/web/src/pages/Channel/EditTagModal.js b/web/src/pages/Channel/EditTagModal.js index 52dd4bbb..1b370297 100644 --- a/web/src/pages/Channel/EditTagModal.js +++ b/web/src/pages/Channel/EditTagModal.js @@ -194,6 +194,24 @@ const EditTagModal = (props) => { }, [originModelOptions, inputs.models]); 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({ ...originInputs, tag: tag, @@ -201,7 +219,8 @@ const EditTagModal = (props) => { }); fetchModels().then(); fetchGroups().then(); - }, [visible]); + fetchTagModels().then(); // Call the new function + }, [visible, tag]); // Add tag to dependency array const addCustomModels = () => { if (customModel.trim() === '') return; From a92952f07034adcf19fb3492991bc8ca4f6b2b84 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Sun, 8 Jun 2025 12:14:49 +0800 Subject: [PATCH 06/35] =?UTF-8?q?=F0=9F=8E=A8=20fix:=20Import=20Semi=20UI?= =?UTF-8?q?=20CSS=20explicitly=20to=20resolve=20missing=20component=20styl?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add explicit import of '@douyinfe/semi-ui/dist/css/semi.css' in index.js - Ensures Semi Design components render with proper styling - Resolves issue where Semi components appeared unstyled after dependency updates This change addresses the style loading issue that occurred after adding antd dependency and updating the build configuration. The explicit import ensures consistent style loading regardless of plugin behavior changes. --- web/src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/index.js b/web/src/index.js index 0f57f5a1..ef8a3a07 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; +import '@douyinfe/semi-ui/dist/css/semi.css'; import { UserProvider } from './context/User'; import 'react-toastify/dist/ReactToastify.css'; import { StatusProvider } from './context/Status'; From c26599ef46b8c0f3692ccaaa118ece532b184569 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Sun, 8 Jun 2025 12:23:54 +0800 Subject: [PATCH 07/35] =?UTF-8?q?=F0=9F=92=84=20style(Logs):=20Add=20round?= =?UTF-8?q?ed=20corners=20to=20image=20view=20button=20in=20MjLogsTable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add rounded-full class to "查看图片" (View Image) button for consistent UI styling - All other buttons in both MjLogsTable.js and TaskLogsTable.js already have rounded corners applied - Ensures uniform button styling across the log tables interface --- web/src/components/table/MjLogsTable.js | 3 ++- web/src/components/table/TaskLogsTable.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/components/table/MjLogsTable.js b/web/src/components/table/MjLogsTable.js index 48513eb1..b4cf046e 100644 --- a/web/src/components/table/MjLogsTable.js +++ b/web/src/components/table/MjLogsTable.js @@ -462,7 +462,7 @@ const LogsTable = () => { percent={text ? parseInt(text.replace('%', '')) : 0} showInfo={true} aria-label='drawing progress' - style={{ minWidth: '200px' }} + style={{ minWidth: '160px' }} /> } @@ -483,6 +483,7 @@ const LogsTable = () => { setModalImageUrl(text); setIsModalOpenurl(true); }} + className="!rounded-full" > {t('查看图片')} diff --git a/web/src/components/table/TaskLogsTable.js b/web/src/components/table/TaskLogsTable.js index 4e329d29..91ccc06c 100644 --- a/web/src/components/table/TaskLogsTable.js +++ b/web/src/components/table/TaskLogsTable.js @@ -395,7 +395,7 @@ const LogsTable = () => { percent={text ? parseInt(text.replace('%', '')) : 0} showInfo={true} aria-label='task progress' - style={{ minWidth: '200px' }} + style={{ minWidth: '160px' }} /> ) } From 97a8219845ab9e81b58d73f8d33748e2ad9707aa Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Sun, 8 Jun 2025 12:38:03 +0800 Subject: [PATCH 08/35] =?UTF-8?q?=E2=9C=A8=20feat(token):=20auto-generate?= =?UTF-8?q?=20default=20token=20names=20when=20user=20input=20is=20empty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When creating tokens, if the user doesn't provide a token name (empty or whitespace-only), the system will now automatically generate a name using the format "default-xxxxxx" where "xxxxxx" is a 6-character random alphanumeric string. This enhancement ensures that all created tokens have meaningful names and improves the user experience by removing the requirement to manually input token names for quick token creation scenarios. Changes: - Modified token creation logic to detect empty token names - Added automatic fallback to "default" base name when user input is missing - Maintained existing behavior for multiple token creation with random suffixes - Ensured consistent naming pattern across single and batch token creation --- web/src/pages/Token/EditToken.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/web/src/pages/Token/EditToken.js b/web/src/pages/Token/EditToken.js index 946164da..71f611bd 100644 --- a/web/src/pages/Token/EditToken.js +++ b/web/src/pages/Token/EditToken.js @@ -219,9 +219,15 @@ const EditToken = (props) => { let successCount = 0; // 记录成功创建的令牌数量 for (let i = 0; i < tokenCount; i++) { 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); From b47274bfadc637722f473bdfa217a30656406b23 Mon Sep 17 00:00:00 2001 From: RedwindA Date: Sun, 8 Jun 2025 13:23:59 +0800 Subject: [PATCH 09/35] =?UTF-8?q?=F0=9F=90=9B=20fix(EditTagModal):=20add?= =?UTF-8?q?=20info=20banner=20to=20clarify=20modelList=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/pages/Channel/EditTagModal.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/src/pages/Channel/EditTagModal.js b/web/src/pages/Channel/EditTagModal.js index 1b370297..695ed2b4 100644 --- a/web/src/pages/Channel/EditTagModal.js +++ b/web/src/pages/Channel/EditTagModal.js @@ -366,6 +366,11 @@ const EditTagModal = (props) => {
{t('模型')} + { - setLogType(parseInt(value)); - loadLogs(0, pageSize, parseInt(value)); - }} - > - {t('全部')} - {t('充值')} - {t('消费')} - {t('管理')} - {t('系统')} - {t('错误')} - - - {/* 其他搜索字段 */} - } - placeholder={t('令牌名称')} - value={token_name} - onChange={(value) => handleInputChange(value, 'token_name')} - className='!rounded-full' - showClear - /> - - } - placeholder={t('模型名称')} - value={model_name} - onChange={(value) => handleInputChange(value, 'model_name')} - className='!rounded-full' - showClear - /> - - } - placeholder={t('分组')} - value={group} - onChange={(value) => handleInputChange(value, 'group')} - className='!rounded-full' - showClear - /> - - {isAdminUser && ( - <> - } - placeholder={t('渠道 ID')} - value={channel} - onChange={(value) => handleInputChange(value, 'channel')} + {/* 操作按钮区域 */} +
+
+
+ +
- - {/* 操作按钮区域 */} -
-
-
- - + > + {t('重置')} + + +
-
+
} shadows='always' From 86354e305ead50c89149cfd134a8ead0b5fd5a07 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Sun, 8 Jun 2025 18:41:04 +0800 Subject: [PATCH 16/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(components)?= =?UTF-8?q?:=20migrate=20all=20table=20components=20to=20use=20Form=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactor LogsTable, MjLogsTable, TokensTable, UsersTable, and ChannelsTable to use Semi-UI Form components - Replace individual input state management with centralized Form API - Add form validation and consistent form handling across all tables - Implement auto-search functionality with proper state update timing - Add reset functionality to clear all search filters - Improve responsive layout design for better mobile experience - Remove duplicate form initial values and consolidate form logic - Remove column visibility feature from ChannelsTable to simplify UI - Standardize search form structure and styling across all table components - Fix state update timing issues in search functionality - Add proper form submission handling with loading states BREAKING CHANGE: Form state management has been completely rewritten. All table components now use Form API instead of individual useState hooks. Column visibility settings for ChannelsTable have been removed. --- web/src/components/table/ChannelsTable.js | 365 ++++++------------- web/src/components/table/LogsTable.js | 19 +- web/src/components/table/MjLogsTable.js | 180 +++++---- web/src/components/table/RedemptionsTable.js | 108 ++++-- web/src/components/table/TaskLogsTable.js | 179 +++++---- web/src/components/table/TokensTable.js | 121 +++--- web/src/components/table/UsersTable.js | 149 +++++--- 7 files changed, 592 insertions(+), 529 deletions(-) diff --git a/web/src/components/table/ChannelsTable.js b/web/src/components/table/ChannelsTable.js index 105aa217..73840535 100644 --- a/web/src/components/table/ChannelsTable.js +++ b/web/src/components/table/ChannelsTable.js @@ -25,9 +25,8 @@ import { Tag, Tooltip, Typography, - Checkbox, Card, - Select + Form } from '@douyinfe/semi-ui'; import EditChannel from '../../pages/Channel/EditChannel.js'; import { @@ -149,108 +148,17 @@ const ChannelsTable = () => { } }; - // Define column keys for selection - const COLUMN_KEYS = { - ID: 'id', - NAME: 'name', - GROUP: 'group', - TYPE: 'type', - STATUS: 'status', - RESPONSE_TIME: 'response_time', - BALANCE: 'balance', - PRIORITY: 'priority', - WEIGHT: 'weight', - OPERATE: 'operate', - }; - - // State for column visibility - const [visibleColumns, setVisibleColumns] = useState({}); - const [showColumnSelector, setShowColumnSelector] = useState(false); - - // Load saved column preferences from localStorage - useEffect(() => { - const savedColumns = localStorage.getItem('channels-table-columns'); - if (savedColumns) { - try { - const parsed = JSON.parse(savedColumns); - // Make sure all columns are accounted for - const defaults = getDefaultColumnVisibility(); - const merged = { ...defaults, ...parsed }; - setVisibleColumns(merged); - } catch (e) { - console.error('Failed to parse saved column preferences', e); - initDefaultColumns(); - } - } else { - initDefaultColumns(); - } - }, []); - - // Update table when column visibility changes - useEffect(() => { - if (Object.keys(visibleColumns).length > 0) { - // Save to localStorage - localStorage.setItem( - 'channels-table-columns', - JSON.stringify(visibleColumns), - ); - } - }, [visibleColumns]); - - // Get default column visibility - const getDefaultColumnVisibility = () => { - return { - [COLUMN_KEYS.ID]: true, - [COLUMN_KEYS.NAME]: true, - [COLUMN_KEYS.GROUP]: true, - [COLUMN_KEYS.TYPE]: true, - [COLUMN_KEYS.STATUS]: true, - [COLUMN_KEYS.RESPONSE_TIME]: true, - [COLUMN_KEYS.BALANCE]: true, - [COLUMN_KEYS.PRIORITY]: true, - [COLUMN_KEYS.WEIGHT]: true, - [COLUMN_KEYS.OPERATE]: true, - }; - }; - - // Initialize default column visibility - const initDefaultColumns = () => { - const defaults = getDefaultColumnVisibility(); - setVisibleColumns(defaults); - }; - - // Handle column visibility change - const handleColumnVisibilityChange = (columnKey, checked) => { - const updatedColumns = { ...visibleColumns, [columnKey]: checked }; - setVisibleColumns(updatedColumns); - }; - - // Handle "Select All" checkbox - const handleSelectAll = (checked) => { - const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]); - const updatedColumns = {}; - - allKeys.forEach((key) => { - updatedColumns[key] = checked; - }); - - setVisibleColumns(updatedColumns); - }; - - // Define all columns with keys - const allColumns = [ + // Define all columns + const columns = [ { - key: COLUMN_KEYS.ID, title: t('ID'), dataIndex: 'id', }, { - key: COLUMN_KEYS.NAME, title: t('名称'), dataIndex: 'name', }, { - key: COLUMN_KEYS.GROUP, title: t('分组'), dataIndex: 'group', render: (text, record, index) => ( @@ -269,7 +177,6 @@ const ChannelsTable = () => { ), }, { - key: COLUMN_KEYS.TYPE, title: t('类型'), dataIndex: 'type', render: (text, record, index) => { @@ -281,7 +188,6 @@ const ChannelsTable = () => { }, }, { - key: COLUMN_KEYS.STATUS, title: t('状态'), dataIndex: 'status', render: (text, record, index) => { @@ -307,7 +213,6 @@ const ChannelsTable = () => { }, }, { - key: COLUMN_KEYS.RESPONSE_TIME, title: t('响应时间'), dataIndex: 'response_time', render: (text, record, index) => ( @@ -315,7 +220,6 @@ const ChannelsTable = () => { ), }, { - key: COLUMN_KEYS.BALANCE, title: t('已用/剩余'), dataIndex: 'expired_time', render: (text, record, index) => { @@ -354,7 +258,6 @@ const ChannelsTable = () => { }, }, { - key: COLUMN_KEYS.PRIORITY, title: t('优先级'), dataIndex: 'priority', render: (text, record, index) => { @@ -406,7 +309,6 @@ const ChannelsTable = () => { }, }, { - key: COLUMN_KEYS.WEIGHT, title: t('权重'), dataIndex: 'weight', render: (text, record, index) => { @@ -458,7 +360,6 @@ const ChannelsTable = () => { }, }, { - key: COLUMN_KEYS.OPERATE, title: '', dataIndex: 'operate', fixed: 'right', @@ -631,96 +532,10 @@ const ChannelsTable = () => { }, ]; - // Filter columns based on visibility settings - const getVisibleColumns = () => { - return allColumns.filter((column) => visibleColumns[column.key]); - }; - - // Column selector modal - const renderColumnSelector = () => { - return ( - setShowColumnSelector(false)} - footer={ -
- - - -
- } - size="middle" - centered={true} - > -
- v === true)} - indeterminate={ - Object.values(visibleColumns).some((v) => v === true) && - !Object.values(visibleColumns).every((v) => v === true) - } - onChange={(e) => handleSelectAll(e.target.checked)} - > - {t('全选')} - -
-
- {allColumns.map((column) => { - // Skip columns without title - if (!column.title) { - return null; - } - - return ( -
- - handleColumnVisibilityChange(column.key, e.target.checked) - } - > - {column.title} - -
- ); - })} -
-
- ); - }; - const [channels, setChannels] = useState([]); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); const [idSort, setIdSort] = useState(false); - const [searchKeyword, setSearchKeyword] = useState(''); - const [searchGroup, setSearchGroup] = useState(''); - const [searchModel, setSearchModel] = useState(''); const [searching, setSearching] = useState(false); const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); const [channelCount, setChannelCount] = useState(pageSize); @@ -745,6 +560,16 @@ const ChannelsTable = () => { const [testQueue, setTestQueue] = useState([]); const [isProcessingQueue, setIsProcessingQueue] = useState(false); + // Form API 引用 + const [formApi, setFormApi] = useState(null); + + // Form 初始值 + const formInitValues = { + searchKeyword: '', + searchGroup: '', + searchModel: '', + }; + const removeRecord = (record) => { let newDataSource = [...channels]; if (record.id != null) { @@ -896,15 +721,11 @@ const ChannelsTable = () => { }; const refresh = async () => { + const { searchKeyword, searchGroup, searchModel } = getFormValues(); if (searchKeyword === '' && searchGroup === '' && searchModel === '') { await loadChannels(activePage - 1, pageSize, idSort, enableTagMode); } else { - await searchChannels( - searchKeyword, - searchGroup, - searchModel, - enableTagMode, - ); + await searchChannels(enableTagMode); } }; @@ -1010,12 +831,19 @@ const ChannelsTable = () => { } }; - const searchChannels = async ( - searchKeyword, - searchGroup, - searchModel, - enableTagMode, - ) => { + // 获取表单值的辅助函数,确保所有值都是字符串 + const getFormValues = () => { + const formValues = formApi ? formApi.getValues() : {}; + return { + searchKeyword: formValues.searchKeyword || '', + searchGroup: formValues.searchGroup || '', + searchModel: formValues.searchModel || '', + }; + }; + + const searchChannels = async (enableTagMode) => { + const { searchKeyword, searchGroup, searchModel } = getFormValues(); + if (searchKeyword === '' && searchGroup === '' && searchModel === '') { await loadChannels(activePage - 1, pageSize, idSort, enableTagMode); // setActivePage(1); @@ -1540,71 +1368,83 @@ const ChannelsTable = () => { > {t('刷新')} - -
-
- } - placeholder={t('搜索渠道的 ID,名称,密钥和API地址 ...')} - value={searchKeyword} - loading={searching} - onChange={(v) => { - setSearchKeyword(v.trim()); - }} - className="!rounded-full" - showClear - /> -
-
- } - placeholder={t('模型关键字')} - value={searchModel} - loading={searching} - onChange={(v) => { - setSearchModel(v.trim()); - }} - className="!rounded-full" - showClear - /> -
-
- } - placeholder={t('任务 ID')} - value={mj_id} - onChange={(value) => handleInputChange(value, 'mj_id')} - className="!rounded-full" - showClear - /> - - {/* 渠道 ID - 仅管理员可见 */} - {isAdminUser && ( - } - placeholder={t('渠道 ID')} - value={channel_id} - onChange={(value) => handleInputChange(value, 'channel_id')} + placeholder={t('任务 ID')} className="!rounded-full" showClear + pure /> - )} -
- {/* 操作按钮区域 */} -
-
-
- - + {/* 渠道 ID - 仅管理员可见 */} + {isAdminUser && ( + } + placeholder={t('渠道 ID')} + className="!rounded-full" + showClear + pure + /> + )} +
+ + {/* 操作按钮区域 */} +
+
+
+ + + +
-
+ } shadows='always' diff --git a/web/src/components/table/RedemptionsTable.js b/web/src/components/table/RedemptionsTable.js index 62ccb7ac..bf8985aa 100644 --- a/web/src/components/table/RedemptionsTable.js +++ b/web/src/components/table/RedemptionsTable.js @@ -14,7 +14,7 @@ import { Card, Divider, Dropdown, - Input, + Form, Modal, Popover, Space, @@ -223,7 +223,6 @@ const RedemptionsTable = () => { const [redemptions, setRedemptions] = useState([]); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); - const [searchKeyword, setSearchKeyword] = useState(''); const [searching, setSearching] = useState(false); const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE); const [selectedKeys, setSelectedKeys] = useState([]); @@ -233,6 +232,22 @@ const RedemptionsTable = () => { }); 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 = () => { setShowEdit(false); setTimeout(() => { @@ -340,8 +355,14 @@ const RedemptionsTable = () => { setLoading(false); }; - const searchRedemptions = async (keyword, page, pageSize) => { - if (searchKeyword === '') { + const searchRedemptions = async (keyword = null, page, pageSize) => { + // 如果没有传递keyword参数,从表单获取值 + if (keyword === null) { + const formValues = getFormValues(); + keyword = formValues.searchKeyword; + } + + if (keyword === '') { await loadRedemptions(page, pageSize); return; } @@ -361,10 +382,6 @@ const RedemptionsTable = () => { setSearching(false); }; - const handleKeywordChange = async (value) => { - setSearchKeyword(value.trim()); - }; - const sortRedemption = (key) => { if (redemptions.length === 0) return; setLoading(true); @@ -381,6 +398,7 @@ const RedemptionsTable = () => { const handlePageChange = (page) => { setActivePage(page); + const { searchKeyword } = getFormValues(); if (searchKeyword === '') { loadRedemptions(page, pageSize).then(); } else { @@ -457,28 +475,59 @@ const RedemptionsTable = () => { -
-
- } - placeholder={t('关键字(id或者名称)')} - value={searchKeyword} - onChange={handleKeywordChange} - className="!rounded-full" - showClear - /> +
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" + > +
+
+ } + placeholder={t('关键字(id或者名称)')} + className="!rounded-full" + showClear + pure + /> +
+
+ + +
- -
+
); @@ -517,6 +566,7 @@ const RedemptionsTable = () => { onPageSizeChange: (size) => { setPageSize(size); setActivePage(1); + const { searchKeyword } = getFormValues(); if (searchKeyword === '') { loadRedemptions(1, size).then(); } else { diff --git a/web/src/components/table/TaskLogsTable.js b/web/src/components/table/TaskLogsTable.js index 91ccc06c..cc8cd6d5 100644 --- a/web/src/components/table/TaskLogsTable.js +++ b/web/src/components/table/TaskLogsTable.js @@ -13,9 +13,8 @@ import { Button, Card, Checkbox, - DatePicker, Divider, - Input, + Form, Layout, Modal, Progress, @@ -437,21 +436,43 @@ const LogsTable = () => { const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); const [logCount, setLogCount] = useState(ITEMS_PER_PAGE); - const [logType] = useState(0); let now = new Date(); // 初始化start_timestamp为前一天 let zeroNow = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const [inputs, setInputs] = useState({ + + // Form 初始值 + const formInitValues = { channel_id: '', task_id: '', - start_timestamp: timestamp2string(zeroNow.getTime() / 1000), - end_timestamp: '', - }); - const { channel_id, task_id, start_timestamp, end_timestamp } = inputs; + dateRange: [ + timestamp2string(zeroNow.getTime() / 1000), + timestamp2string(now.getTime() / 1000 + 3600) + ], + }; - const handleInputChange = (value, name) => { - setInputs((inputs) => ({ ...inputs, [name]: value })); + // Form API 引用 + const [formApi, setFormApi] = useState(null); + + // 获取表单值的辅助函数 + const getFormValues = () => { + const formValues = formApi ? formApi.getValues() : {}; + + // 处理时间范围 + let start_timestamp = timestamp2string(zeroNow.getTime() / 1000); + let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600); + + if (formValues.dateRange && Array.isArray(formValues.dateRange) && formValues.dateRange.length === 2) { + start_timestamp = formValues.dateRange[0]; + end_timestamp = formValues.dateRange[1]; + } + + return { + channel_id: formValues.channel_id || '', + task_id: formValues.task_id || '', + start_timestamp, + end_timestamp, + }; }; const setLogsFormat = (logs) => { @@ -469,6 +490,7 @@ const LogsTable = () => { setLoading(true); let url = ''; + const { channel_id, task_id, start_timestamp, end_timestamp } = getFormValues(); let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000); let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000); if (isAdminUser) { @@ -528,7 +550,7 @@ const LogsTable = () => { const localPageSize = parseInt(localStorage.getItem('task-page-size')) || ITEMS_PER_PAGE; setPageSize(localPageSize); loadLogs(0, localPageSize).then(); - }, [logType]); + }, []); // 列选择器模态框 const renderColumnSelector = () => { @@ -628,70 +650,93 @@ const LogsTable = () => { {/* 搜索表单区域 */} -
-
- {/* 时间选择器 */} -
- { - if (Array.isArray(value) && value.length === 2) { - handleInputChange(value[0], 'start_timestamp'); - handleInputChange(value[1], 'end_timestamp'); - } - }} - /> -
+
setFormApi(api)} + onSubmit={refresh} + allowEmpty={true} + autoComplete="off" + layout="vertical" + trigger="change" + stopValidateWithError={false} + > +
+
+ {/* 时间选择器 */} +
+ +
- {/* 任务 ID */} - } - placeholder={t('任务 ID')} - value={task_id} - onChange={(value) => handleInputChange(value, 'task_id')} - className="!rounded-full" - showClear - /> - - {/* 渠道 ID - 仅管理员可见 */} - {isAdminUser && ( - } - placeholder={t('渠道 ID')} - value={channel_id} - onChange={(value) => handleInputChange(value, 'channel_id')} + placeholder={t('任务 ID')} className="!rounded-full" showClear + pure /> - )} -
- {/* 操作按钮区域 */} -
-
-
- - + {/* 渠道 ID - 仅管理员可见 */} + {isAdminUser && ( + } + placeholder={t('渠道 ID')} + className="!rounded-full" + showClear + pure + /> + )} +
+ + {/* 操作按钮区域 */} +
+
+
+ + + +
-
+
} shadows='always' diff --git a/web/src/components/table/TokensTable.js b/web/src/components/table/TokensTable.js index 7d4f5af0..9d0ec522 100644 --- a/web/src/components/table/TokensTable.js +++ b/web/src/components/table/TokensTable.js @@ -14,12 +14,12 @@ import { Button, Card, Dropdown, + Form, Modal, Space, SplitButtonGroup, Table, Tag, - Input, } from '@douyinfe/semi-ui'; import { @@ -335,14 +335,29 @@ const TokensTable = () => { const [tokenCount, setTokenCount] = useState(pageSize); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); - const [searchKeyword, setSearchKeyword] = useState(''); - const [searchToken, setSearchToken] = useState(''); const [searching, setSearching] = useState(false); - const [chats, setChats] = useState([]); const [editingToken, setEditingToken] = useState({ 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 = () => { setShowEdit(false); setTimeout(() => { @@ -416,8 +431,6 @@ const TokensTable = () => { window.open(url, '_blank'); }; - - useEffect(() => { loadTokens(0) .then() @@ -472,6 +485,7 @@ const TokensTable = () => { }; const searchTokens = async () => { + const { searchKeyword, searchToken } = getFormValues(); if (searchKeyword === '' && searchToken === '') { await loadTokens(0); setActivePage(1); @@ -491,14 +505,6 @@ const TokensTable = () => { setSearching(false); }; - const handleKeywordChange = async (value) => { - setSearchKeyword(value.trim()); - }; - - const handleSearchTokenChange = async (value) => { - setSearchToken(value.trim()); - }; - const sortToken = (key) => { if (tokens.length === 0) return; setLoading(true); @@ -580,36 +586,65 @@ const TokensTable = () => {
-
-
- } - placeholder={t('搜索关键字')} - value={searchKeyword} - onChange={handleKeywordChange} - className="!rounded-full" - showClear - /> +
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" + > +
+
+ } + placeholder={t('搜索关键字')} + className="!rounded-full" + showClear + pure + /> +
+
+ } + placeholder={t('密钥')} + className="!rounded-full" + showClear + pure + /> +
+
+ + +
-
- } - placeholder={t('密钥')} - value={searchToken} - onChange={handleSearchTokenChange} - className="!rounded-full" - showClear - /> -
- -
+
); diff --git a/web/src/components/table/UsersTable.js b/web/src/components/table/UsersTable.js index 8c713a1a..247a015a 100644 --- a/web/src/components/table/UsersTable.js +++ b/web/src/components/table/UsersTable.js @@ -5,9 +5,8 @@ import { Card, Divider, Dropdown, - Input, + Form, Modal, - Select, Space, Table, Tag, @@ -285,9 +284,7 @@ const UsersTable = () => { const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); - const [searchKeyword, setSearchKeyword] = useState(''); const [searching, setSearching] = useState(false); - const [searchGroup, setSearchGroup] = useState(''); const [groupOptions, setGroupOptions] = useState([]); const [userCount, setUserCount] = useState(ITEMS_PER_PAGE); const [showAddUser, setShowAddUser] = useState(false); @@ -296,6 +293,24 @@ const UsersTable = () => { id: undefined, }); + // Form 初始值 + const formInitValues = { + searchKeyword: '', + searchGroup: '', + }; + + // Form API 引用 + const [formApi, setFormApi] = useState(null); + + // 获取表单值的辅助函数 + const getFormValues = () => { + const formValues = formApi ? formApi.getValues() : {}; + return { + searchKeyword: formValues.searchKeyword || '', + searchGroup: formValues.searchGroup || '', + }; + }; + const removeRecord = (key) => { let newDataSource = [...users]; if (key != null) { @@ -363,9 +378,16 @@ const UsersTable = () => { const searchUsers = async ( startIdx, pageSize, - searchKeyword, - searchGroup, + searchKeyword = null, + searchGroup = null, ) => { + // 如果没有传递参数,从表单获取值 + if (searchKeyword === null || searchGroup === null) { + const formValues = getFormValues(); + searchKeyword = formValues.searchKeyword; + searchGroup = formValues.searchGroup; + } + if (searchKeyword === '' && searchGroup === '') { // if keyword is blank, load files instead. await loadUsers(startIdx, pageSize); @@ -387,12 +409,9 @@ const UsersTable = () => { setSearching(false); }; - const handleKeywordChange = async (value) => { - setSearchKeyword(value.trim()); - }; - const handlePageChange = (page) => { setActivePage(page); + const { searchKeyword, searchGroup } = getFormValues(); if (searchKeyword === '' && searchGroup === '') { loadUsers(page, pageSize).then(); } else { @@ -413,10 +432,11 @@ const UsersTable = () => { const refresh = async () => { setActivePage(1); - if (searchKeyword === '') { - await loadUsers(activePage, pageSize); + const { searchKeyword, searchGroup } = getFormValues(); + if (searchKeyword === '' && searchGroup === '') { + await loadUsers(1, pageSize); } else { - await searchUsers(activePage, pageSize, searchKeyword, searchGroup); + await searchUsers(1, pageSize, searchKeyword, searchGroup); } }; @@ -488,41 +508,76 @@ const UsersTable = () => { -
-
- } - placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')} - value={searchKeyword} - onChange={handleKeywordChange} - className="!rounded-full" - showClear - /> +
setFormApi(api)} + onSubmit={() => { + setActivePage(1); + searchUsers(1, pageSize); + }} + allowEmpty={true} + autoComplete="off" + layout="horizontal" + trigger="change" + stopValidateWithError={false} + className="w-full md:w-auto order-1 md:order-2" + > +
+
+ } + placeholder={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')} + className="!rounded-full" + showClear + pure + /> +
+
+ { + // 分组变化时自动搜索 + setTimeout(() => { + setActivePage(1); + searchUsers(1, pageSize); + }, 100); + }} + className="!rounded-full w-full" + showClear + pure + /> +
+
+ + +
-
- setRedemptionCode(value)} + size="large" + className="!rounded-lg" + prefix={} + /> +
+ +
+ {topUpLink && ( + + )} + +
+
+
+ + + {/* 右侧:邀请信息部分 */} +
-
- +
+
@@ -524,7 +703,7 @@ const TopUp = () => { theme="solid" onClick={() => setOpenTransfer(true)} size="small" - className="!rounded-lg !bg-blue-500 hover:!bg-blue-600" + className="!rounded-lg !bg-slate-600 hover:!bg-slate-700" icon={} > {t('划转')} @@ -536,7 +715,7 @@ const TopUp = () => {
-
+
{
{renderQuota(userState?.user?.aff_quota)}
-
{
-
-
-
- -
-
- {t('兑换余额')} -
{t('使用兑换码充值余额')}
-
-
- -
-
- {t('兑换码')} - setRedemptionCode(value)} - size="large" - className="!rounded-lg" - prefix={} - /> -
- -
- {topUpLink && ( - - )} - -
-
-
- -
-
-
- -
-
- {t('在线充值')} -
{t('支持多种支付方式')}
-
-
- -
-
-
- {t('充值数量')} - {amountLoading ? ( - - ) : ( - {t('实付金额:') + ' ' + renderAmount()} - )} -
- { - if (value && value >= 1) { - setTopUpCount(value); - await getAmount(value); - } - }} - onBlur={(e) => { - const value = parseInt(e.target.value); - if (!value || value < 1) { - setTopUpCount(1); - getAmount(1); - } - }} - size="large" - className="!rounded-lg w-full" - prefix={} - formatter={(value) => value ? `${value}` : ''} - parser={(value) => value ? parseInt(value.replace(/[^\d]/g, '')) : 0} - /> -
- -
- - -
- - {!enableOnlineTopUp && ( - - {t('在线充值功能未开启')} -
- } - description={ -
- {t('管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。')} -
- } - /> - )} -
-