فهرست منبع

feat:AI会话增加mongodb持久化

Jerry Liang 2 ماه پیش
والد
کامیت
90647c03b6
4فایلهای تغییر یافته به همراه241 افزوده شده و 41 حذف شده
  1. 14 0
      service/domain/aiChatDomain.go
  2. 138 0
      service/repositories/mongo/aiChatRepo.go
  3. 12 1
      service/repositories/repositories.go
  4. 77 40
      service/services/aiChatServices.go

+ 14 - 0
service/domain/aiChatDomain.go

@@ -0,0 +1,14 @@
+package domain
+
+import "time"
+
+// AiChatSession AI 助手会话
+type AiChatSession struct {
+	UserId     string    `bson:"userId"`
+	SessionId  string    `bson:"sessionId"`
+	Title      string    `bson:"title"`
+	IsActive   bool      `bson:"isActive"`
+	CreatedAt  time.Time `bson:"createdAt"`
+	LastActive time.Time `bson:"lastActive"`
+	UpdatedAt  time.Time `bson:"updatedAt"`
+}

+ 138 - 0
service/repositories/mongo/aiChatRepo.go

@@ -0,0 +1,138 @@
+package mongo
+
+import (
+	"context"
+	"fwh-toolbox-server/database"
+	"fwh-toolbox-server/domain"
+	"time"
+
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo/options"
+)
+
+const CollectionName_AiChatSession = "AiChatSessions"
+
+// UpsertAiChatSession 创建或更新会话记录
+func UpsertAiChatSession(sess domain.AiChatSession) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_AiChatSession)
+
+	filter := bson.M{
+		"userId":    sess.UserId,
+		"sessionId": sess.SessionId,
+	}
+	update := bson.M{
+		"$set": bson.M{
+			"title":      sess.Title,
+			"isActive":   sess.IsActive,
+			"lastActive": sess.LastActive,
+			"updatedAt":  time.Now(),
+		},
+		"$setOnInsert": bson.M{
+			"userId":    sess.UserId,
+			"sessionId": sess.SessionId,
+			"createdAt": sess.CreatedAt,
+		},
+	}
+
+	opts := options.Update().SetUpsert(true)
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	return err
+}
+
+// GetAiChatSessionsByUser 获取用户的所有会话(按 lastActive 倒序)
+func GetAiChatSessionsByUser(userId string) ([]domain.AiChatSession, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_AiChatSession)
+
+	filter := bson.M{"userId": userId}
+	findOpts := options.Find().
+		SetSort(bson.D{{Key: "lastActive", Value: -1}}).
+		SetLimit(50)
+
+	cursor, err := collection.Find(ctx, filter, findOpts)
+	if err != nil {
+		return nil, err
+	}
+	defer cursor.Close(ctx)
+
+	var sessions []domain.AiChatSession
+	if err := cursor.All(ctx, &sessions); err != nil {
+		return nil, err
+	}
+	return sessions, nil
+}
+
+// UpdateAiChatSessionTitle 更新会话标题
+func UpdateAiChatSessionTitle(userId string, sessionId string, title string) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_AiChatSession)
+
+	filter := bson.M{"userId": userId, "sessionId": sessionId}
+	update := bson.M{
+		"$set": bson.M{
+			"title":     title,
+			"updatedAt": time.Now(),
+		},
+	}
+
+	_, err := collection.UpdateOne(ctx, filter, update)
+	return err
+}
+
+// UpdateAiChatSessionActive 更新会话的活跃状态
+func UpdateAiChatSessionActive(userId string, sessionId string, isActive bool) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_AiChatSession)
+
+	filter := bson.M{"userId": userId, "sessionId": sessionId}
+	update := bson.M{
+		"$set": bson.M{
+			"isActive":   isActive,
+			"lastActive": time.Now(),
+			"updatedAt":  time.Now(),
+		},
+	}
+
+	_, err := collection.UpdateOne(ctx, filter, update)
+	return err
+}
+
+// DeactivateAllAiChatSessions 将用户所有会话设为非活跃
+func DeactivateAllAiChatSessions(userId string) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_AiChatSession)
+
+	filter := bson.M{"userId": userId, "isActive": true}
+	update := bson.M{
+		"$set": bson.M{
+			"isActive":  false,
+			"updatedAt": time.Now(),
+		},
+	}
+
+	_, err := collection.UpdateMany(ctx, filter, update)
+	return err
+}
+
+// DeleteAiChatSession 删除会话记录
+func DeleteAiChatSession(userId string, sessionId string) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection(CollectionName_AiChatSession)
+
+	filter := bson.M{"userId": userId, "sessionId": sessionId}
+	_, err := collection.DeleteOne(ctx, filter)
+	return err
+}

