固化聊天
This commit is contained in:
@@ -54,8 +54,11 @@ var allowedImageTypes = map[string]bool{
|
||||
|
||||
// chatRequestFromFrontend is the expected POST body
|
||||
type chatRequest struct {
|
||||
Messages []chatMessage `json:"messages"`
|
||||
OpenAIName string `json:"openaiName,omitempty"`
|
||||
Messages []chatMessage `json:"messages"`
|
||||
OpenAIName string `json:"openaiName,omitempty"`
|
||||
ConversationID uint `json:"conversationId,omitempty"`
|
||||
ClientLocalID string `json:"clientLocalId,omitempty"`
|
||||
SaveToServer bool `json:"saveToServer,omitempty"`
|
||||
}
|
||||
|
||||
type chatMessage struct {
|
||||
@@ -156,6 +159,12 @@ func ApiAIChat(r *gin.RouterGroup) {
|
||||
r.GET("/openai", handleOpenAIProfiles)
|
||||
r.POST("/chat", handleChat)
|
||||
|
||||
conversations := r.Group("/conversations")
|
||||
conversations.POST("/list", handleAIChatConversationList)
|
||||
conversations.POST("/get", handleAIChatConversationGet)
|
||||
conversations.POST("/update", handleAIChatConversationUpdate)
|
||||
conversations.POST("/delete", handleAIChatConversationDelete)
|
||||
|
||||
admin := r.Group("/admin")
|
||||
admin.POST("/config", handleAIChatAdminGetConfig)
|
||||
admin.POST("/config/update", handleAIChatAdminUpdateConfig)
|
||||
@@ -193,7 +202,7 @@ func handleOpenAIProfiles(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
func handleChat(ctx *gin.Context) {
|
||||
data, _ := SeparateData(ctx)
|
||||
data, cookieValue := SeparateData(ctx)
|
||||
|
||||
if data == nil {
|
||||
sendSSEError(ctx, "请求数据为空")
|
||||
@@ -211,6 +220,13 @@ func handleChat(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var currentUser *TabUser
|
||||
if cookieValue != "" {
|
||||
if user, err := AuthenticationAuthorityFromCookie(cookieValue); err == nil {
|
||||
currentUser = user
|
||||
}
|
||||
}
|
||||
|
||||
// Check ai config
|
||||
cfg := getAIChatConfig()
|
||||
profile, ok := selectOpenAIProfile(cfg, req.OpenAIName)
|
||||
@@ -230,13 +246,28 @@ func handleChat(ctx *gin.Context) {
|
||||
flusher, _ := ctx.Writer.(http.Flusher)
|
||||
tracker := newTokenUsageTracker()
|
||||
|
||||
traceEvents := []sseEvent{}
|
||||
emitTrace := func(tool, stage, status, message string, data map[string]interface{}) {
|
||||
sendSSE(ctx, flusher, sseEvent{Type: "trace", Tool: tool, Stage: stage, Status: status, Message: message, Data: data})
|
||||
event := sseEvent{Type: "trace", Tool: tool, Stage: stage, Status: status, Message: message, Data: data}
|
||||
traceEvents = append(traceEvents, event)
|
||||
sendSSE(ctx, flusher, event)
|
||||
}
|
||||
emitStats := func(stats tokenUsageStats) {
|
||||
sendSSE(ctx, flusher, sseEvent{Type: "stats", Stats: &stats})
|
||||
}
|
||||
|
||||
conversation, persistErr := prepareAIChatPersistence(currentUser, req, profile.Name)
|
||||
if persistErr != nil {
|
||||
emitTrace("chat", "persist", "error", "聊天保存失败,将继续仅本次对话", map[string]interface{}{"error": persistErr.Error()})
|
||||
conversation = nil
|
||||
} else if conversation != nil {
|
||||
sendSSE(ctx, flusher, sseEvent{Type: "conversation", Data: map[string]interface{}{
|
||||
"id": conversation.ID,
|
||||
"title": conversation.Title,
|
||||
"clientLocalId": conversation.ClientLocalID,
|
||||
}})
|
||||
}
|
||||
|
||||
toolConfigs := []agents.ToolConfig{}
|
||||
if cfg.ToolRouter.Enabled {
|
||||
toolConfigs = buildToolConfigs(cfg.ToolRouter.Tools)
|
||||
@@ -289,6 +320,9 @@ func handleChat(ctx *gin.Context) {
|
||||
modelPromptTokens := estimateOpenAIMessagesTokens(apiReq.Messages)
|
||||
completionTokens := 0
|
||||
modelUsageReceived := false
|
||||
assistantContent := strings.Builder{}
|
||||
reasoningContent := strings.Builder{}
|
||||
var finalStats *tokenUsageStats
|
||||
streamStarted := time.Now()
|
||||
windowStarted := streamStarted
|
||||
windowTokens := 0
|
||||
@@ -305,10 +339,12 @@ func handleChat(ctx *gin.Context) {
|
||||
reasoningText = choice.Delta.Thinking
|
||||
}
|
||||
if reasoningText != "" {
|
||||
reasoningContent.WriteString(reasoningText)
|
||||
sendSSE(ctx, flusher, sseEvent{Type: "reasoning", Text: reasoningText})
|
||||
}
|
||||
|
||||
if choice.Delta.Content != "" {
|
||||
assistantContent.WriteString(choice.Delta.Content)
|
||||
deltaTokens := estimateTokenCount(choice.Delta.Content)
|
||||
completionTokens += deltaTokens
|
||||
windowTokens += deltaTokens
|
||||
@@ -321,17 +357,23 @@ func handleChat(ctx *gin.Context) {
|
||||
peakTokensPerSecond = maxFloat(peakTokensPerSecond, float64(windowTokens)/elapsedWindow)
|
||||
}
|
||||
stats := tracker.setModelEstimate(modelPromptTokens, completionTokens).snapshot(tokensPerSecond(completionTokens, streamStarted), peakTokensPerSecond)
|
||||
finalStats = &stats
|
||||
sendSSE(ctx, flusher, sseEvent{Type: "delta", Text: choice.Delta.Content, Stats: &stats})
|
||||
}
|
||||
}
|
||||
if chunk.Usage != nil {
|
||||
modelUsageReceived = true
|
||||
stats := tracker.setModelUsage(chunk.Usage).snapshot(tokensPerSecond(tracker.completionTokens, streamStarted), peakTokensPerSecond)
|
||||
finalStats = &stats
|
||||
emitStats(stats)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
sendSSE(ctx, flusher, sseEvent{Type: "error", Error: "请求失败: " + err.Error()})
|
||||
errorText := "请求失败: " + err.Error()
|
||||
if conversation != nil && assistantContent.Len() > 0 {
|
||||
_ = persistAIChatAssistantMessage(conversation, assistantContent.String(), reasoningContent.String(), traceEvents, finalStats)
|
||||
}
|
||||
sendSSE(ctx, flusher, sseEvent{Type: "error", Error: errorText})
|
||||
sendSSEDone(ctx, flusher)
|
||||
return
|
||||
}
|
||||
@@ -344,9 +386,16 @@ func handleChat(ctx *gin.Context) {
|
||||
}
|
||||
emitTrace("model", "stream", "success", "模型回复完成", nil)
|
||||
if modelUsageReceived {
|
||||
emitStats(tracker.snapshot(tokensPerSecond(tracker.completionTokens, streamStarted), peakTokensPerSecond))
|
||||
stats := tracker.snapshot(tokensPerSecond(tracker.completionTokens, streamStarted), peakTokensPerSecond)
|
||||
finalStats = &stats
|
||||
emitStats(stats)
|
||||
} else {
|
||||
emitStats(tracker.setModelEstimate(modelPromptTokens, completionTokens).snapshot(tokensPerSecond(completionTokens, streamStarted), peakTokensPerSecond))
|
||||
stats := tracker.setModelEstimate(modelPromptTokens, completionTokens).snapshot(tokensPerSecond(completionTokens, streamStarted), peakTokensPerSecond)
|
||||
finalStats = &stats
|
||||
emitStats(stats)
|
||||
}
|
||||
if err := persistAIChatAssistantMessage(conversation, assistantContent.String(), reasoningContent.String(), traceEvents, finalStats); err != nil {
|
||||
sendSSE(ctx, flusher, sseEvent{Type: "trace", Tool: "chat", Stage: "persist", Status: "error", Message: "助手回复保存失败", Data: map[string]interface{}{"error": err.Error()}})
|
||||
}
|
||||
sendSSEDone(ctx, flusher)
|
||||
flusher.Flush()
|
||||
|
||||
@@ -52,6 +52,33 @@ type TabAIChatTool struct {
|
||||
UpdatedAt *time.Time `gorm:"type:datetime;autoUpdateTime"`
|
||||
}
|
||||
|
||||
type TabAIChatConversation struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement"`
|
||||
UserID uint `gorm:"not null;index:idx_ai_chat_conversation_user_updated,priority:1"`
|
||||
Title string `gorm:"size:200"`
|
||||
OpenAIName string `gorm:"size:100;index"`
|
||||
ClientLocalID string `gorm:"size:100;index"`
|
||||
MessageCount int `gorm:"default:0"`
|
||||
LastMessageAt *time.Time `gorm:"type:datetime;index"`
|
||||
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"`
|
||||
UpdatedAt *time.Time `gorm:"type:datetime;autoUpdateTime;index:idx_ai_chat_conversation_user_updated,priority:2"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
}
|
||||
|
||||
type TabAIChatMessage struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement"`
|
||||
ConversationID uint `gorm:"not null;index:idx_ai_chat_message_conversation_seq,priority:1"`
|
||||
UserID uint `gorm:"not null;index"`
|
||||
Role string `gorm:"size:20;index"`
|
||||
Content string `gorm:"type:text"`
|
||||
ImageURL string `gorm:"type:text"`
|
||||
Reasoning string `gorm:"type:text"`
|
||||
TracesJSON string `gorm:"type:text"`
|
||||
StatsJSON string `gorm:"type:text"`
|
||||
Seq int `gorm:"index:idx_ai_chat_message_conversation_seq,priority:2"`
|
||||
CreatedAt *time.Time `gorm:"type:datetime;autoCreateTime"`
|
||||
}
|
||||
|
||||
var aiChatConfigMu sync.RWMutex
|
||||
|
||||
func ApiAIChatInit() {
|
||||
@@ -60,6 +87,8 @@ func ApiAIChatInit() {
|
||||
&TabAIChatOpenAIProfile{},
|
||||
&TabAIChatToolRouter{},
|
||||
&TabAIChatTool{},
|
||||
&TabAIChatConversation{},
|
||||
&TabAIChatMessage{},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"ops/models"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type aiChatConversationListReq struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
type aiChatConversationIDReq struct {
|
||||
ID uint `json:"id"`
|
||||
}
|
||||
|
||||
type aiChatConversationUpdateReq struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
func handleAIChatConversationList(ctx *gin.Context) {
|
||||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||
if !isAuth {
|
||||
ReturnJson(ctx, "userCookieError", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req aiChatConversationListReq
|
||||
if data != nil {
|
||||
_ = decodeJSON(data, &req)
|
||||
}
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 || req.PageSize > 100 {
|
||||
req.PageSize = 50
|
||||
}
|
||||
|
||||
query := models.DB.Model(&TabAIChatConversation{}).Where("user_id = ?", user.ID)
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
ReturnJson(ctx, "apiErr", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var conversations []TabAIChatConversation
|
||||
if err := query.Order("last_message_at desc, updated_at desc, id desc").
|
||||
Offset((req.Page - 1) * req.PageSize).
|
||||
Limit(req.PageSize).
|
||||
Find(&conversations).Error; err != nil {
|
||||
ReturnJson(ctx, "apiErr", nil)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]map[string]interface{}, 0, len(conversations))
|
||||
for _, conversation := range conversations {
|
||||
items = append(items, aiChatConversationResponse(conversation))
|
||||
}
|
||||
ReturnJson(ctx, "apiOK", gin.H{
|
||||
"conversations": items,
|
||||
"total": total,
|
||||
"page": req.Page,
|
||||
"page_size": req.PageSize,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAIChatConversationGet(ctx *gin.Context) {
|
||||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||
if !isAuth {
|
||||
ReturnJson(ctx, "userCookieError", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req aiChatConversationIDReq
|
||||
if err := decodeJSON(data, &req); err != nil || req.ID == 0 {
|
||||
ReturnJson(ctx, "jsonErr", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var conversation TabAIChatConversation
|
||||
if err := models.DB.Where("id = ? AND user_id = ?", req.ID, user.ID).First(&conversation).Error; err != nil {
|
||||
ReturnJson(ctx, "apiErr", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var messages []TabAIChatMessage
|
||||
if err := models.DB.Where("conversation_id = ? AND user_id = ?", conversation.ID, user.ID).
|
||||
Order("seq asc, id asc").
|
||||
Find(&messages).Error; err != nil {
|
||||
ReturnJson(ctx, "apiErr", nil)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]map[string]interface{}, 0, len(messages))
|
||||
for _, message := range messages {
|
||||
items = append(items, aiChatMessageResponse(message))
|
||||
}
|
||||
ReturnJson(ctx, "apiOK", gin.H{
|
||||
"conversation": aiChatConversationResponse(conversation),
|
||||
"messages": items,
|
||||
})
|
||||
}
|
||||
|
||||
func handleAIChatConversationUpdate(ctx *gin.Context) {
|
||||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||
if !isAuth {
|
||||
ReturnJson(ctx, "userCookieError", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req aiChatConversationUpdateReq
|
||||
if err := decodeJSON(data, &req); err != nil || req.ID == 0 {
|
||||
ReturnJson(ctx, "jsonErr", nil)
|
||||
return
|
||||
}
|
||||
title := strings.TrimSpace(req.Title)
|
||||
if title == "" {
|
||||
title = "新对话"
|
||||
}
|
||||
if len([]rune(title)) > 200 {
|
||||
title = string([]rune(title)[:200])
|
||||
}
|
||||
|
||||
result := models.DB.Model(&TabAIChatConversation{}).
|
||||
Where("id = ? AND user_id = ?", req.ID, user.ID).
|
||||
Updates(map[string]interface{}{"title": title})
|
||||
if result.Error != nil || result.RowsAffected == 0 {
|
||||
ReturnJson(ctx, "apiErr", nil)
|
||||
return
|
||||
}
|
||||
ReturnJson(ctx, "apiOK", gin.H{"id": req.ID, "title": title})
|
||||
}
|
||||
|
||||
func handleAIChatConversationDelete(ctx *gin.Context) {
|
||||
isAuth, user, data := AuthenticationAuthority(ctx)
|
||||
if !isAuth {
|
||||
ReturnJson(ctx, "userCookieError", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var req aiChatConversationIDReq
|
||||
if err := decodeJSON(data, &req); err != nil || req.ID == 0 {
|
||||
ReturnJson(ctx, "jsonErr", nil)
|
||||
return
|
||||
}
|
||||
|
||||
err := models.DB.Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Where("id = ? AND user_id = ?", req.ID, user.ID).Delete(&TabAIChatConversation{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return tx.Where("conversation_id = ? AND user_id = ?", req.ID, user.ID).Delete(&TabAIChatMessage{}).Error
|
||||
})
|
||||
if err != nil {
|
||||
ReturnJson(ctx, "apiErr", nil)
|
||||
return
|
||||
}
|
||||
ReturnJson(ctx, "apiOK", gin.H{"id": req.ID})
|
||||
}
|
||||
|
||||
func aiChatConversationResponse(conversation TabAIChatConversation) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": conversation.ID,
|
||||
"title": conversation.Title,
|
||||
"openaiName": conversation.OpenAIName,
|
||||
"clientLocalId": conversation.ClientLocalID,
|
||||
"messageCount": conversation.MessageCount,
|
||||
"lastMessageAt": conversation.LastMessageAt,
|
||||
"createdAt": conversation.CreatedAt,
|
||||
"updatedAt": conversation.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func aiChatMessageResponse(message TabAIChatMessage) map[string]interface{} {
|
||||
item := map[string]interface{}{
|
||||
"id": message.ID,
|
||||
"conversationId": message.ConversationID,
|
||||
"role": message.Role,
|
||||
"content": message.Content,
|
||||
"image_url": message.ImageURL,
|
||||
"reasoning": message.Reasoning,
|
||||
"seq": message.Seq,
|
||||
"createdAt": message.CreatedAt,
|
||||
}
|
||||
var traces []sseEvent
|
||||
if message.TracesJSON != "" && json.Unmarshal([]byte(message.TracesJSON), &traces) == nil {
|
||||
item["traces"] = traces
|
||||
}
|
||||
var stats tokenUsageStats
|
||||
if message.StatsJSON != "" && json.Unmarshal([]byte(message.StatsJSON), &stats) == nil {
|
||||
item["stats"] = stats
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func prepareAIChatPersistence(user *TabUser, req chatRequest, openAIName string) (*TabAIChatConversation, error) {
|
||||
if user == nil || !req.SaveToServer {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
lastUserMessage, ok := lastAIChatUserMessage(req.Messages)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var conversation TabAIChatConversation
|
||||
if req.ConversationID > 0 {
|
||||
if err := models.DB.Where("id = ? AND user_id = ?", req.ConversationID, user.ID).First(&conversation).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if strings.TrimSpace(req.ClientLocalID) != "" {
|
||||
err := models.DB.Where("user_id = ? AND client_local_id = ?", user.ID, strings.TrimSpace(req.ClientLocalID)).First(&conversation).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
conversation = TabAIChatConversation{}
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if conversation.ID == 0 {
|
||||
conversation = TabAIChatConversation{
|
||||
UserID: user.ID,
|
||||
Title: makeAIChatTitle(lastUserMessage),
|
||||
OpenAIName: openAIName,
|
||||
ClientLocalID: strings.TrimSpace(req.ClientLocalID),
|
||||
LastMessageAt: &now,
|
||||
}
|
||||
if err := models.DB.Create(&conversation).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if conversation.OpenAIName == "" && openAIName != "" {
|
||||
_ = models.DB.Model(&conversation).Update("open_ai_name", openAIName).Error
|
||||
conversation.OpenAIName = openAIName
|
||||
}
|
||||
|
||||
if err := createAIChatMessage(conversation.ID, user.ID, lastUserMessage, "", nil, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := refreshAIChatConversationSummary(conversation.ID, user.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := models.DB.Where("id = ? AND user_id = ?", conversation.ID, user.ID).First(&conversation).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &conversation, nil
|
||||
}
|
||||
|
||||
func persistAIChatAssistantMessage(conversation *TabAIChatConversation, content string, reasoning string, traces []sseEvent, stats *tokenUsageStats) error {
|
||||
if conversation == nil {
|
||||
return nil
|
||||
}
|
||||
message := chatMessage{Role: "assistant", Content: content}
|
||||
if err := createAIChatMessage(conversation.ID, conversation.UserID, message, reasoning, traces, stats); err != nil {
|
||||
return err
|
||||
}
|
||||
return refreshAIChatConversationSummary(conversation.ID, conversation.UserID)
|
||||
}
|
||||
|
||||
func createAIChatMessage(conversationID uint, userID uint, message chatMessage, reasoning string, traces []sseEvent, stats *tokenUsageStats) error {
|
||||
var maxSeq int
|
||||
models.DB.Model(&TabAIChatMessage{}).
|
||||
Where("conversation_id = ? AND user_id = ?", conversationID, userID).
|
||||
Select("COALESCE(MAX(seq), 0)").
|
||||
Scan(&maxSeq)
|
||||
|
||||
imageURL := message.ImageURL
|
||||
if imageURL == "" {
|
||||
imageURL = message.ImageURLAlias
|
||||
}
|
||||
tracesJSON := ""
|
||||
if len(traces) > 0 {
|
||||
if data, err := json.Marshal(traces); err == nil {
|
||||
tracesJSON = string(data)
|
||||
}
|
||||
}
|
||||
statsJSON := ""
|
||||
if stats != nil {
|
||||
if data, err := json.Marshal(stats); err == nil {
|
||||
statsJSON = string(data)
|
||||
}
|
||||
}
|
||||
|
||||
row := TabAIChatMessage{
|
||||
ConversationID: conversationID,
|
||||
UserID: userID,
|
||||
Role: message.Role,
|
||||
Content: message.Content,
|
||||
ImageURL: imageURL,
|
||||
Reasoning: reasoning,
|
||||
TracesJSON: tracesJSON,
|
||||
StatsJSON: statsJSON,
|
||||
Seq: maxSeq + 1,
|
||||
}
|
||||
return models.DB.Create(&row).Error
|
||||
}
|
||||
|
||||
func refreshAIChatConversationSummary(conversationID uint, userID uint) error {
|
||||
var count int64
|
||||
if err := models.DB.Model(&TabAIChatMessage{}).Where("conversation_id = ? AND user_id = ?", conversationID, userID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var lastMessage TabAIChatMessage
|
||||
lastMessageAt := time.Now()
|
||||
if err := models.DB.Where("conversation_id = ? AND user_id = ?", conversationID, userID).Order("seq desc, id desc").First(&lastMessage).Error; err == nil && lastMessage.CreatedAt != nil {
|
||||
lastMessageAt = *lastMessage.CreatedAt
|
||||
}
|
||||
return models.DB.Model(&TabAIChatConversation{}).
|
||||
Where("id = ? AND user_id = ?", conversationID, userID).
|
||||
Updates(map[string]interface{}{
|
||||
"message_count": int(count),
|
||||
"last_message_at": lastMessageAt,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func lastAIChatUserMessage(messages []chatMessage) (chatMessage, bool) {
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
if messages[i].Role == "user" {
|
||||
return messages[i], true
|
||||
}
|
||||
}
|
||||
return chatMessage{}, false
|
||||
}
|
||||
|
||||
func makeAIChatTitle(message chatMessage) string {
|
||||
title := strings.TrimSpace(message.Content)
|
||||
if title == "" {
|
||||
title = "新对话"
|
||||
}
|
||||
runes := []rune(title)
|
||||
if len(runes) > 40 {
|
||||
title = string(runes[:40])
|
||||
}
|
||||
return title
|
||||
}
|
||||
Reference in New Issue
Block a user