完善 SSE 功能

This commit is contained in:
RockYang
2025-05-27 08:16:02 +08:00
parent 41e4b1c7ac
commit e685876cc0
7 changed files with 487 additions and 547 deletions

View File

@@ -52,17 +52,6 @@ type Delta struct {
} `json:"function_call,omitempty"` } `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 { type ChatModel struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`

View File

@@ -8,7 +8,6 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"bufio"
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
@@ -33,7 +32,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
req2 "github.com/imroc/req/v3"
"github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -117,17 +115,6 @@ func (h *ChatHandler) Chat(c *gin.Context) {
return 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 // 使用旧的聊天数据覆盖模型和角色ID
var chat model.ChatItem var chat model.ChatItem
h.DB.Where("chat_id", data.ChatId).First(&chat) 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) logger.Debugf("聊天上下文:%+v", chatCtx)
} }
reqMgs := make([]interface{}, 0) reqMgs := make([]any, 0)
for i := len(chatCtx) - 1; i >= 0; i-- { for i := len(chatCtx) - 1; i >= 0; i-- {
reqMgs = append(reqMgs, chatCtx[i]) reqMgs = append(reqMgs, chatCtx[i])
@@ -297,6 +284,8 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
fullPrompt := prompt fullPrompt := prompt
text := prompt text := prompt
for _, file := range session.Files {
// extract files in prompt // extract files in prompt
files := utils.ExtractFileURLs(prompt) files := utils.ExtractFileURLs(prompt)
logger.Debugf("detected FILES: %+v", files) logger.Debugf("detected FILES: %+v", files)
@@ -688,220 +677,220 @@ func (h *ChatHandler) TextToSpeech(c *gin.Context) {
c.Writer.Write(audioBytes) c.Writer.Write(audioBytes)
} }
// OPenAI 消息发送实现 // // OPenAI 消息发送实现
func (h *ChatHandler) sendOpenAiMessage( // func (h *ChatHandler) sendOpenAiMessage(
req types.ApiRequest, // req types.ApiRequest,
userVo vo.User, // userVo vo.User,
ctx context.Context, // ctx context.Context,
session *types.ChatSession, // session *types.ChatSession,
role model.ChatRole, // role model.ChatRole,
prompt string, // prompt string,
c *gin.Context) error { // c *gin.Context) error {
promptCreatedAt := time.Now() // 记录提问时间 // promptCreatedAt := time.Now() // 记录提问时间
start := time.Now() // start := time.Now()
var apiKey = model.ApiKey{} // var apiKey = model.ApiKey{}
response, err := h.doRequest(ctx, req, session, &apiKey) // response, err := h.doRequest(ctx, req, session, &apiKey)
logger.Info("HTTP请求完成耗时", time.Since(start)) // logger.Info("HTTP请求完成耗时", time.Since(start))
if err != nil { // if err != nil {
if strings.Contains(err.Error(), "context canceled") { // if strings.Contains(err.Error(), "context canceled") {
return fmt.Errorf("用户取消了请求:%s", prompt) // return fmt.Errorf("用户取消了请求:%s", prompt)
} else if strings.Contains(err.Error(), "no available key") { // } else if strings.Contains(err.Error(), "no available key") {
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY请联系管理员") // return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY请联系管理员")
} // }
return err // return err
} else { // } else {
defer response.Body.Close() // defer response.Body.Close()
} // }
if response.StatusCode != 200 { // if response.StatusCode != 200 {
body, _ := io.ReadAll(response.Body) // body, _ := io.ReadAll(response.Body)
return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body)) // return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body))
} // }
contentType := response.Header.Get("Content-Type") // contentType := response.Header.Get("Content-Type")
if strings.Contains(contentType, "text/event-stream") { // if strings.Contains(contentType, "text/event-stream") {
replyCreatedAt := time.Now() // 记录回复时间 // replyCreatedAt := time.Now() // 记录回复时间
// 循环读取 Chunk 消息 // // 循环读取 Chunk 消息
var message = types.Message{Role: "assistant"} // var message = types.Message{Role: "assistant"}
var contents = make([]string, 0) // var contents = make([]string, 0)
var function model.Function // var function model.Function
var toolCall = false // var toolCall = false
var arguments = make([]string, 0) // var arguments = make([]string, 0)
var reasoning = false // var reasoning = false
pushMessage(c, ChatEventStart, "开始响应") // pushMessage(c, ChatEventStart, "开始响应")
scanner := bufio.NewScanner(response.Body) // scanner := bufio.NewScanner(response.Body)
for scanner.Scan() { // for scanner.Scan() {
line := scanner.Text() // line := scanner.Text()
if !strings.Contains(line, "data:") || len(line) < 30 { // if !strings.Contains(line, "data:") || len(line) < 30 {
continue // continue
} // }
var responseBody = types.ApiResponse{} // var responseBody = types.ApiResponse{}
err = json.Unmarshal([]byte(line[6:]), &responseBody) // err = json.Unmarshal([]byte(line[6:]), &responseBody)
if err != nil { // 数据解析出错 // if err != nil { // 数据解析出错
return errors.New(line) // return errors.New(line)
} // }
if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行 // if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
continue // continue
} // }
if responseBody.Choices[0].Delta.Content == nil && // if responseBody.Choices[0].Delta.Content == nil &&
responseBody.Choices[0].Delta.ToolCalls == nil && // responseBody.Choices[0].Delta.ToolCalls == nil &&
responseBody.Choices[0].Delta.ReasoningContent == "" { // responseBody.Choices[0].Delta.ReasoningContent == "" {
continue // continue
} // }
if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 { // if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
pushMessage(c, ChatEventError, "抱歉😔😔😔AI助手由于未知原因已经停止输出内容。") // pushMessage(c, ChatEventError, "抱歉😔😔😔AI助手由于未知原因已经停止输出内容。")
break // break
} // }
var tool types.ToolCall // var tool types.ToolCall
if len(responseBody.Choices[0].Delta.ToolCalls) > 0 { // if len(responseBody.Choices[0].Delta.ToolCalls) > 0 {
tool = responseBody.Choices[0].Delta.ToolCalls[0] // tool = responseBody.Choices[0].Delta.ToolCalls[0]
if toolCall && tool.Function.Name == "" { // if toolCall && tool.Function.Name == "" {
arguments = append(arguments, tool.Function.Arguments) // arguments = append(arguments, tool.Function.Arguments)
continue // continue
} // }
} // }
// 兼容 Function Call // // 兼容 Function Call
fun := responseBody.Choices[0].Delta.FunctionCall // fun := responseBody.Choices[0].Delta.FunctionCall
if fun.Name != "" { // if fun.Name != "" {
tool = *new(types.ToolCall) // tool = *new(types.ToolCall)
tool.Function.Name = fun.Name // tool.Function.Name = fun.Name
} else if toolCall { // } else if toolCall {
arguments = append(arguments, fun.Arguments) // arguments = append(arguments, fun.Arguments)
continue // continue
} // }
if !utils.IsEmptyValue(tool) { // if !utils.IsEmptyValue(tool) {
res := h.DB.Where("name = ?", tool.Function.Name).First(&function) // res := h.DB.Where("name = ?", tool.Function.Name).First(&function)
if res.Error == nil { // if res.Error == nil {
toolCall = true // toolCall = true
callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label) // callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ // pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
"type": "text", // "type": "text",
"content": callMsg, // "content": callMsg,
}) // })
contents = append(contents, callMsg) // contents = append(contents, callMsg)
} // }
continue // continue
} // }
if responseBody.Choices[0].FinishReason == "tool_calls" || // if responseBody.Choices[0].FinishReason == "tool_calls" ||
responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕 // responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
break // break
} // }
// output stopped // // output stopped
if responseBody.Choices[0].FinishReason != "" { // if responseBody.Choices[0].FinishReason != "" {
break // 输出完成或者输出中断了 // break // 输出完成或者输出中断了
} else { // 正常输出结果 // } else { // 正常输出结果
// 兼容思考过程 // // 兼容思考过程
if responseBody.Choices[0].Delta.ReasoningContent != "" { // if responseBody.Choices[0].Delta.ReasoningContent != "" {
reasoningContent := responseBody.Choices[0].Delta.ReasoningContent // reasoningContent := responseBody.Choices[0].Delta.ReasoningContent
if !reasoning { // if !reasoning {
reasoningContent = fmt.Sprintf("<think>%s", reasoningContent) // reasoningContent = fmt.Sprintf("<think>%s", reasoningContent)
reasoning = true // reasoning = true
} // }
pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ // pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
"type": "text", // "type": "text",
"content": reasoningContent, // "content": reasoningContent,
}) // })
contents = append(contents, reasoningContent) // contents = append(contents, reasoningContent)
} else if responseBody.Choices[0].Delta.Content != "" { // } else if responseBody.Choices[0].Delta.Content != "" {
finalContent := responseBody.Choices[0].Delta.Content // finalContent := responseBody.Choices[0].Delta.Content
if reasoning { // if reasoning {
finalContent = fmt.Sprintf("</think>%s", responseBody.Choices[0].Delta.Content) // finalContent = fmt.Sprintf("</think>%s", responseBody.Choices[0].Delta.Content)
reasoning = false // reasoning = false
} // }
contents = append(contents, utils.InterfaceToString(finalContent)) // contents = append(contents, utils.InterfaceToString(finalContent))
pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ // pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
"type": "text", // "type": "text",
"content": finalContent, // "content": finalContent,
}) // })
} // }
} // }
} // end for // } // end for
if err := scanner.Err(); err != nil { // if err := scanner.Err(); err != nil {
if strings.Contains(err.Error(), "context canceled") { // if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt) // logger.Info("用户取消了请求:", prompt)
} else { // } else {
logger.Error("信息读取出错:", err) // logger.Error("信息读取出错:", err)
} // }
} // }
if toolCall { // 调用函数完成任务 // if toolCall { // 调用函数完成任务
params := make(map[string]any) // params := make(map[string]any)
_ = utils.JsonDecode(strings.Join(arguments, ""), &params) // _ = utils.JsonDecode(strings.Join(arguments, ""), &params)
logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params) // logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params)
params["user_id"] = userVo.Id // params["user_id"] = userVo.Id
var apiRes types.BizVo // var apiRes types.BizVo
r, err := req2.C().R().SetHeader("Body-Type", "application/json"). // r, err := req2.C().R().SetHeader("Body-Type", "application/json").
SetHeader("Authorization", function.Token). // SetHeader("Authorization", function.Token).
SetBody(params).Post(function.Action) // SetBody(params).Post(function.Action)
errMsg := "" // errMsg := ""
if err != nil { // if err != nil {
errMsg = err.Error() // errMsg = err.Error()
} else { // } else {
all, _ := io.ReadAll(r.Body) // all, _ := io.ReadAll(r.Body)
err = json.Unmarshal(all, &apiRes) // err = json.Unmarshal(all, &apiRes)
if err != nil { // if err != nil {
errMsg = err.Error() // errMsg = err.Error()
} else if apiRes.Code != types.Success { // } else if apiRes.Code != types.Success {
errMsg = apiRes.Message // errMsg = apiRes.Message
} // }
} // }
if errMsg != "" { // if errMsg != "" {
errMsg = "调用函数工具出错:" + errMsg // errMsg = "调用函数工具出错:" + errMsg
contents = append(contents, errMsg) // contents = append(contents, errMsg)
} else { // } else {
errMsg = utils.InterfaceToString(apiRes.Data) // errMsg = utils.InterfaceToString(apiRes.Data)
contents = append(contents, errMsg) // contents = append(contents, errMsg)
} // }
pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ // pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
"type": "text", // "type": "text",
"content": errMsg, // "content": errMsg,
}) // })
} // }
// 消息发送成功 // // 消息发送成功
if len(contents) > 0 { // if len(contents) > 0 {
usage := Usage{ // usage := Usage{
Prompt: prompt, // Prompt: prompt,
Content: strings.Join(contents, ""), // Content: strings.Join(contents, ""),
PromptTokens: 0, // PromptTokens: 0,
CompletionTokens: 0, // CompletionTokens: 0,
TotalTokens: 0, // TotalTokens: 0,
} // }
message.Content = usage.Content // message.Content = usage.Content
h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt) // h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt)
} // }
} else { // } else {
var respVo OpenAIResVo // var respVo OpenAIResVo
body, err := io.ReadAll(response.Body) // body, err := io.ReadAll(response.Body)
if err != nil { // if err != nil {
return fmt.Errorf("读取响应失败:%v", body) // return fmt.Errorf("读取响应失败:%v", body)
} // }
err = json.Unmarshal(body, &respVo) // err = json.Unmarshal(body, &respVo)
if err != nil { // if err != nil {
return fmt.Errorf("解析响应失败:%v", body) // return fmt.Errorf("解析响应失败:%v", body)
} // }
content := respVo.Choices[0].Message.Content // content := respVo.Choices[0].Message.Content
if strings.HasPrefix(req.Model, "o1-") { // if strings.HasPrefix(req.Model, "o1-") {
content = fmt.Sprintf("AI思考结束耗时%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Content) // content = fmt.Sprintf("AI思考结束耗时%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Content)
} // }
pushMessage(c, ChatEventMessageDelta, map[string]interface{}{ // pushMessage(c, ChatEventMessageDelta, map[string]interface{}{
"type": "text", // "type": "text",
"content": content, // "content": content,
}) // })
respVo.Usage.Prompt = prompt // respVo.Usage.Prompt = prompt
respVo.Usage.Content = content // respVo.Usage.Content = content
h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now()) // h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now())
} // }
return nil // return nil
} // }

