From e685876cc00ce215ebd5a8e6c6f2ef836df3c15b Mon Sep 17 00:00:00 2001 From: RockYang Date: Tue, 27 May 2025 08:16:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=20SSE=20=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/core/types/chat.go | 11 - api/handler/chat_handler.go | 421 ++++++++++++------------- api/handler/chat_openai_handler.go | 481 ++++++++++++++--------------- web/src/assets/css/chat-plus.styl | 2 +- web/src/components/ChatPrompt.vue | 35 +-- web/src/components/ChatSetting.vue | 58 ++-- web/src/views/ChatPlus.vue | 26 +- 7 files changed, 487 insertions(+), 547 deletions(-) diff --git a/api/core/types/chat.go b/api/core/types/chat.go index aa8ac01f..20805378 100644 --- a/api/core/types/chat.go +++ b/api/core/types/chat.go @@ -52,17 +52,6 @@ type Delta struct { } `json:"function_call,omitempty"` } -// ChatSession 聊天会话对象 -type ChatSession struct { - UserId uint `json:"user_id"` - ClientIP string `json:"client_ip"` // 客户端 IP - ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段 - Model ChatModel `json:"model"` // GPT 模型 - Start int64 `json:"start"` // 开始请求时间戳 - Tools []int `json:"tools"` // 工具函数列表 - Stream bool `json:"stream"` // 是否采用流式输出 -} - type ChatModel struct { Id uint `json:"id"` Name string `json:"name"` diff --git a/api/handler/chat_handler.go b/api/handler/chat_handler.go index ea8ccc33..b6317b24 100644 --- a/api/handler/chat_handler.go +++ b/api/handler/chat_handler.go @@ -8,7 +8,6 @@ package handler // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( - "bufio" "bytes" "context" "encoding/json" @@ -33,7 +32,6 @@ import ( "github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" - req2 "github.com/imroc/req/v3" "github.com/sashabaranov/go-openai" "gorm.io/gorm" ) @@ -117,17 +115,6 @@ func (h *ChatHandler) Chat(c *gin.Context) { return } - session := &types.ChatSession{ - ClientIP: c.ClientIP(), - UserId: data.UserId, - ChatId: data.ChatId, - Tools: data.Tools, - Stream: data.Stream, - Model: types.ChatModel{ - KeyId: data.ModelId, - }, - } - // 使用旧的聊天数据覆盖模型和角色ID var chat model.ChatItem h.DB.Where("chat_id", data.ChatId).First(&chat) @@ -289,7 +276,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio logger.Debugf("聊天上下文:%+v", chatCtx) } - reqMgs := make([]interface{}, 0) + reqMgs := make([]any, 0) for i := len(chatCtx) - 1; i >= 0; i-- { reqMgs = append(reqMgs, chatCtx[i]) @@ -297,6 +284,8 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio fullPrompt := prompt text := prompt + + for _, file := range session.Files { // extract files in prompt files := utils.ExtractFileURLs(prompt) logger.Debugf("detected FILES: %+v", files) @@ -688,220 +677,220 @@ func (h *ChatHandler) TextToSpeech(c *gin.Context) { c.Writer.Write(audioBytes) } -// OPenAI 消息发送实现 -func (h *ChatHandler) sendOpenAiMessage( - req types.ApiRequest, - userVo vo.User, - ctx context.Context, - session *types.ChatSession, - role model.ChatRole, - prompt string, - c *gin.Context) error { - promptCreatedAt := time.Now() // 记录提问时间 - start := time.Now() - var apiKey = model.ApiKey{} - response, err := h.doRequest(ctx, req, session, &apiKey) - logger.Info("HTTP请求完成,耗时:", time.Since(start)) - if err != nil { - if strings.Contains(err.Error(), "context canceled") { - return fmt.Errorf("用户取消了请求:%s", prompt) - } else if strings.Contains(err.Error(), "no available key") { - return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!") - } - return err - } else { - defer response.Body.Close() - } +// // OPenAI 消息发送实现 +// func (h *ChatHandler) sendOpenAiMessage( +// req types.ApiRequest, +// userVo vo.User, +// ctx context.Context, +// session *types.ChatSession, +// role model.ChatRole, +// prompt string, +// c *gin.Context) error { +// promptCreatedAt := time.Now() // 记录提问时间 +// start := time.Now() +// var apiKey = model.ApiKey{} +// response, err := h.doRequest(ctx, req, session, &apiKey) +// logger.Info("HTTP请求完成,耗时:", time.Since(start)) +// if err != nil { +// if strings.Contains(err.Error(), "context canceled") { +// return fmt.Errorf("用户取消了请求:%s", prompt) +// } else if strings.Contains(err.Error(), "no available key") { +// return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!") +// } +// return err +// } else { +// defer response.Body.Close() +// } - if response.StatusCode != 200 { - body, _ := io.ReadAll(response.Body) - return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body)) - } +// if response.StatusCode != 200 { +// body, _ := io.ReadAll(response.Body) +// return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body)) +// } - contentType := response.Header.Get("Content-Type") - if strings.Contains(contentType, "text/event-stream") { - replyCreatedAt := time.Now() // 记录回复时间 - // 循环读取 Chunk 消息 - var message = types.Message{Role: "assistant"} - var contents = make([]string, 0) - var function model.Function - var toolCall = false - var arguments = make([]string, 0) - var reasoning = false +// contentType := response.Header.Get("Content-Type") +// if strings.Contains(contentType, "text/event-stream") { +// replyCreatedAt := time.Now() // 记录回复时间 +// // 循环读取 Chunk 消息 +// var message = types.Message{Role: "assistant"} +// var contents = make([]string, 0) +// var function model.Function +// var toolCall = false +// var arguments = make([]string, 0) +// var reasoning = false - pushMessage(c, ChatEventStart, "开始响应") - scanner := bufio.NewScanner(response.Body) - for scanner.Scan() { - line := scanner.Text() - if !strings.Contains(line, "data:") || len(line) < 30 { - continue - } - var responseBody = types.ApiResponse{} - err = json.Unmarshal([]byte(line[6:]), &responseBody) - if err != nil { // 数据解析出错 - return errors.New(line) - } - if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行 - continue - } - if responseBody.Choices[0].Delta.Content == nil && - responseBody.Choices[0].Delta.ToolCalls == nil && - responseBody.Choices[0].Delta.ReasoningContent == "" { - continue - } +// pushMessage(c, ChatEventStart, "开始响应") +// scanner := bufio.NewScanner(response.Body) +// for scanner.Scan() { +// line := scanner.Text() +// if !strings.Contains(line, "data:") || len(line) < 30 { +// continue +// } +// var responseBody = types.ApiResponse{} +// err = json.Unmarshal([]byte(line[6:]), &responseBody) +// if err != nil { // 数据解析出错 +// return errors.New(line) +// } +// if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行 +// continue +// } +// if responseBody.Choices[0].Delta.Content == nil && +// responseBody.Choices[0].Delta.ToolCalls == nil && +// responseBody.Choices[0].Delta.ReasoningContent == "" { +// continue +// } - if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 { - pushMessage(c, ChatEventError, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。") - break - } +// if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 { +// pushMessage(c, ChatEventError, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。") +// break +// } - var tool types.ToolCall - if len(responseBody.Choices[0].Delta.ToolCalls) > 0 { - tool = responseBody.Choices[0].Delta.ToolCalls[0] - if toolCall && tool.Function.Name == "" { - arguments = append(arguments, tool.Function.Arguments) - continue - } - } +// var tool types.ToolCall +// if len(responseBody.Choices[0].Delta.ToolCalls) > 0 { +// tool = responseBody.Choices[0].Delta.ToolCalls[0] +// if toolCall && tool.Function.Name == "" { +// arguments = append(arguments, tool.Function.Arguments) +// continue +// } +// } - // 兼容 Function Call - fun := responseBody.Choices[0].Delta.FunctionCall - if fun.Name != "" { - tool = *new(types.ToolCall) - tool.Function.Name = fun.Name - } else if toolCall { - arguments = append(arguments, fun.Arguments) - continue - } +// // 兼容 Function Call +// fun := responseBody.Choices[0].Delta.FunctionCall +// if fun.Name != "" { +// tool = *new(types.ToolCall) +// tool.Function.Name = fun.Name +// } else if toolCall { +// arguments = append(arguments, fun.Arguments) +// continue +// } - if !utils.IsEmptyValue(tool) { - res := h.DB.Where("name = ?", tool.Function.Name).First(&function) - if res.Error == nil { - toolCall = true - callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label) - pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ - "type": "text", - "content": callMsg, - }) - contents = append(contents, callMsg) - } - continue - } +// if !utils.IsEmptyValue(tool) { +// res := h.DB.Where("name = ?", tool.Function.Name).First(&function) +// if res.Error == nil { +// toolCall = true +// callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label) +// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ +// "type": "text", +// "content": callMsg, +// }) +// contents = append(contents, callMsg) +// } +// continue +// } - if responseBody.Choices[0].FinishReason == "tool_calls" || - responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕 - break - } +// if responseBody.Choices[0].FinishReason == "tool_calls" || +// responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕 +// break +// } - // output stopped - if responseBody.Choices[0].FinishReason != "" { - break // 输出完成或者输出中断了 - } else { // 正常输出结果 - // 兼容思考过程 - if responseBody.Choices[0].Delta.ReasoningContent != "" { - reasoningContent := responseBody.Choices[0].Delta.ReasoningContent - if !reasoning { - reasoningContent = fmt.Sprintf("%s", reasoningContent) - reasoning = true - } +// // output stopped +// if responseBody.Choices[0].FinishReason != "" { +// break // 输出完成或者输出中断了 +// } else { // 正常输出结果 +// // 兼容思考过程 +// if responseBody.Choices[0].Delta.ReasoningContent != "" { +// reasoningContent := responseBody.Choices[0].Delta.ReasoningContent +// if !reasoning { +// reasoningContent = fmt.Sprintf("%s", reasoningContent) +// reasoning = true +// } - pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ - "type": "text", - "content": reasoningContent, - }) - contents = append(contents, reasoningContent) - } else if responseBody.Choices[0].Delta.Content != "" { - finalContent := responseBody.Choices[0].Delta.Content - if reasoning { - finalContent = fmt.Sprintf("%s", responseBody.Choices[0].Delta.Content) - reasoning = false - } - contents = append(contents, utils.InterfaceToString(finalContent)) - pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ - "type": "text", - "content": finalContent, - }) - } - } - } // end for +// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ +// "type": "text", +// "content": reasoningContent, +// }) +// contents = append(contents, reasoningContent) +// } else if responseBody.Choices[0].Delta.Content != "" { +// finalContent := responseBody.Choices[0].Delta.Content +// if reasoning { +// finalContent = fmt.Sprintf("%s", responseBody.Choices[0].Delta.Content) +// reasoning = false +// } +// contents = append(contents, utils.InterfaceToString(finalContent)) +// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ +// "type": "text", +// "content": finalContent, +// }) +// } +// } +// } // end for - if err := scanner.Err(); err != nil { - if strings.Contains(err.Error(), "context canceled") { - logger.Info("用户取消了请求:", prompt) - } else { - logger.Error("信息读取出错:", err) - } - } +// if err := scanner.Err(); err != nil { +// if strings.Contains(err.Error(), "context canceled") { +// logger.Info("用户取消了请求:", prompt) +// } else { +// logger.Error("信息读取出错:", err) +// } +// } - if toolCall { // 调用函数完成任务 - params := make(map[string]any) - _ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms) - logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params) - params["user_id"] = userVo.Id - var apiRes types.BizVo - r, err := req2.C().R().SetHeader("Body-Type", "application/json"). - SetHeader("Authorization", function.Token). - SetBody(params).Post(function.Action) - errMsg := "" - if err != nil { - errMsg = err.Error() - } else { - all, _ := io.ReadAll(r.Body) - err = json.Unmarshal(all, &apiRes) - if err != nil { - errMsg = err.Error() - } else if apiRes.Code != types.Success { - errMsg = apiRes.Message - } - } +// if toolCall { // 调用函数完成任务 +// params := make(map[string]any) +// _ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms) +// logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params) +// params["user_id"] = userVo.Id +// var apiRes types.BizVo +// r, err := req2.C().R().SetHeader("Body-Type", "application/json"). +// SetHeader("Authorization", function.Token). +// SetBody(params).Post(function.Action) +// errMsg := "" +// if err != nil { +// errMsg = err.Error() +// } else { +// all, _ := io.ReadAll(r.Body) +// err = json.Unmarshal(all, &apiRes) +// if err != nil { +// errMsg = err.Error() +// } else if apiRes.Code != types.Success { +// errMsg = apiRes.Message +// } +// } - if errMsg != "" { - errMsg = "调用函数工具出错:" + errMsg - contents = append(contents, errMsg) - } else { - errMsg = utils.InterfaceToString(apiRes.Data) - contents = append(contents, errMsg) - } - pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ - "type": "text", - "content": errMsg, - }) - } +// if errMsg != "" { +// errMsg = "调用函数工具出错:" + errMsg +// contents = append(contents, errMsg) +// } else { +// errMsg = utils.InterfaceToString(apiRes.Data) +// contents = append(contents, errMsg) +// } +// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ +// "type": "text", +// "content": errMsg, +// }) +// } - // 消息发送成功 - if len(contents) > 0 { - usage := Usage{ - Prompt: prompt, - Content: strings.Join(contents, ""), - PromptTokens: 0, - CompletionTokens: 0, - TotalTokens: 0, - } - message.Content = usage.Content - h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt) - } - } else { - var respVo OpenAIResVo - body, err := io.ReadAll(response.Body) - if err != nil { - return fmt.Errorf("读取响应失败:%v", body) - } - err = json.Unmarshal(body, &respVo) - if err != nil { - return fmt.Errorf("解析响应失败:%v", body) - } - content := respVo.Choices[0].Message.Content - if strings.HasPrefix(req.Model, "o1-") { - content = fmt.Sprintf("AI思考结束,耗时:%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Content) - } - pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ - "type": "text", - "content": content, - }) - respVo.Usage.Prompt = prompt - respVo.Usage.Content = content - h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now()) - } +// // 消息发送成功 +// if len(contents) > 0 { +// usage := Usage{ +// Prompt: prompt, +// Content: strings.Join(contents, ""), +// PromptTokens: 0, +// CompletionTokens: 0, +// TotalTokens: 0, +// } +// message.Content = usage.Content +// h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt) +// } +// } else { +// var respVo OpenAIResVo +// body, err := io.ReadAll(response.Body) +// if err != nil { +// return fmt.Errorf("读取响应失败:%v", body) +// } +// err = json.Unmarshal(body, &respVo) +// if err != nil { +// return fmt.Errorf("解析响应失败:%v", body) +// } +// content := respVo.Choices[0].Message.Content +// if strings.HasPrefix(req.Model, "o1-") { +// content = fmt.Sprintf("AI思考结束,耗时:%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Content) +// } +// pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ +// "type": "text", +// "content": content, +// }) +// respVo.Usage.Prompt = prompt +// respVo.Usage.Content = content +// h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now()) +// } - return nil -} +// return nil +// } diff --git a/api/handler/chat_openai_handler.go b/api/handler/chat_openai_handler.go index 00c13093..ef9a5722 100644 --- a/api/handler/chat_openai_handler.go +++ b/api/handler/chat_openai_handler.go @@ -1,271 +1,254 @@ package handler -// // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// // * Copyright 2023 The Geek-AI Authors. All rights reserved. -// // * Use of this source code is governed by a Apache-2.0 license -// // * that can be found in the LICENSE file. -// // * @Author yangjian102621@163.com -// // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * Copyright 2023 The Geek-AI Authors. All rights reserved. +// * Use of this source code is governed by a Apache-2.0 license +// * that can be found in the LICENSE file. +// * @Author yangjian102621@163.com +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// import ( -// "bufio" -// "context" -// "encoding/json" -// "errors" -// "fmt" -// "geekai/core/types" -// "geekai/store/model" -// "geekai/store/vo" -// "geekai/utils" -// "io" -// "strings" -// "time" +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "geekai/core/types" + "geekai/store/model" + "geekai/store/vo" + "geekai/utils" + "io" + "strings" + "time" -// req2 "github.com/imroc/req/v3" -// ) + "github.com/gin-gonic/gin" + req2 "github.com/imroc/req/v3" +) -// type Usage struct { -// Prompt string `json:"prompt,omitempty"` -// Content string `json:"content,omitempty"` -// PromptTokens int `json:"prompt_tokens"` -// CompletionTokens int `json:"completion_tokens"` -// TotalTokens int `json:"total_tokens"` -// } +type Usage struct { + Prompt string `json:"prompt,omitempty"` + Content string `json:"content,omitempty"` + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` +} -// type OpenAIResVo struct { -// Id string `json:"id"` -// Object string `json:"object"` -// Created int `json:"created"` -// Model string `json:"model"` -// SystemFingerprint string `json:"system_fingerprint"` -// Choices []struct { -// Index int `json:"index"` -// Message struct { -// Role string `json:"role"` -// Content string `json:"content"` -// } `json:"message"` -// Logprobs interface{} `json:"logprobs"` -// FinishReason string `json:"finish_reason"` -// } `json:"choices"` -// Usage Usage `json:"usage"` -// } +type OpenAIResVo struct { + Id string `json:"id"` + Object string `json:"object"` + Created int `json:"created"` + Model string `json:"model"` + SystemFingerprint string `json:"system_fingerprint"` + Choices []struct { + Index int `json:"index"` + Message struct { + Role string `json:"role"` + Content string `json:"content"` + } `json:"message"` + Logprobs interface{} `json:"logprobs"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` + Usage Usage `json:"usage"` +} -// // OPenAI 消息发送实现 -// func (h *ChatHandler) sendOpenAiMessage( -// req types.ApiRequest, -// userVo vo.User, -// ctx context.Context, -// session *types.ChatSession, -// role model.ChatRole, -// prompt string, -// messageChan chan interface{}) error { -// promptCreatedAt := time.Now() // 记录提问时间 -// start := time.Now() -// var apiKey = model.ApiKey{} -// response, err := h.doRequest(ctx, req, session, &apiKey) -// logger.Info("HTTP请求完成,耗时:", time.Since(start)) -// if err != nil { -// if strings.Contains(err.Error(), "context canceled") { -// return fmt.Errorf("用户取消了请求:%s", prompt) -// } else if strings.Contains(err.Error(), "no available key") { -// return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!") -// } -// return err -// } else { -// defer response.Body.Close() -// } +// OPenAI 消息发送实现 +func (h *ChatHandler) sendOpenAiMessage( + req types.ApiRequest, + userVo vo.User, + ctx context.Context, + session *types.ChatSession, + role model.ChatRole, + prompt string, + c *gin.Context) error { + promptCreatedAt := time.Now() // 记录提问时间 + start := time.Now() + var apiKey = model.ApiKey{} + response, err := h.doRequest(ctx, req, session, &apiKey) + logger.Info("HTTP请求完成,耗时:", time.Since(start)) + if err != nil { + if strings.Contains(err.Error(), "context canceled") { + return fmt.Errorf("用户取消了请求:%s", prompt) + } else if strings.Contains(err.Error(), "no available key") { + return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!") + } + return err + } else { + defer response.Body.Close() + } -// if response.StatusCode != 200 { -// body, _ := io.ReadAll(response.Body) -// return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body)) -// } + if response.StatusCode != 200 { + body, _ := io.ReadAll(response.Body) + return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body)) + } -// contentType := response.Header.Get("Content-Type") -// if strings.Contains(contentType, "text/event-stream") { -// replyCreatedAt := time.Now() // 记录回复时间 -// // 循环读取 Chunk 消息 -// var message = types.Message{Role: "assistant"} -// var contents = make([]string, 0) -// var function model.Function -// var toolCall = false -// var arguments = make([]string, 0) -// var reasoning = false + contentType := response.Header.Get("Content-Type") + if strings.Contains(contentType, "text/event-stream") { + replyCreatedAt := time.Now() // 记录回复时间 + // 循环读取 Chunk 消息 + var message = types.Message{Role: "assistant"} + var contents = make([]string, 0) + var function model.Function + var toolCall = false + var arguments = make([]string, 0) + var reasoning = false -// scanner := bufio.NewScanner(response.Body) -// for scanner.Scan() { -// line := scanner.Text() -// if !strings.Contains(line, "data:") || len(line) < 30 { -// continue -// } -// var responseBody = types.ApiResponse{} -// err = json.Unmarshal([]byte(line[6:]), &responseBody) -// if err != nil { // 数据解析出错 -// return errors.New(line) -// } -// if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行 -// continue -// } -// if responseBody.Choices[0].Delta.Content == nil && -// responseBody.Choices[0].Delta.ToolCalls == nil && -// responseBody.Choices[0].Delta.ReasoningContent == "" { -// continue -// } + scanner := bufio.NewScanner(response.Body) + for scanner.Scan() { + line := scanner.Text() + if !strings.Contains(line, "data:") || len(line) < 30 { + continue + } + var responseBody = types.ApiResponse{} + err = json.Unmarshal([]byte(line[6:]), &responseBody) + if err != nil { // 数据解析出错 + return errors.New(line) + } + if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行 + continue + } + if responseBody.Choices[0].Delta.Content == nil && + responseBody.Choices[0].Delta.ToolCalls == nil && + responseBody.Choices[0].Delta.ReasoningContent == "" { + continue + } -// if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 { -// messageChan <- map[string]interface{}{ -// "type": "text", -// "body": "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。", -// } -// break -// } + if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 { + pushMessage(c, "text", "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。") + break + } -// var tool types.ToolCall -// if len(responseBody.Choices[0].Delta.ToolCalls) > 0 { -// tool = responseBody.Choices[0].Delta.ToolCalls[0] -// if toolCall && tool.Function.Name == "" { -// arguments = append(arguments, tool.Function.Arguments) -// continue -// } -// } + var tool types.ToolCall + if len(responseBody.Choices[0].Delta.ToolCalls) > 0 { + tool = responseBody.Choices[0].Delta.ToolCalls[0] + if toolCall && tool.Function.Name == "" { + arguments = append(arguments, tool.Function.Arguments) + continue + } + } -// // 兼容 Function Call -// fun := responseBody.Choices[0].Delta.FunctionCall -// if fun.Name != "" { -// tool = *new(types.ToolCall) -// tool.Function.Name = fun.Name -// } else if toolCall { -// arguments = append(arguments, fun.Arguments) -// continue -// } + // 兼容 Function Call + fun := responseBody.Choices[0].Delta.FunctionCall + if fun.Name != "" { + tool = *new(types.ToolCall) + tool.Function.Name = fun.Name + } else if toolCall { + arguments = append(arguments, fun.Arguments) + continue + } -// if !utils.IsEmptyValue(tool) { -// res := h.DB.Where("name = ?", tool.Function.Name).First(&function) -// if res.Error == nil { -// toolCall = true -// callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label) -// messageChan <- map[string]interface{}{ -// "type": "text", -// "body": callMsg, -// } -// contents = append(contents, callMsg) -// } -// continue -// } + if !utils.IsEmptyValue(tool) { + res := h.DB.Where("name = ?", tool.Function.Name).First(&function) + if res.Error == nil { + toolCall = true + callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label) + pushMessage(c, "text", callMsg) + contents = append(contents, callMsg) + } + continue + } -// if responseBody.Choices[0].FinishReason == "tool_calls" || -// responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕 -// break -// } + if responseBody.Choices[0].FinishReason == "tool_calls" || + responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕 + break + } -// // output stopped -// if responseBody.Choices[0].FinishReason != "" { -// break // 输出完成或者输出中断了 -// } else { // 正常输出结果 -// // 兼容思考过程 -// if responseBody.Choices[0].Delta.ReasoningContent != "" { -// reasoningContent := responseBody.Choices[0].Delta.ReasoningContent -// if !reasoning { -// reasoningContent = fmt.Sprintf("%s", reasoningContent) -// reasoning = true -// } + // output stopped + if responseBody.Choices[0].FinishReason != "" { + break // 输出完成或者输出中断了 + } else { // 正常输出结果 + // 兼容思考过程 + if responseBody.Choices[0].Delta.ReasoningContent != "" { + reasoningContent := responseBody.Choices[0].Delta.ReasoningContent + if !reasoning { + reasoningContent = fmt.Sprintf("%s", reasoningContent) + reasoning = true + } -// messageChan <- map[string]interface{}{ -// "type": "text", -// "body": reasoningContent, -// } -// contents = append(contents, reasoningContent) -// } else if responseBody.Choices[0].Delta.Content != "" { -// finalContent := responseBody.Choices[0].Delta.Content -// if reasoning { -// finalContent = fmt.Sprintf("%s", responseBody.Choices[0].Delta.Content) -// reasoning = false -// } -// contents = append(contents, utils.InterfaceToString(finalContent)) -// messageChan <- map[string]interface{}{ -// "type": "text", -// "body": finalContent, -// } -// } -// } -// } // end for + pushMessage(c, "text", reasoningContent) + contents = append(contents, reasoningContent) + } else if responseBody.Choices[0].Delta.Content != "" { + finalContent := responseBody.Choices[0].Delta.Content + if reasoning { + finalContent = fmt.Sprintf("%s", responseBody.Choices[0].Delta.Content) + reasoning = false + } + contents = append(contents, utils.InterfaceToString(finalContent)) + pushMessage(c, "text", finalContent) + } + } + } // end for -// if err := scanner.Err(); err != nil { -// if strings.Contains(err.Error(), "context canceled") { -// logger.Info("用户取消了请求:", prompt) -// } else { -// logger.Error("信息读取出错:", err) -// } -// } + if err := scanner.Err(); err != nil { + if strings.Contains(err.Error(), "context canceled") { + logger.Info("用户取消了请求:", prompt) + } else { + logger.Error("信息读取出错:", err) + } + } -// if toolCall { // 调用函数完成任务 -// params := make(map[string]any) -// _ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms) -// logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params) -// params["user_id"] = userVo.Id -// var apiRes types.BizVo -// r, err := req2.C().R().SetHeader("Body-Type", "application/json"). -// SetHeader("Authorization", function.Token). -// SetBody(params).Post(function.Action) -// errMsg := "" -// if err != nil { -// errMsg = err.Error() -// } else { -// all, _ := io.ReadAll(r.Body) -// err = json.Unmarshal(all, &apiRes) -// if err != nil { -// errMsg = err.Error() -// } else if apiRes.Code != types.Success { -// errMsg = apiRes.Message -// } -// } + if toolCall { // 调用函数完成任务 + params := make(map[string]any) + _ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms) + logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params) + params["user_id"] = userVo.Id + var apiRes types.BizVo + r, err := req2.C().R().SetHeader("Body-Type", "application/json"). + SetHeader("Authorization", function.Token). + SetBody(params).Post(function.Action) + errMsg := "" + if err != nil { + errMsg = err.Error() + } else { + all, _ := io.ReadAll(r.Body) + err = json.Unmarshal(all, &apiRes) + if err != nil { + errMsg = err.Error() + } else if apiRes.Code != types.Success { + errMsg = apiRes.Message + } + } -// if errMsg != "" { -// errMsg = "调用函数工具出错:" + errMsg -// contents = append(contents, errMsg) -// } else { -// errMsg = utils.InterfaceToString(apiRes.Data) -// contents = append(contents, errMsg) -// } -// messageChan <- map[string]interface{}{ -// "type": "text", -// "body": errMsg, -// } -// } + if errMsg != "" { + errMsg = "调用函数工具出错:" + errMsg + contents = append(contents, errMsg) + } else { + errMsg = utils.InterfaceToString(apiRes.Data) + contents = append(contents, errMsg) + } + pushMessage(c, "text", errMsg) + } -// // 消息发送成功 -// if len(contents) > 0 { -// usage := Usage{ -// Prompt: prompt, -// Content: strings.Join(contents, ""), -// PromptTokens: 0, -// CompletionTokens: 0, -// TotalTokens: 0, -// } -// message.Content = usage.Content -// h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt) -// } -// } else { // 非流式输出 -// var respVo OpenAIResVo -// body, err := io.ReadAll(response.Body) -// if err != nil { -// return fmt.Errorf("读取响应失败:%v", body) -// } -// err = json.Unmarshal(body, &respVo) -// if err != nil { -// return fmt.Errorf("解析响应失败:%v", body) -// } -// content := respVo.Choices[0].Message.Content -// if strings.HasPrefix(req.Model, "o1-") { -// content = fmt.Sprintf("AI思考结束,耗时:%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Content) -// } -// messageChan <- map[string]interface{}{ -// "type": "text", -// "body": content, -// } -// respVo.Usage.Prompt = prompt -// respVo.Usage.Content = content -// h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now()) -// } + // 消息发送成功 + if len(contents) > 0 { + usage := Usage{ + Prompt: prompt, + Content: strings.Join(contents, ""), + PromptTokens: 0, + CompletionTokens: 0, + TotalTokens: 0, + } + message.Content = usage.Content + h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt) + } + } else { // 非流式输出 + var respVo OpenAIResVo + body, err := io.ReadAll(response.Body) + if err != nil { + return fmt.Errorf("读取响应失败:%v", body) + } + err = json.Unmarshal(body, &respVo) + if err != nil { + return fmt.Errorf("解析响应失败:%v", body) + } + content := respVo.Choices[0].Message.Content + if strings.HasPrefix(req.Model, "o1-") { + content = fmt.Sprintf("AI思考结束,耗时:%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Content) + } + pushMessage(c, "text", content) + respVo.Usage.Prompt = prompt + respVo.Usage.Content = content + h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now()) + } -// return nil -// } + return nil +} diff --git a/web/src/assets/css/chat-plus.styl b/web/src/assets/css/chat-plus.styl index f1ccb937..3335718d 100644 --- a/web/src/assets/css/chat-plus.styl +++ b/web/src/assets/css/chat-plus.styl @@ -5,7 +5,7 @@ .chat-page { height: 100%; - :deep (.el-message-box__message){ + :deep(.el-message-box__message){ font-size: 18px !important } .newChat{ diff --git a/web/src/components/ChatPrompt.vue b/web/src/components/ChatPrompt.vue index a7aec543..0841f467 100644 --- a/web/src/components/ChatPrompt.vue +++ b/web/src/components/ChatPrompt.vue @@ -137,6 +137,7 @@ const props = defineProps({ tokens: 0, model: '', icon: '', + files: [], }, }, listStyle: { @@ -146,7 +147,7 @@ const props = defineProps({ }) const finalTokens = ref(props.data.tokens) const content = ref(processPrompt(props.data.content)) -const files = ref([]) +const files = ref(props.data.files) // 定义emit事件 const emit = defineEmits(['edit']) @@ -159,38 +160,6 @@ const processFiles = () => { if (!props.data.content) { return } - - // 提取图片|文件链接 - const linkRegex = /(https?:\/\/\S+)/g - const links = props.data.content.match(linkRegex) - const urlPrefix = `${window.location.protocol}//${window.location.host}` - if (links) { - // 把本地链接转换为相对路径 - const _links = links.map((link) => { - if (link.startsWith(urlPrefix)) { - return link.replace(urlPrefix, '') - } - return link - }) - // 合并数组并去重 - const urls = [...new Set([...links, ..._links])] - httpPost('/api/upload/list', { urls: urls }) - .then((res) => { - files.value = res.data.items - - // for (let link of links) { - // if (isExternalImg(link, files.value)) { - // files.value.push({ url: link, ext: ".png" }); - // } - // } - }) - .catch(() => {}) - - // 替换图片|文件链接 - for (let link of links) { - content.value = content.value.replace(link, '') - } - } content.value = md.render(content.value.trim()) } const isExternalImg = (link, files) => { diff --git a/web/src/components/ChatSetting.vue b/web/src/components/ChatSetting.vue index 38dfaaef..0e55a42e 100644 --- a/web/src/components/ChatSetting.vue +++ b/web/src/components/ChatSetting.vue @@ -1,22 +1,36 @@ @@ -73,4 +87,4 @@ const changeTTSModel = (item) => { .chat-setting { } - \ No newline at end of file + diff --git a/web/src/views/ChatPlus.vue b/web/src/views/ChatPlus.vue index c33b66e8..97c19fcf 100644 --- a/web/src/views/ChatPlus.vue +++ b/web/src/views/ChatPlus.vue @@ -709,11 +709,6 @@ onMounted(() => { // 初始化数据 const initData = async () => { try { - // 获取用户信息 - const user = await checkSession() - loginUser.value = user - isLogin.value = true - // 获取角色列表 const roleRes = await httpGet('/api/app/list') roles.value = roleRes.data @@ -728,6 +723,11 @@ const initData = async () => { modelID.value = models.value[0].id } + // 获取用户信息 + const user = await checkSession() + loginUser.value = user + isLogin.value = true + // 获取聊天列表 const chatRes = await httpGet('/api/chat/list') allChats.value = chatRes.data @@ -739,7 +739,7 @@ const initData = async () => { if (error.response?.status === 401) { isLogin.value = false } else { - showMessageError('初始化数据失败:' + error.message) + console.warn('初始化数据失败:' + error.message) } } } @@ -762,20 +762,15 @@ const sendMessage = async function () { return false } - // 如果携带了文件,则串上文件地址 - let content = prompt.value - if (files.value.length > 0) { - content += files.value.map((file) => file.url).join(' ') - } - // 追加消息 chatData.value.push({ type: 'prompt', id: randString(32), icon: loginUser.value.avatar, - content: content, + content: prompt.value, model: getModelValue(modelID.value), created_at: new Date().getTime() / 1000, + files: files.value, }) // 添加空回复消息 @@ -809,9 +804,10 @@ const sendMessage = async function () { role_id: roleId.value, model_id: modelID.value, chat_id: chatId.value, - content: content, + content: prompt.value, tools: toolSelected.value, stream: stream.value, + files: files.value, }), openWhenHidden: true, onopen(response) { @@ -902,7 +898,7 @@ const sendMessage = async function () { ElMessage.error('发送消息失败,请重试') } - tmpChatTitle.value = content + tmpChatTitle.value = prompt.value prompt.value = '' files.value = [] row.value = 1