347 lines
9.7 KiB
Go
347 lines
9.7 KiB
Go
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
|
|
}
|