View File

@@ -1,271 +1,254 @@
package handler package handler
// // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// // * Copyright 2023 The Geek-AI Authors. All rights reserved. // * Copyright 2023 The Geek-AI Authors. All rights reserved.
// // * Use of this source code is governed by a Apache-2.0 license // * Use of this source code is governed by a Apache-2.0 license
// // * that can be found in the LICENSE file. // * that can be found in the LICENSE file.
// // * @Author yangjian102621@163.com // * @Author yangjian102621@163.com
// // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// import ( import (
// "bufio" "bufio"
// "context" "context"
// "encoding/json" "encoding/json"
// "errors" "errors"
// "fmt" "fmt"
// "geekai/core/types" "geekai/core/types"
// "geekai/store/model" "geekai/store/model"
// "geekai/store/vo" "geekai/store/vo"
// "geekai/utils" "geekai/utils"
// "io" "io"
// "strings" "strings"
// "time" "time"
// req2 "github.com/imroc/req/v3" "github.com/gin-gonic/gin"
// ) req2 "github.com/imroc/req/v3"
)
// type Usage struct { type Usage struct {
// Prompt string `json:"prompt,omitempty"` Prompt string `json:"prompt,omitempty"`
// Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
// PromptTokens int `json:"prompt_tokens"` PromptTokens int `json:"prompt_tokens"`
// CompletionTokens int `json:"completion_tokens"` CompletionTokens int `json:"completion_tokens"`
// TotalTokens int `json:"total_tokens"` TotalTokens int `json:"total_tokens"`
// } }
// type OpenAIResVo struct { type OpenAIResVo struct {
// Id string `json:"id"` Id string `json:"id"`
// Object string `json:"object"` Object string `json:"object"`
// Created int `json:"created"` Created int `json:"created"`
// Model string `json:"model"` Model string `json:"model"`
// SystemFingerprint string `json:"system_fingerprint"` SystemFingerprint string `json:"system_fingerprint"`
// Choices []struct { Choices []struct {
// Index int `json:"index"` Index int `json:"index"`
// Message struct { Message struct {
// Role string `json:"role"` Role string `json:"role"`
// Content string `json:"content"` Content string `json:"content"`
// } `json:"message"` } `json:"message"`
// Logprobs interface{} `json:"logprobs"` Logprobs interface{} `json:"logprobs"`
// FinishReason string `json:"finish_reason"` FinishReason string `json:"finish_reason"`
// } `json:"choices"` } `json:"choices"`
// Usage Usage `json:"usage"` Usage Usage `json:"usage"`
// } }
// // OPenAI 消息发送实现 // OPenAI 消息发送实现
// func (h *ChatHandler) sendOpenAiMessage( func (h *ChatHandler) sendOpenAiMessage(
// req types.ApiRequest, req types.ApiRequest,
// userVo vo.User, userVo vo.User,
// ctx context.Context, ctx context.Context,
// session *types.ChatSession, session *types.ChatSession,
// role model.ChatRole, role model.ChatRole,
// prompt string, prompt string,
// messageChan chan interface{}) error { c *gin.Context) error {
// promptCreatedAt := time.Now() // 记录提问时间 promptCreatedAt := time.Now() // 记录提问时间
// start := time.Now() start := time.Now()
// var apiKey = model.ApiKey{} var apiKey = model.ApiKey{}
// response, err := h.doRequest(ctx, req, session, &apiKey) response, err := h.doRequest(ctx, req, session, &apiKey)
// logger.Info("HTTP请求完成耗时", time.Since(start)) logger.Info("HTTP请求完成耗时", time.Since(start))
// if err != nil { if err != nil {
// if strings.Contains(err.Error(), "context canceled") { if strings.Contains(err.Error(), "context canceled") {
// return fmt.Errorf("用户取消了请求:%s", prompt) return fmt.Errorf("用户取消了请求:%s", prompt)
// } else if strings.Contains(err.Error(), "no available key") { } else if strings.Contains(err.Error(), "no available key") {
// return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY请联系管理员") return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY请联系管理员")
// } }
// return err return err
// } else { } else {
// defer response.Body.Close() defer response.Body.Close()
// } }
// if response.StatusCode != 200 { if response.StatusCode != 200 {
// body, _ := io.ReadAll(response.Body) body, _ := io.ReadAll(response.Body)
// return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body)) return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body))
// } }
// contentType := response.Header.Get("Content-Type") contentType := response.Header.Get("Content-Type")
// if strings.Contains(contentType, "text/event-stream") { if strings.Contains(contentType, "text/event-stream") {
// replyCreatedAt := time.Now() // 记录回复时间 replyCreatedAt := time.Now() // 记录回复时间
// // 循环读取 Chunk 消息 // 循环读取 Chunk 消息
// var message = types.Message{Role: "assistant"} var message = types.Message{Role: "assistant"}
// var contents = make([]string, 0) var contents = make([]string, 0)
// var function model.Function var function model.Function
// var toolCall = false var toolCall = false
// var arguments = make([]string, 0) var arguments = make([]string, 0)
// var reasoning = false var reasoning = false
// scanner := bufio.NewScanner(response.Body) scanner := bufio.NewScanner(response.Body)
// for scanner.Scan() { for scanner.Scan() {
// line := scanner.Text() line := scanner.Text()
// if !strings.Contains(line, "data:") || len(line) < 30 { if !strings.Contains(line, "data:") || len(line) < 30 {
// continue continue
// } }
// var responseBody = types.ApiResponse{} var responseBody = types.ApiResponse{}
// err = json.Unmarshal([]byte(line[6:]), &responseBody) err = json.Unmarshal([]byte(line[6:]), &responseBody)
// if err != nil { // 数据解析出错 if err != nil { // 数据解析出错
// return errors.New(line) return errors.New(line)
// } }
// if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行 if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
// continue continue
// } }
// if responseBody.Choices[0].Delta.Content == nil && if responseBody.Choices[0].Delta.Content == nil &&
// responseBody.Choices[0].Delta.ToolCalls == nil && responseBody.Choices[0].Delta.ToolCalls == nil &&
// responseBody.Choices[0].Delta.ReasoningContent == "" { responseBody.Choices[0].Delta.ReasoningContent == "" {
// continue continue
// } }
// if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 { if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
// messageChan <- map[string]interface{}{ pushMessage(c, "text", "抱歉😔😔😔AI助手由于未知原因已经停止输出内容。")
// "type": "text", break
// "body": "抱歉😔😔😔AI助手由于未知原因已经停止输出内容。", }
// }
// break
// }
// var tool types.ToolCall var tool types.ToolCall
// if len(responseBody.Choices[0].Delta.ToolCalls) > 0 { if len(responseBody.Choices[0].Delta.ToolCalls) > 0 {
// tool = responseBody.Choices[0].Delta.ToolCalls[0] tool = responseBody.Choices[0].Delta.ToolCalls[0]
// if toolCall && tool.Function.Name == "" { if toolCall && tool.Function.Name == "" {
// arguments = append(arguments, tool.Function.Arguments) arguments = append(arguments, tool.Function.Arguments)
// continue continue
// } }
// } }
// // 兼容 Function Call // 兼容 Function Call
// fun := responseBody.Choices[0].Delta.FunctionCall fun := responseBody.Choices[0].Delta.FunctionCall
// if fun.Name != "" { if fun.Name != "" {
// tool = *new(types.ToolCall) tool = *new(types.ToolCall)
// tool.Function.Name = fun.Name tool.Function.Name = fun.Name
// } else if toolCall { } else if toolCall {
// arguments = append(arguments, fun.Arguments) arguments = append(arguments, fun.Arguments)
// continue continue
// } }
// if !utils.IsEmptyValue(tool) { if !utils.IsEmptyValue(tool) {
// res := h.DB.Where("name = ?", tool.Function.Name).First(&function) res := h.DB.Where("name = ?", tool.Function.Name).First(&function)
// if res.Error == nil { if res.Error == nil {
// toolCall = true toolCall = true
// callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label) callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
// messageChan <- map[string]interface{}{ pushMessage(c, "text", callMsg)
// "type": "text", contents = append(contents, callMsg)
// "body": callMsg, }
// } continue
// contents = append(contents, callMsg) }
// }
// continue
// }
// if responseBody.Choices[0].FinishReason == "tool_calls" || if responseBody.Choices[0].FinishReason == "tool_calls" ||
// responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕 responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
// break break
// } }
// // output stopped // output stopped
// if responseBody.Choices[0].FinishReason != "" { if responseBody.Choices[0].FinishReason != "" {
// break // 输出完成或者输出中断了 break // 输出完成或者输出中断了
// } else { // 正常输出结果 } else { // 正常输出结果
// // 兼容思考过程 // 兼容思考过程
// if responseBody.Choices[0].Delta.ReasoningContent != "" { if responseBody.Choices[0].Delta.ReasoningContent != "" {
// reasoningContent := responseBody.Choices[0].Delta.ReasoningContent reasoningContent := responseBody.Choices[0].Delta.ReasoningContent
// if !reasoning { if !reasoning {
// reasoningContent = fmt.Sprintf("<think>%s", reasoningContent) reasoningContent = fmt.Sprintf("<think>%s", reasoningContent)
// reasoning = true reasoning = true
// } }
// messageChan <- map[string]interface{}{ pushMessage(c, "text", reasoningContent)
// "type": "text", contents = append(contents, reasoningContent)
// "body": reasoningContent, } else if responseBody.Choices[0].Delta.Content != "" {
// } finalContent := responseBody.Choices[0].Delta.Content
// contents = append(contents, reasoningContent) if reasoning {
// } else if responseBody.Choices[0].Delta.Content != "" { finalContent = fmt.Sprintf("</think>%s", responseBody.Choices[0].Delta.Content)
// finalContent := responseBody.Choices[0].Delta.Content reasoning = false
// if reasoning { }
// finalContent = fmt.Sprintf("</think>%s", responseBody.Choices[0].Delta.Content) contents = append(contents, utils.InterfaceToString(finalContent))
// reasoning = false pushMessage(c, "text", finalContent)
// } }
// contents = append(contents, utils.InterfaceToString(finalContent)) }
// messageChan <- map[string]interface{}{ } // end for
// "type": "text",
// "body": finalContent,
// }
// }
// }
// } // end for
// if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
// if strings.Contains(err.Error(), "context canceled") { if strings.Contains(err.Error(), "context canceled") {
// logger.Info("用户取消了请求:", prompt) logger.Info("用户取消了请求:", prompt)
// } else { } else {
// logger.Error("信息读取出错:", err) logger.Error("信息读取出错:", err)
// } }
// } }
// if toolCall { // 调用函数完成任务 if toolCall { // 调用函数完成任务
// params := make(map[string]any) params := make(map[string]any)
// _ = utils.JsonDecode(strings.Join(arguments, ""), &params) _ = utils.JsonDecode(strings.Join(arguments, ""), &params)
// logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params) logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params)
// params["user_id"] = userVo.Id params["user_id"] = userVo.Id
// var apiRes types.BizVo var apiRes types.BizVo
// r, err := req2.C().R().SetHeader("Body-Type", "application/json"). r, err := req2.C().R().SetHeader("Body-Type", "application/json").
// SetHeader("Authorization", function.Token). SetHeader("Authorization", function.Token).
// SetBody(params).Post(function.Action) SetBody(params).Post(function.Action)
// errMsg := "" errMsg := ""
// if err != nil { if err != nil {
// errMsg = err.Error() errMsg = err.Error()
// } else { } else {
// all, _ := io.ReadAll(r.Body) all, _ := io.ReadAll(r.Body)
// err = json.Unmarshal(all, &apiRes) err = json.Unmarshal(all, &apiRes)
// if err != nil { if err != nil {
// errMsg = err.Error() errMsg = err.Error()
// } else if apiRes.Code != types.Success { } else if apiRes.Code != types.Success {
// errMsg = apiRes.Message errMsg = apiRes.Message
// } }
// } }
// if errMsg != "" { if errMsg != "" {
// errMsg = "调用函数工具出错:" + errMsg errMsg = "调用函数工具出错:" + errMsg
// contents = append(contents, errMsg) contents = append(contents, errMsg)
// } else { } else {
// errMsg = utils.InterfaceToString(apiRes.Data) errMsg = utils.InterfaceToString(apiRes.Data)
// contents = append(contents, errMsg) contents = append(contents, errMsg)
// } }
// messageChan <- map[string]interface{}{ pushMessage(c, "text", errMsg)
// "type": "text", }
// "body": errMsg,
// }
// }
// // 消息发送成功 // 消息发送成功
// if len(contents) > 0 { if len(contents) > 0 {
// usage := Usage{ usage := Usage{
// Prompt: prompt, Prompt: prompt,
// Content: strings.Join(contents, ""), Content: strings.Join(contents, ""),
// PromptTokens: 0, PromptTokens: 0,
// CompletionTokens: 0, CompletionTokens: 0,
// TotalTokens: 0, TotalTokens: 0,
// } }
// message.Content = usage.Content message.Content = usage.Content
// h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt) h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt)
// } }
// } else { // 非流式输出 } else { // 非流式输出
// var respVo OpenAIResVo var respVo OpenAIResVo
// body, err := io.ReadAll(response.Body) body, err := io.ReadAll(response.Body)
// if err != nil { if err != nil {
// return fmt.Errorf("读取响应失败:%v", body) return fmt.Errorf("读取响应失败:%v", body)
// } }
// err = json.Unmarshal(body, &respVo) err = json.Unmarshal(body, &respVo)
// if err != nil { if err != nil {
// return fmt.Errorf("解析响应失败:%v", body) return fmt.Errorf("解析响应失败:%v", body)
// } }
// content := respVo.Choices[0].Message.Content content := respVo.Choices[0].Message.Content
// if strings.HasPrefix(req.Model, "o1-") { if strings.HasPrefix(req.Model, "o1-") {
// content = fmt.Sprintf("AI思考结束耗时%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Content) content = fmt.Sprintf("AI思考结束耗时%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Content)
// } }
// messageChan <- map[string]interface{}{ pushMessage(c, "text", content)
// "type": "text", respVo.Usage.Prompt = prompt
// "body": content, respVo.Usage.Content = content
// } h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now())
// 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
// } }