+ 12 - 1
service/repositories/repositories.go

@@ -1,6 +1,7 @@
 package repositories
 
 import (
+	"fwh-toolbox-server/domain"
 	"fwh-toolbox-server/repositories/api/acceleronix"
 	"fwh-toolbox-server/repositories/api/fwh_pro"
 	"fwh-toolbox-server/repositories/api/plane_v1"
@@ -533,7 +534,7 @@ var RefreshAutoUpdateConfigFileInfo = redis.RefreshAutoUpdateConfigFileInfo
 var GetAutoUpdateConfigFileInfoTotal = redis.GetAutoUpdateConfigFileInfoTotal
 var GetAutoUpdateConfigFileInfoByPage = redis.GetAutoUpdateConfigFileInfoByPage
 
-// AIChat
+// AIChat - Redis
 type AiSessionItem = redis.AiSessionItem
 type AiActiveSession = redis.AiActiveSession
 
@@ -545,6 +546,16 @@ var GetAiSessionList = redis.GetAiSessionList
 var DeleteAiSessionList = redis.DeleteAiSessionList
 var UpdateAiSessionTitle = redis.UpdateAiSessionTitle
 
+// AIChat - MongoDB
+type AiChatSession = domain.AiChatSession
+
+var UpsertAiChatSession = mongo.UpsertAiChatSession
+var GetAiChatSessionsByUser = mongo.GetAiChatSessionsByUser
+var UpdateAiChatSessionTitle = mongo.UpdateAiChatSessionTitle
+var UpdateAiChatSessionActive = mongo.UpdateAiChatSessionActive
+var DeactivateAllAiChatSessions = mongo.DeactivateAllAiChatSessions
+var DeleteAiChatSession = mongo.DeleteAiChatSession
+
 /*************** MQTT ***************/
 // FwhMqtt
 var FwhMqttInit = fwh_mqtt.InitFwhMqttClient

+ 77 - 40
service/services/aiChatServices.go

@@ -107,7 +107,7 @@ func (m *ACPManager) CreateNewSession(userId string) (*ACPSession, error) {
 
 	// 归档当前活跃会话
 	if sess, ok := m.sessions[userId]; ok {
-		m.archiveToRedis(userId, sess)
+		m.archiveSession(userId, sess)
 		sess.Client.Close()
 		delete(m.sessions, userId)
 	}
@@ -127,7 +127,7 @@ func (m *ACPManager) LoadSession(userId string, sessionId string) (*ACPSession,
 			return sess, nil
 		}
 		// 归档当前活跃会话
-		m.archiveToRedis(userId, sess)
+		m.archiveSession(userId, sess)
 		sess.Client.Close()
 		delete(m.sessions, userId)
 	}
@@ -176,13 +176,8 @@ func (m *ACPManager) LoadSession(userId string, sessionId string) (*ACPSession,
 	client.UserId = userId
 	m.sessions[userId] = sess
 
-	// 更新 Redis 活跃会话
-	repositories.SaveAiSession(userId, repositories.AiActiveSession{
-		SessionId:  sess.SessionId,
-		Title:      sess.Title,
-		CreatedAt:  sess.CreatedAt.UnixMilli(),
-		LastActive: sess.LastActive.UnixMilli(),
-	})
+	// 持久化
+	m.activateSession(userId, sess)
 
 	zap.L().Info("[AIChat] 会话加载成功", zap.String("userId", userId), zap.String("sessionId", sessionId))
 	return sess, nil
@@ -251,18 +246,12 @@ func (m *ACPManager) UpdateSessionTitle(userId string, sessionId string, title s
 	// 检查当前活跃会话
 	if sess, ok := m.sessions[userId]; ok && sess.SessionId == sessionId {
 		sess.Title = title
-		// 同步到 Redis 活跃会话 key
-		repositories.SaveAiSession(userId, repositories.AiActiveSession{
-			SessionId:  sess.SessionId,
-			Title:      sess.Title,
-			CreatedAt:  sess.CreatedAt.UnixMilli(),
-			LastActive: sess.LastActive.UnixMilli(),
-		})
+		m.saveSessionTitle(userId, sess)
 		return
 	}
 
-	// 同步到 Redis 历史列表
-	repositories.UpdateAiSessionTitle(userId, sessionId, title)
+	// 历史会话
+	m.saveHistorySessionTitle(userId, sessionId, title)
 }
 
 // DeleteSessionFromHistory 删除某会话
@@ -274,12 +263,12 @@ func (m *ACPManager) DeleteSessionFromHistory(userId string, sessionId string) {
 	if sess, ok := m.sessions[userId]; ok && sess.SessionId == sessionId {
 		sess.Client.Close()
 		delete(m.sessions, userId)
-		repositories.DeleteAiSession(userId)
+		m.removeSession(userId, sessionId, true)
 		return
 	}
 
-	// 从 Redis 历史列表中移除
-	m.removeFromRedisHistory(userId, sessionId)
+	// 历史会话
+	m.removeSession(userId, sessionId, false)
 }
 
 // DeleteSession 删除当前活跃会话
@@ -290,7 +279,7 @@ func (m *ACPManager) DeleteSession(userId string) {
 	if sess, ok := m.sessions[userId]; ok {
 		sess.Client.Close()
 		delete(m.sessions, userId)
-		repositories.DeleteAiSession(userId)
+		m.removeSession(userId, sess.SessionId, true)
 		zap.L().Info("[AIChat] 会话已删除", zap.String("userId", userId))
 	}
 }
@@ -395,13 +384,8 @@ func (m *ACPManager) createSession(userId string) (*ACPSession, error) {
 	client.UserId = userId
 	m.sessions[userId] = sess
 
-	// 持久化活跃会话到 Redis
-	repositories.SaveAiSession(userId, repositories.AiActiveSession{
-		SessionId:  sess.SessionId,
-		Title:      sess.Title,
-		CreatedAt:  sess.CreatedAt.UnixMilli(),
-		LastActive: sess.LastActive.UnixMilli(),
-	})
+	// 持久化
+	m.saveNewSession(userId, sess)
 
 	zap.L().Info("[AIChat] 会话创建成功", zap.String("userId", userId), zap.String("sessionId", sessionId))
 	return sess, nil
@@ -439,8 +423,41 @@ func (m *ACPManager) startAndLoadSession(userId string, active *repositories.AiA
 	return sess, nil
 }
 
-// archiveToRedis 将会话归档到 Redis 历史列表
-func (m *ACPManager) archiveToRedis(userId string, sess *ACPSession) {
+// ============ 内部方法:会话存储 ============
+
+// persistNewSession 持久化新创建的活跃会话(Redis + MongoDB)
+func (m *ACPManager) saveNewSession(userId string, sess *ACPSession) {
+	repositories.SaveAiSession(userId, repositories.AiActiveSession{
+		SessionId:  sess.SessionId,
+		Title:      sess.Title,
+		CreatedAt:  sess.CreatedAt.UnixMilli(),
+		LastActive: sess.LastActive.UnixMilli(),
+	})
+	repositories.DeactivateAllAiChatSessions(userId)
+	repositories.UpsertAiChatSession(repositories.AiChatSession{
+		UserId:     userId,
+		SessionId:  sess.SessionId,
+		Title:      sess.Title,
+		IsActive:   true,
+		CreatedAt:  sess.CreatedAt,
+		LastActive: sess.LastActive,
+	})
+}
+
+// persistActivateSession 持久化会话激活(Redis + MongoDB)
+func (m *ACPManager) activateSession(userId string, sess *ACPSession) {
+	repositories.SaveAiSession(userId, repositories.AiActiveSession{
+		SessionId:  sess.SessionId,
+		Title:      sess.Title,
+		CreatedAt:  sess.CreatedAt.UnixMilli(),
+		LastActive: sess.LastActive.UnixMilli(),
+	})
+	repositories.DeactivateAllAiChatSessions(userId)
+	repositories.UpdateAiChatSessionActive(userId, sess.SessionId, true)
+}
+
+// persistArchiveSession 持久化会话归档(Redis + MongoDB)
+func (m *ACPManager) archiveSession(userId string, sess *ACPSession) {
 	item := repositories.AiSessionItem{
 		SessionId:  sess.SessionId,
 		Title:      sess.Title,
@@ -448,18 +465,12 @@ func (m *ACPManager) archiveToRedis(userId string, sess *ACPSession) {
 		LastActive: sess.LastActive.UnixMilli(),
 	}
 
-	// 读取现有历史
 	list, _ := repositories.GetAiSessionList(userId)
-
-	// 插入头部
 	list = append([]repositories.AiSessionItem{item}, list...)
 
-	// 超过 50 条:删除多余的 Kiro 会话
 	if len(list) > 50 {
 		overflow := list[50:]
 		list = list[:50]
-
-		// 后台异步删除多余会话
 		go func() {
 			for _, old := range overflow {
 				m.deleteKiroSession(old.SessionId)
@@ -470,11 +481,37 @@ func (m *ACPManager) archiveToRedis(userId string, sess *ACPSession) {
 	if err := repositories.SaveAiSessionList(userId, list); err != nil {
 		zap.L().Warn("[AIChat] 归档会话到Redis失败", zap.String("userId", userId), zap.Error(err))
 	}
+	repositories.UpdateAiChatSessionActive(userId, sess.SessionId, false)
+}
+
+// persistUpdateTitle 持久化标题更新(Redis + MongoDB)
+func (m *ACPManager) saveSessionTitle(userId string, sess *ACPSession) {
+	repositories.SaveAiSession(userId, repositories.AiActiveSession{
+		SessionId:  sess.SessionId,
+		Title:      sess.Title,
+		CreatedAt:  sess.CreatedAt.UnixMilli(),
+		LastActive: sess.LastActive.UnixMilli(),
+	})
+	repositories.UpdateAiChatSessionTitle(userId, sess.SessionId, sess.Title)
+}
+
+// persistUpdateHistoryTitle 持久化历史会话标题更新(Redis + MongoDB)
+func (m *ACPManager) saveHistorySessionTitle(userId string, sessionId string, title string) {
+	repositories.UpdateAiSessionTitle(userId, sessionId, title)
+	repositories.UpdateAiChatSessionTitle(userId, sessionId, title)
+}
+
+// persistDeleteSession 持久化删除会话(Redis + MongoDB)
+func (m *ACPManager) removeSession(userId string, sessionId string, isActive bool) {
+	if isActive {
+		repositories.DeleteAiSession(userId)
+	} else {
+		m.removeFromRedisHistory(userId, sessionId)
+	}
+	repositories.DeleteAiChatSession(userId, sessionId)
 }
 
-// archiveActiveSessionFromRedis 将 Redis session key 中的活跃会话归档到历史列表
-// 用于后端重启后、内存中无活跃会话时的归档保护
-// removeFromRedisHistory 从 Redis 历史列表中移除指定会话
+// ============ 内部方法:Redis 操作 ============
 func (m *ACPManager) removeFromRedisHistory(userId string, sessionId string) {
 	list, err := repositories.GetAiSessionList(userId)
 	if err != nil || len(list) == 0 {