View File

@@ -5,7 +5,7 @@
.chat-page { .chat-page {
height: 100%; height: 100%;
:deep (.el-message-box__message){ :deep(.el-message-box__message){
font-size: 18px !important font-size: 18px !important
} }
.newChat{ .newChat{

View File

@@ -137,6 +137,7 @@ const props = defineProps({
tokens: 0, tokens: 0,
model: '', model: '',
icon: '', icon: '',
files: [],
}, },
}, },
listStyle: { listStyle: {
@@ -146,7 +147,7 @@ const props = defineProps({
}) })
const finalTokens = ref(props.data.tokens) const finalTokens = ref(props.data.tokens)
const content = ref(processPrompt(props.data.content)) const content = ref(processPrompt(props.data.content))
const files = ref([]) const files = ref(props.data.files)
// 定义emit事件 // 定义emit事件
const emit = defineEmits(['edit']) const emit = defineEmits(['edit'])
@@ -159,38 +160,6 @@ const processFiles = () => {
if (!props.data.content) { if (!props.data.content) {
return 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()) content.value = md.render(content.value.trim())
} }
const isExternalImg = (link, files) => { const isExternalImg = (link, files) => {

View File

@@ -1,22 +1,36 @@
<template> <template>
<el-dialog <el-dialog
class="config-dialog" class="config-dialog"
v-model="showDialog" v-model="showDialog"
:close-on-click-modal="true" :close-on-click-modal="true"
:before-close="close" :before-close="close"
style="max-width: 600px" style="max-width: 600px"
title="聊天配置" title="聊天配置"
> >
<div class="chat-setting"> <div class="chat-setting">
<el-form :model="data" label-width="100px" label-position="left"> <el-form :model="data" label-width="100px" label-position="left">
<el-form-item label="聊天样式:"> <el-form-item label="聊天样式:">
<el-radio-group v-model="data.style" @change="(val) => {store.setChatListStyle(val)}"> <el-radio-group
v-model="data.style"
@change="
(val) => {
store.setChatListStyle(val)
}
"
>
<el-radio value="list">列表样式</el-radio> <el-radio value="list">列表样式</el-radio>
<el-radio value="chat">对话样式</el-radio> <el-radio value="chat">对话样式</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="流式输出:"> <el-form-item label="流式输出:">
<el-switch v-model="data.stream" @change="(val) => {store.setChatStream(val)}" /> <el-switch
v-model="data.stream"
@change="
(val) => {
store.setChatStream(val)
}
"
/>
</el-form-item> </el-form-item>
<el-form-item label="语音音色:"> <el-form-item label="语音音色:">
<el-select v-model="data.ttsModel" placeholder="请选择语音音色" @change="changeTTSModel"> <el-select v-model="data.ttsModel" placeholder="请选择语音音色" @change="changeTTSModel">
@@ -31,10 +45,10 @@
</template> </template>
<script setup> <script setup>
import {computed, ref, onMounted} from "vue" import { computed, ref, onMounted } from 'vue'
import {useSharedStore} from "@/store/sharedata"; import { useSharedStore } from '@/store/sharedata'
import {httpGet} from "@/utils/http"; import { httpGet } from '@/utils/http'
const store = useSharedStore(); const store = useSharedStore()
const data = ref({ const data = ref({
style: store.chatListStyle, style: store.chatListStyle,
@@ -44,28 +58,28 @@ const data = ref({
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const props = defineProps({ const props = defineProps({
show: Boolean, show: Boolean,
}); })
const showDialog = computed(() => { const showDialog = computed(() => {
return props.show return props.show
}) })
const emits = defineEmits(['hide']); const emits = defineEmits(['hide'])
const close = function () { const close = function () {
emits('hide', false); emits('hide', false)
} }
const models = ref([]); const models = ref([])
onMounted(() => { onMounted(() => {
// 获取模型列表 // 获取模型列表
httpGet("/api/model/list?type=tts").then((res) => { httpGet('/api/model/list?type=tts').then((res) => {
models.value = res.data; models.value = res.data
if (!data.ttsModel) { if (!data.ttsModel && models.value.length > 0) {
store.setTtsModel(models.value[0].id); store.setTtsModel(models.value[0].id)
} }
}) })
}) })
const changeTTSModel = (item) => { const changeTTSModel = (item) => {
store.setTtsModel(item); store.setTtsModel(item)
} }
</script> </script>

View File

@@ -709,11 +709,6 @@ onMounted(() => {
// 初始化数据 // 初始化数据
const initData = async () => { const initData = async () => {
try { try {
// 获取用户信息
const user = await checkSession()
loginUser.value = user
isLogin.value = true
// 获取角色列表 // 获取角色列表
const roleRes = await httpGet('/api/app/list') const roleRes = await httpGet('/api/app/list')
roles.value = roleRes.data roles.value = roleRes.data
@@ -728,6 +723,11 @@ const initData = async () => {
modelID.value = models.value[0].id modelID.value = models.value[0].id
} }
// 获取用户信息
const user = await checkSession()
loginUser.value = user
isLogin.value = true
// 获取聊天列表 // 获取聊天列表
const chatRes = await httpGet('/api/chat/list') const chatRes = await httpGet('/api/chat/list')
allChats.value = chatRes.data allChats.value = chatRes.data
@@ -739,7 +739,7 @@ const initData = async () => {
if (error.response?.status === 401) { if (error.response?.status === 401) {
isLogin.value = false isLogin.value = false
} else { } else {
showMessageError('初始化数据失败:' + error.message) console.warn('初始化数据失败:' + error.message)
} }
} }
} }
@@ -762,20 +762,15 @@ const sendMessage = async function () {
return false return false
} }
// 如果携带了文件,则串上文件地址
let content = prompt.value
if (files.value.length > 0) {
content += files.value.map((file) => file.url).join(' ')
}
// 追加消息 // 追加消息
chatData.value.push({ chatData.value.push({
type: 'prompt', type: 'prompt',
id: randString(32), id: randString(32),
icon: loginUser.value.avatar, icon: loginUser.value.avatar,
content: content, content: prompt.value,
model: getModelValue(modelID.value), model: getModelValue(modelID.value),
created_at: new Date().getTime() / 1000, created_at: new Date().getTime() / 1000,
files: files.value,
}) })
// 添加空回复消息 // 添加空回复消息
@@ -809,9 +804,10 @@ const sendMessage = async function () {
role_id: roleId.value, role_id: roleId.value,
model_id: modelID.value, model_id: modelID.value,
chat_id: chatId.value, chat_id: chatId.value,
content: content, content: prompt.value,
tools: toolSelected.value, tools: toolSelected.value,
stream: stream.value, stream: stream.value,
files: files.value,
}), }),
openWhenHidden: true, openWhenHidden: true,
onopen(response) { onopen(response) {
@@ -902,7 +898,7 @@ const sendMessage = async function () {
ElMessage.error('发送消息失败,请重试') ElMessage.error('发送消息失败,请重试')
} }
tmpChatTitle.value = content tmpChatTitle.value = prompt.value
prompt.value = '' prompt.value = ''
files.value = [] files.value = []
row.value = 1 row.value = 1