Browse Source

perf:优化AI助手会话使用体验

Jerry Liang 2 months ago
parent
commit
bb384e10df

+ 5 - 29
service/controllers/aiChatControllers.go

@@ -9,6 +9,7 @@ import (
 	"path/filepath"
 	"path/filepath"
 
 
 	"github.com/gofiber/fiber/v2"
 	"github.com/gofiber/fiber/v2"
+	"github.com/gofiber/fiber/v2/utils"
 	"github.com/minio/minio-go/v7"
 	"github.com/minio/minio-go/v7"
 
 
 	"fwh-toolbox-server/middleware"
 	"fwh-toolbox-server/middleware"
@@ -72,31 +73,6 @@ func AiChat(c *fiber.Ctx) error {
 	return nil
 	return nil
 }
 }
 
 
-// GetAiSessions 获取当前用户的会话信息
-// GET /ai/sessions
-func GetAiSessions(c *fiber.Ctx) error {
-	userId := middleware.AuthGetUserName(c)
-	sess, err := acpManager.GetOrCreateSession(userId)
-	if err != nil {
-		return Error(c, fmt.Sprintf("AI 会话创建失败: %v", err))
-	}
-
-	if sess == nil {
-		return Success(c, fiber.Map{
-			"active":    false,
-			"sessionId": "",
-		})
-	}
-
-	return Success(c, fiber.Map{
-		"active":         true,
-		"sessionId":      sess.SessionId,
-		"title":          sess.Title,
-		"welcomeMessage": sess.WelcomeMessage,
-		"lastActive":     sess.LastActive,
-	})
-}
-
 // GetAiSessionList 获取用户的会话历史列表
 // GetAiSessionList 获取用户的会话历史列表
 // GET /ai/sessions/list
 // GET /ai/sessions/list
 func GetAiSessionList(c *fiber.Ctx) error {
 func GetAiSessionList(c *fiber.Ctx) error {
@@ -125,7 +101,7 @@ func CreateAiSession(c *fiber.Ctx) error {
 // LoadAiSession 加载历史会话(恢复 AI 上下文)
 // LoadAiSession 加载历史会话(恢复 AI 上下文)
 // POST /ai/sessions/:id/load
 // POST /ai/sessions/:id/load
 func LoadAiSession(c *fiber.Ctx) error {
 func LoadAiSession(c *fiber.Ctx) error {
-	sessionId := c.Params("id")
+	sessionId := utils.CopyString(c.Params("id"))
 	if sessionId == "" {
 	if sessionId == "" {
 		return Fail(c, "sessionId 不能为空")
 		return Fail(c, "sessionId 不能为空")
 	}
 	}
@@ -146,7 +122,7 @@ func LoadAiSession(c *fiber.Ctx) error {
 // UpdateAiSessionTitle 更新会话标题
 // UpdateAiSessionTitle 更新会话标题
 // PATCH /ai/sessions/:id
 // PATCH /ai/sessions/:id
 func UpdateAiSessionTitle(c *fiber.Ctx) error {
 func UpdateAiSessionTitle(c *fiber.Ctx) error {
-	sessionId := c.Params("id")
+	sessionId := utils.CopyString(c.Params("id"))
 	if sessionId == "" {
 	if sessionId == "" {
 		return Fail(c, "sessionId 不能为空")
 		return Fail(c, "sessionId 不能为空")
 	}
 	}
@@ -166,7 +142,7 @@ func UpdateAiSessionTitle(c *fiber.Ctx) error {
 // DeleteAiSession 删除 AI 会话
 // DeleteAiSession 删除 AI 会话
 // DELETE /ai/sessions/:id
 // DELETE /ai/sessions/:id
 func DeleteAiSession(c *fiber.Ctx) error {
 func DeleteAiSession(c *fiber.Ctx) error {
-	sessionId := c.Params("id")
+	sessionId := utils.CopyString(c.Params("id"))
 	userId := middleware.AuthGetUserName(c)
 	userId := middleware.AuthGetUserName(c)
 
 
 	if sessionId == "" {
 	if sessionId == "" {
@@ -216,7 +192,7 @@ func ResolvePermission(c *fiber.Ctx) error {
 // DownloadAiFile 下载 AI 生成的文件
 // DownloadAiFile 下载 AI 生成的文件
 // GET /ai/files/download?key=xxx
 // GET /ai/files/download?key=xxx
 func DownloadAiFile(c *fiber.Ctx) error {
 func DownloadAiFile(c *fiber.Ctx) error {
-	key := c.Query("key")
+	key := utils.CopyString(c.Query("key"))
 	if key == "" {
 	if key == "" {
 		return Fail(c, "key 不能为空")
 		return Fail(c, "key 不能为空")
 	}
 	}

+ 114 - 0
service/repositories/redis/aiChatRepo.go

@@ -0,0 +1,114 @@
+package redis
+
+import (
+	"context"
+	"encoding/json"
+	"fwh-toolbox-server/cache"
+	"time"
+
+	"go.uber.org/zap"
+)
+
+var (
+	aiChatCtx            = context.Background()
+	aiChatSessionKey     = "fwhtoolbox:aiChat:session:"     // {prefix}{userId} → sessionId
+	aiChatSessionListKey = "fwhtoolbox:aiChat:sessionList:" // {prefix}{userId} → JSON array
+	aiChatTTL            = 7 * 24 * time.Hour
+)
+
+// AiSessionItem 会话列表项(Redis 存储结构)
+type AiSessionItem struct {
+	SessionId  string `json:"sessionId"`
+	Title      string `json:"title"`
+	CreatedAt  int64  `json:"createdAt"`
+	LastActive int64  `json:"lastActive"`
+}
+
+// ============ 当前活跃会话 ============
+
+// AiActiveSession 当前活跃会话信息(Redis 存储结构)
+type AiActiveSession struct {
+	SessionId  string `json:"sessionId"`
+	Title      string `json:"title"`
+	CreatedAt  int64  `json:"createdAt"`
+	LastActive int64  `json:"lastActive"`
+}
+
+// SaveAiSession 保存用户当前活跃会话完整信息
+func SaveAiSession(userId string, sess AiActiveSession) error {
+	key := aiChatSessionKey + userId
+	data, err := json.Marshal(sess)
+	if err != nil {
+		return err
+	}
+	return cache.Set(aiChatCtx, key, string(data), aiChatTTL)
+}
+
+// GetAiSession 获取用户保存的活跃会话信息
+func GetAiSession(userId string) (*AiActiveSession, error) {
+	key := aiChatSessionKey + userId
+	raw, err := cache.Get(aiChatCtx, key)
+	if err != nil {
+		return nil, err
+	}
+	var sess AiActiveSession
+	if err := json.Unmarshal([]byte(raw), &sess); err != nil {
+		return nil, err
+	}
+	return &sess, nil
+}
+
+// DeleteAiSession 删除用户保存的活跃会话信息
+func DeleteAiSession(userId string) {
+	key := aiChatSessionKey + userId
+	cache.Delete(aiChatCtx, key)
+}
+
+// ============ 会话历史列表 ============
+
+// SaveAiSessionList 保存用户完整会话历史列表
+func SaveAiSessionList(userId string, list []AiSessionItem) error {
+	key := aiChatSessionListKey + userId
+	data, err := json.Marshal(list)
+	if err != nil {
+		return err
+	}
+	return cache.Set(aiChatCtx, key, string(data), aiChatTTL)
+}
+
+// GetAiSessionList 获取用户完整会话历史列表
+func GetAiSessionList(userId string) ([]AiSessionItem, error) {
+	key := aiChatSessionListKey + userId
+	raw, err := cache.Get(aiChatCtx, key)
+	if err != nil {
+		return nil, err
+	}
+	var list []AiSessionItem
+	if err := json.Unmarshal([]byte(raw), &list); err != nil {
+		return nil, err
+	}
+	return list, nil
+}
+
+// DeleteAiSessionList 删除用户会话历史列表
+func DeleteAiSessionList(userId string) {
+	key := aiChatSessionListKey + userId
+	cache.Delete(aiChatCtx, key)
+}
+
+// UpdateAiSessionTitle 更新列表中指定会话的标题
+func UpdateAiSessionTitle(userId string, sessionId string, title string) {
+	list, err := GetAiSessionList(userId)
+	if err != nil {
+		return
+	}
+	for i := range list {
+		if list[i].SessionId == sessionId {
+			list[i].Title = title
+			break
+		}
+	}
+	if err := SaveAiSessionList(userId, list); err != nil {
+		zap.L().Warn("[AIChat] 更新会话标题到Redis失败", zap.Error(err))
+	}
+}

+ 12 - 0
service/repositories/repositories.go

@@ -533,6 +533,18 @@ var RefreshAutoUpdateConfigFileInfo = redis.RefreshAutoUpdateConfigFileInfo
 var GetAutoUpdateConfigFileInfoTotal = redis.GetAutoUpdateConfigFileInfoTotal
 var GetAutoUpdateConfigFileInfoTotal = redis.GetAutoUpdateConfigFileInfoTotal
 var GetAutoUpdateConfigFileInfoByPage = redis.GetAutoUpdateConfigFileInfoByPage
 var GetAutoUpdateConfigFileInfoByPage = redis.GetAutoUpdateConfigFileInfoByPage
 
 
+// AIChat
+type AiSessionItem = redis.AiSessionItem
+type AiActiveSession = redis.AiActiveSession
+
+var SaveAiSession = redis.SaveAiSession
+var GetAiSession = redis.GetAiSession
+var DeleteAiSession = redis.DeleteAiSession
+var SaveAiSessionList = redis.SaveAiSessionList
+var GetAiSessionList = redis.GetAiSessionList
+var DeleteAiSessionList = redis.DeleteAiSessionList
+var UpdateAiSessionTitle = redis.UpdateAiSessionTitle
+
 /*************** MQTT ***************/
 /*************** MQTT ***************/
 // FwhMqtt
 // FwhMqtt
 var FwhMqttInit = fwh_mqtt.InitFwhMqttClient
 var FwhMqttInit = fwh_mqtt.InitFwhMqttClient

+ 0 - 3
service/routes/aiChatRoutes.go

@@ -10,9 +10,6 @@ func AiChatSetup(api fiber.Router) {
 	// 发送消息(SSE 流式响应)
 	// 发送消息(SSE 流式响应)
 	api.Post("/ai/chat", controllers.AiChat)
 	api.Post("/ai/chat", controllers.AiChat)
 
 
-	// 获取当前用户会话信息
-	api.Get("/ai/sessions", controllers.GetAiSessions)
-
 	// 获取会话历史列表
 	// 获取会话历史列表
 	api.Get("/ai/sessions/list", controllers.GetAiSessionList)
 	api.Get("/ai/sessions/list", controllers.GetAiSessionList)
 
 

+ 278 - 159
service/services/aiChatServices.go

@@ -3,7 +3,9 @@ package services
 import (
 import (
 	"encoding/json"
 	"encoding/json"
 	"fmt"
 	"fmt"
+	"fwh-toolbox-server/repositories"
 	"fwh-toolbox-server/utils/acp"
 	"fwh-toolbox-server/utils/acp"
+	"os/exec"
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
@@ -39,25 +41,21 @@ type SessionListItem struct {
 	Title      string `json:"title"`
 	Title      string `json:"title"`
 	CreatedAt  int64  `json:"createdAt"`
 	CreatedAt  int64  `json:"createdAt"`
 	LastActive int64  `json:"lastActive"`
 	LastActive int64  `json:"lastActive"`
-	IsActive   bool   `json:"isActive"` // 是否为当前激活的会话
+	IsActive   bool   `json:"isActive"`
 }
 }
 
 
 // ACPManager 管理所有用户的 ACP 会话
 // ACPManager 管理所有用户的 ACP 会话
 type ACPManager struct {
 type ACPManager struct {
-	sessions   map[string]*ACPSession       // key: userId -> 当前活跃会话
-	history    map[string][]SessionListItem // key: userId -> 历史会话列表(元数据)
-	mu         sync.RWMutex
-	maxIdle    time.Duration
+	sessions   map[string]*ACPSession // key: userId -> 当前活跃会话(持有进程引用)
+	mu         sync.Mutex
 	serviceDir string
 	serviceDir string
 	acpConfig  acp.ClientConfig
 	acpConfig  acp.ClientConfig
 }
 }
 
 
 // NewACPManager 创建 ACP 管理器
 // NewACPManager 创建 ACP 管理器
 func NewACPManager(serviceDir string, env []string) *ACPManager {
 func NewACPManager(serviceDir string, env []string) *ACPManager {
-	mgr := &ACPManager{
+	return &ACPManager{
 		sessions:   make(map[string]*ACPSession),
 		sessions:   make(map[string]*ACPSession),
-		history:    make(map[string][]SessionListItem),
-		maxIdle:    30 * time.Minute,
 		serviceDir: serviceDir,
 		serviceDir: serviceDir,
 		acpConfig: acp.ClientConfig{
 		acpConfig: acp.ClientConfig{
 			Command:    "kiro-cli",
 			Command:    "kiro-cli",
@@ -66,50 +64,55 @@ func NewACPManager(serviceDir string, env []string) *ACPManager {
 			Env:        env,
 			Env:        env,
 		},
 		},
 	}
 	}
-
-	// 定时清理空闲会话
-	go func() {
-		ticker := time.NewTicker(5 * time.Minute)
-		defer ticker.Stop()
-		for range ticker.C {
-			mgr.CleanupIdle()
-		}
-	}()
-
-	return mgr
 }
 }
 
 
+// ============ 会话生命周期 ============
+
 // GetOrCreateSession 获取或创建用户的 ACP 会话
 // GetOrCreateSession 获取或创建用户的 ACP 会话
+// 优先级:内存活跃会话 → Redis 持久化恢复 → 创建新会话
 func (m *ACPManager) GetOrCreateSession(userId string) (*ACPSession, error) {
 func (m *ACPManager) GetOrCreateSession(userId string) (*ACPSession, error) {
 	m.mu.Lock()
 	m.mu.Lock()
 	defer m.mu.Unlock()
 	defer m.mu.Unlock()
 
 
+	// 1. 检查内存中是否有活跃会话
 	if sess, ok := m.sessions[userId]; ok {
 	if sess, ok := m.sessions[userId]; ok {
 		if sess.Client.IsAlive() {
 		if sess.Client.IsAlive() {
 			sess.LastActive = time.Now()
 			sess.LastActive = time.Now()
 			return sess, nil
 			return sess, nil
 		}
 		}
-		// 进程已退出,清理后重新创建
 		sess.Client.Close()
 		sess.Client.Close()
 		delete(m.sessions, userId)
 		delete(m.sessions, userId)
 	}
 	}
 
 
-	return m.createNewSessionLocked(userId)
+	// 2. 尝试从 Redis 恢复会话
+	savedSession, err := repositories.GetAiSession(userId)
+	if err == nil && savedSession != nil {
+		sess, err := m.startAndLoadSession(userId, savedSession)
+		if err == nil {
+			zap.L().Info("[AIChat] 从 Redis 恢复会话成功", zap.String("userId", userId), zap.String("sessionId", savedSession.SessionId))
+			return sess, nil
+		}
+		zap.L().Warn("[AIChat] 从 Redis 恢复会话失败,将创建新会话", zap.String("userId", userId), zap.Error(err))
+		repositories.DeleteAiSession(userId)
+	}
+
+	// 3. 创建新会话
+	return m.createSession(userId)
 }
 }
 
 
-// CreateNewSession 强制创建一个新会话(保留旧会话到历史列表)
+// CreateNewSession 强制创建一个新会话(归档旧会话到 Redis 历史)
 func (m *ACPManager) CreateNewSession(userId string) (*ACPSession, error) {
 func (m *ACPManager) CreateNewSession(userId string) (*ACPSession, error) {
 	m.mu.Lock()
 	m.mu.Lock()
 	defer m.mu.Unlock()
 	defer m.mu.Unlock()
 
 
-	// 将当前活跃会话存入历史
+	// 归档当前活跃会话
 	if sess, ok := m.sessions[userId]; ok {
 	if sess, ok := m.sessions[userId]; ok {
-		m.archiveSessionLocked(userId, sess)
+		m.archiveToRedis(userId, sess)
 		sess.Client.Close()
 		sess.Client.Close()
 		delete(m.sessions, userId)
 		delete(m.sessions, userId)
 	}
 	}
 
 
-	return m.createNewSessionLocked(userId)
+	return m.createSession(userId)
 }
 }
 
 
 // LoadSession 加载历史会话(恢复 AI 上下文)
 // LoadSession 加载历史会话(恢复 AI 上下文)
@@ -117,74 +120,80 @@ func (m *ACPManager) LoadSession(userId string, sessionId string) (*ACPSession,
 	m.mu.Lock()
 	m.mu.Lock()
 	defer m.mu.Unlock()
 	defer m.mu.Unlock()
 
 
-	// 将当前活跃会话存入历史
+	// 如果已经是当前会话,直接返回
 	if sess, ok := m.sessions[userId]; ok {
 	if sess, ok := m.sessions[userId]; ok {
 		if sess.SessionId == sessionId {
 		if sess.SessionId == sessionId {
-			// 已经是当前会话,直接返回
 			sess.LastActive = time.Now()
 			sess.LastActive = time.Now()
 			return sess, nil
 			return sess, nil
 		}
 		}
-		m.archiveSessionLocked(userId, sess)
+		// 归档当前活跃会话
+		m.archiveToRedis(userId, sess)
 		sess.Client.Close()
 		sess.Client.Close()
 		delete(m.sessions, userId)
 		delete(m.sessions, userId)
 	}
 	}
 
 
-	// 创建新 ACP Client 并加载历史会话
+	// 从 Redis 历史列表中获取标题
+	item := m.getSessionListFromRedisHistory(userId, sessionId)
+	if item == nil {
+		return nil, fmt.Errorf("redis session/load 失败")
+	}
+
+	// 从 Redis 历史列表中移除该会话(因为它即将变为活跃)
+	m.removeFromRedisHistory(userId, sessionId)
+
+	// 启动 ACP 进程并 load 会话
 	client, err := acp.NewClient(m.acpConfig)
 	client, err := acp.NewClient(m.acpConfig)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-
 	if err := client.Initialize(); err != nil {
 	if err := client.Initialize(); err != nil {
 		client.Close()
 		client.Close()
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	// 使用 session/load 恢复会话(含回放历史消息)
-	loadedId, replayMessages, err := client.LoadSession(sessionId, m.serviceDir)
+	loadedId, welcomeMsg, replayMessages, err := client.LoadSession(sessionId, m.serviceDir)
 	if err != nil {
 	if err != nil {
 		client.Close()
 		client.Close()
-		return nil, fmt.Errorf("加载会话失败: %w", err)
+		return nil, fmt.Errorf("session/load 失败: %w", err)
 	}
 	}
 
 
 	zap.L().Info("[AIChat] session/load 回放完成",
 	zap.L().Info("[AIChat] session/load 回放完成",
 		zap.String("sessionId", loadedId),
 		zap.String("sessionId", loadedId),
+		zap.String("welcomeMsg", welcomeMsg),
 		zap.Int("replayCount", len(replayMessages)),
 		zap.Int("replayCount", len(replayMessages)),
 	)
 	)
 
 
-	// 从历史中找到标题
-	title := ""
-	if userHistory, ok := m.history[userId]; ok {
-		for i, item := range userHistory {
-			if item.SessionId == sessionId {
-				title = item.Title
-				// 从历史列表中移除(因为已变为活跃)
-				m.history[userId] = append(userHistory[:i], userHistory[i+1:]...)
-				break
-			}
-		}
-	}
-
 	sess := &ACPSession{
 	sess := &ACPSession{
 		UserId:         userId,
 		UserId:         userId,
-		SessionId:      loadedId,
-		Title:          title,
+		SessionId:      sessionId,
+		Title:          item.Title,
+		WelcomeMessage: welcomeMsg,
 		ReplayMessages: replayMessages,
 		ReplayMessages: replayMessages,
 		Client:         client,
 		Client:         client,
 		LastActive:     time.Now(),
 		LastActive:     time.Now(),
-		CreatedAt:      time.Now(),
+		CreatedAt:      time.UnixMilli(item.CreatedAt),
 	}
 	}
 	client.UserId = userId
 	client.UserId = userId
-
 	m.sessions[userId] = sess
 	m.sessions[userId] = sess
-	zap.L().Info("[AIChat] 会话加载成功", zap.String("userId", userId), zap.String("sessionId", loadedId))
+
+	// 更新 Redis 活跃会话
+	repositories.SaveAiSession(userId, repositories.AiActiveSession{
+		SessionId:  sess.SessionId,
+		Title:      sess.Title,
+		CreatedAt:  sess.CreatedAt.UnixMilli(),
+		LastActive: sess.LastActive.UnixMilli(),
+	})
+
+	zap.L().Info("[AIChat] 会话加载成功", zap.String("userId", userId), zap.String("sessionId", sessionId))
 	return sess, nil
 	return sess, nil
 }
 }
 
 
-// GetSessionList 获取用户的会话列表(含当前活跃 + 历史)
+// ============ 会话列表查询 ============
+
+// GetSessionList 获取用户的会话列表(当前活跃 + Redis 历史)
 func (m *ACPManager) GetSessionList(userId string) []SessionListItem {
 func (m *ACPManager) GetSessionList(userId string) []SessionListItem {
-	m.mu.RLock()
-	defer m.mu.RUnlock()
+	m.mu.Lock()
+	defer m.mu.Unlock()
 
 
 	list := make([]SessionListItem, 0)
 	list := make([]SessionListItem, 0)
 
 
@@ -197,16 +206,43 @@ func (m *ACPManager) GetSessionList(userId string) []SessionListItem {
 			LastActive: sess.LastActive.UnixMilli(),
 			LastActive: sess.LastActive.UnixMilli(),
 			IsActive:   true,
 			IsActive:   true,
 		})
 		})
+	} else {
+		// 内存无活跃会话(后端重启),从 Redis 恢复完整会话到内存
+		activeSess, err := repositories.GetAiSession(userId)
+		if err == nil && activeSess != nil {
+			sess, err := m.startAndLoadSession(userId, activeSess)
+			if err == nil {
+				list = append(list, SessionListItem{
+					SessionId:  sess.SessionId,
+					Title:      sess.Title,
+					CreatedAt:  sess.CreatedAt.UnixMilli(),
+					LastActive: sess.LastActive.UnixMilli(),
+					IsActive:   true,
+				})
+				zap.L().Info("[AIChat] GetSessionList 恢复会话到内存", zap.String("userId", userId), zap.String("sessionId", sess.SessionId))
+			}
+		}
 	}
 	}
 
 
-	// 历史会话
-	if userHistory, ok := m.history[userId]; ok {
-		list = append(list, userHistory...)
+	// 从 Redis 获取历史列表
+	redisItems, err := repositories.GetAiSessionList(userId)
+	if err == nil && len(redisItems) > 0 {
+		for _, item := range redisItems {
+			list = append(list, SessionListItem{
+				SessionId:  item.SessionId,
+				Title:      item.Title,
+				CreatedAt:  item.CreatedAt,
+				LastActive: item.LastActive,
+				IsActive:   false,
+			})
+		}
 	}
 	}
 
 
 	return list
 	return list
 }
 }
 
 
+// ============ 会话操作 ============
+
 // UpdateSessionTitle 更新会话标题
 // UpdateSessionTitle 更新会话标题
 func (m *ACPManager) UpdateSessionTitle(userId string, sessionId string, title string) {
 func (m *ACPManager) UpdateSessionTitle(userId string, sessionId string, title string) {
 	m.mu.Lock()
 	m.mu.Lock()
@@ -215,21 +251,21 @@ func (m *ACPManager) UpdateSessionTitle(userId string, sessionId string, title s
 	// 检查当前活跃会话
 	// 检查当前活跃会话
 	if sess, ok := m.sessions[userId]; ok && sess.SessionId == sessionId {
 	if sess, ok := m.sessions[userId]; ok && sess.SessionId == sessionId {
 		sess.Title = title
 		sess.Title = title
+		// 同步到 Redis 活跃会话 key
+		repositories.SaveAiSession(userId, repositories.AiActiveSession{
+			SessionId:  sess.SessionId,
+			Title:      sess.Title,
+			CreatedAt:  sess.CreatedAt.UnixMilli(),
+			LastActive: sess.LastActive.UnixMilli(),
+		})
 		return
 		return
 	}
 	}
 
 
-	// 检查历史会话
-	if userHistory, ok := m.history[userId]; ok {
-		for i := range userHistory {
-			if userHistory[i].SessionId == sessionId {
-				userHistory[i].Title = title
-				return
-			}
-		}
-	}
+	// 同步到 Redis 历史列表
+	repositories.UpdateAiSessionTitle(userId, sessionId, title)
 }
 }
 
 
-// DeleteSessionFromHistory 从历史列表中删除某会话
+// DeleteSessionFromHistory 删除某会话
 func (m *ACPManager) DeleteSessionFromHistory(userId string, sessionId string) {
 func (m *ACPManager) DeleteSessionFromHistory(userId string, sessionId string) {
 	m.mu.Lock()
 	m.mu.Lock()
 	defer m.mu.Unlock()
 	defer m.mu.Unlock()
@@ -238,78 +274,51 @@ func (m *ACPManager) DeleteSessionFromHistory(userId string, sessionId string) {
 	if sess, ok := m.sessions[userId]; ok && sess.SessionId == sessionId {
 	if sess, ok := m.sessions[userId]; ok && sess.SessionId == sessionId {
 		sess.Client.Close()
 		sess.Client.Close()
 		delete(m.sessions, userId)
 		delete(m.sessions, userId)
+		repositories.DeleteAiSession(userId)
 		return
 		return
 	}
 	}
 
 
-	// 从历史列表中删除
-	if userHistory, ok := m.history[userId]; ok {
-		for i, item := range userHistory {
-			if item.SessionId == sessionId {
-				m.history[userId] = append(userHistory[:i], userHistory[i+1:]...)
-				return
-			}
-		}
-	}
+	// 从 Redis 历史列表中移除
+	m.removeFromRedisHistory(userId, sessionId)
 }
 }
 
 
-// createNewSessionLocked 内部方法:创建新会话(调用时必须持有 mu 锁)
-func (m *ACPManager) createNewSessionLocked(userId string) (*ACPSession, error) {
-	client, err := acp.NewClient(m.acpConfig)
-	if err != nil {
-		return nil, err
-	}
-
-	if err := client.Initialize(); err != nil {
-		client.Close()
-		return nil, err
-	}
-
-	sessionId, welcomeMsg, err := client.NewSession(m.serviceDir)
-	if err != nil {
-		client.Close()
-		return nil, err
-	}
+// DeleteSession 删除当前活跃会话
+func (m *ACPManager) DeleteSession(userId string) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
 
 
-	now := time.Now()
-	sess := &ACPSession{
-		UserId:         userId,
-		SessionId:      sessionId,
-		Title:          "",
-		WelcomeMessage: welcomeMsg,
-		Client:         client,
-		LastActive:     now,
-		CreatedAt:      now,
+	if sess, ok := m.sessions[userId]; ok {
+		sess.Client.Close()
+		delete(m.sessions, userId)
+		repositories.DeleteAiSession(userId)
+		zap.L().Info("[AIChat] 会话已删除", zap.String("userId", userId))
 	}
 	}
-	client.UserId = userId
-
-	m.sessions[userId] = sess
-	zap.L().Info("[AIChat] 会话创建成功", zap.String("userId", userId), zap.String("sessionId", sessionId))
-	return sess, nil
 }
 }
 
 
-// archiveSessionLocked 内部方法:将会话存入历史(调用时必须持有 mu 锁)
-func (m *ACPManager) archiveSessionLocked(userId string, sess *ACPSession) {
-	item := SessionListItem{
-		SessionId:  sess.SessionId,
-		Title:      sess.Title,
-		CreatedAt:  sess.CreatedAt.UnixMilli(),
-		LastActive: sess.LastActive.UnixMilli(),
-		IsActive:   false,
-	}
-
-	if _, ok := m.history[userId]; !ok {
-		m.history[userId] = []SessionListItem{}
-	}
+// GetSessionInfo 获取用户当前活跃会话信息
+func (m *ACPManager) GetSessionInfo(userId string) *ACPSession {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	return m.sessions[userId]
+}
 
 
-	// 插入到历史列表头部(最近的在前)
-	m.history[userId] = append([]SessionListItem{item}, m.history[userId]...)
+// Cancel 取消当前操作
+func (m *ACPManager) Cancel(sess *ACPSession) {
+	sess.Client.Cancel(sess.SessionId)
+}
 
 
-	// 限制历史数量(最多保留 50 个)
-	if len(m.history[userId]) > 50 {
-		m.history[userId] = m.history[userId][:50]
+// ResolvePermission 前端回传权限选择
+func (m *ACPManager) ResolvePermission(userId string, optionId string) error {
+	sess := m.GetSessionInfo(userId)
+	if sess == nil {
+		return fmt.Errorf("没有活跃的会话")
 	}
 	}
+	sess.Client.ResolvePermission(optionId)
+	return nil
 }
 }
 
 
+// ============ 消息发送 ============
+
 // Prompt 发送消息并流式读取响应
 // Prompt 发送消息并流式读取响应
 func (m *ACPManager) Prompt(sess *ACPSession, message string, eventCh chan<- AIChatEvent) {
 func (m *ACPManager) Prompt(sess *ACPSession, message string, eventCh chan<- AIChatEvent) {
 	sess.Mutex.Lock()
 	sess.Mutex.Lock()
@@ -354,55 +363,165 @@ func (m *ACPManager) Prompt(sess *ACPSession, message string, eventCh chan<- AIC
 	close(eventCh)
 	close(eventCh)
 }
 }
 
 
-// Cancel 取消当前操作
-func (m *ACPManager) Cancel(sess *ACPSession) {
-	sess.Client.Cancel(sess.SessionId)
-}
+// ============ 内部方法 ============
 
 
-// ResolvePermission 前端回传权限选择
-func (m *ACPManager) ResolvePermission(userId string, optionId string) error {
-	sess := m.GetSessionInfo(userId)
-	if sess == nil {
-		return fmt.Errorf("没有活跃的会话")
+// createSession 创建新 ACP 会话
+func (m *ACPManager) createSession(userId string) (*ACPSession, error) {
+	client, err := acp.NewClient(m.acpConfig)
+	if err != nil {
+		return nil, err
 	}
 	}
-	sess.Client.ResolvePermission(optionId)
-	return nil
+	if err := client.Initialize(); err != nil {
+		client.Close()
+		return nil, err
+	}
+
+	sessionId, welcomeMsg, err := client.NewSession(m.serviceDir)
+	if err != nil {
+		client.Close()
+		return nil, err
+	}
+
+	now := time.Now()
+	sess := &ACPSession{
+		UserId:         userId,
+		SessionId:      sessionId,
+		Title:          "",
+		WelcomeMessage: welcomeMsg,
+		Client:         client,
+		LastActive:     now,
+		CreatedAt:      now,
+	}
+	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(),
+	})
+
+	zap.L().Info("[AIChat] 会话创建成功", zap.String("userId", userId), zap.String("sessionId", sessionId))
+	return sess, nil
 }
 }
 
 
-// DeleteSession 删除指定用户的当前活跃会话
-func (m *ACPManager) DeleteSession(userId string) {
-	m.mu.Lock()
-	defer m.mu.Unlock()
+// startAndLoadSession 启动 ACP 进程并 load 指定会话(用于 Redis 恢复)
+func (m *ACPManager) startAndLoadSession(userId string, active *repositories.AiActiveSession) (*ACPSession, error) {
+	client, err := acp.NewClient(m.acpConfig)
+	if err != nil {
+		return nil, err
+	}
+	if err := client.Initialize(); err != nil {
+		client.Close()
+		return nil, err
+	}
 
 
-	if sess, ok := m.sessions[userId]; ok {
-		sess.Client.Close()
-		delete(m.sessions, userId)
-		zap.L().Info("[AIChat] 会话已删除", zap.String("userId", userId))
+	_, welcomeMsg, _, err := client.LoadSession(active.SessionId, m.serviceDir)
+	if err != nil {
+		client.Close()
+		return nil, fmt.Errorf("session/load 失败: %w", err)
+	}
+
+	now := time.Now()
+	sess := &ACPSession{
+		UserId:         userId,
+		SessionId:      active.SessionId,
+		Title:          active.Title,
+		WelcomeMessage: welcomeMsg,
+		Client:         client,
+		LastActive:     now,
+		CreatedAt:      time.UnixMilli(active.CreatedAt),
 	}
 	}
+	client.UserId = userId
+	m.sessions[userId] = sess
+	return sess, nil
 }
 }
 
 
-// GetSessionInfo 获取用户会话信息
-func (m *ACPManager) GetSessionInfo(userId string) *ACPSession {
-	m.mu.RLock()
-	defer m.mu.RUnlock()
-	return m.sessions[userId]
+// archiveToRedis 将会话归档到 Redis 历史列表
+func (m *ACPManager) archiveToRedis(userId string, sess *ACPSession) {
+	item := repositories.AiSessionItem{
+		SessionId:  sess.SessionId,
+		Title:      sess.Title,
+		CreatedAt:  sess.CreatedAt.UnixMilli(),
+		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)
+			}
+		}()
+	}
+
+	if err := repositories.SaveAiSessionList(userId, list); err != nil {
+		zap.L().Warn("[AIChat] 归档会话到Redis失败", zap.String("userId", userId), zap.Error(err))
+	}
 }
 }
 
 
-// CleanupIdle 清理空闲超时的会话
-func (m *ACPManager) CleanupIdle() {
-	m.mu.Lock()
-	defer m.mu.Unlock()
+// archiveActiveSessionFromRedis 将 Redis session key 中的活跃会话归档到历史列表
+// 用于后端重启后、内存中无活跃会话时的归档保护
+// removeFromRedisHistory 从 Redis 历史列表中移除指定会话
+func (m *ACPManager) removeFromRedisHistory(userId string, sessionId string) {
+	list, err := repositories.GetAiSessionList(userId)
+	if err != nil || len(list) == 0 {
+		return
+	}
 
 
-	now := time.Now()
-	for userId, sess := range m.sessions {
-		if now.Sub(sess.LastActive) > m.maxIdle {
-			sess.Client.Close()
-			delete(m.sessions, userId)
-			zap.L().Info("[AIChat] 清理空闲会话", zap.String("userId", userId))
+	for i := range list {
+		if list[i].SessionId == sessionId {
+			list = append(list[:i], list[i+1:]...)
+			repositories.SaveAiSessionList(userId, list)
+			return
+		}
+	}
+}
+
+// getSessionListFromRedisHistory 从 Redis 历史列表中获取会话
+func (m *ACPManager) getSessionListFromRedisHistory(userId string, sessionId string) *repositories.AiSessionItem {
+	list, err := repositories.GetAiSessionList(userId)
+	if err != nil {
+		zap.L().Warn("[AIChat] 从redis读取AiSessionList失败", zap.String("err", err.Error()))
+		return nil
+	}
+	for _, item := range list {
+		if item.SessionId == sessionId {
+			return &item
 		}
 		}
 	}
 	}
+	return nil
+}
+
+// deleteKiroSession 通过 kiro-cli 命令删除会话
+func (m *ACPManager) deleteKiroSession(sessionId string) {
+	cmd := exec.Command("kiro-cli", "chat", "--delete-session", sessionId)
+	cmd.Dir = m.serviceDir
+	if output, err := cmd.CombinedOutput(); err != nil {
+		zap.L().Warn("[AIChat] 删除Kiro会话失败",
+			zap.String("sessionId", sessionId),
+			zap.Error(err),
+			zap.String("output", string(output)),
+		)
+	} else {
+		zap.L().Info("[AIChat] 删除Kiro会话成功", zap.String("sessionId", sessionId))
+	}
 }
 }
 
 
+// ============ 工具函数 ============
+
 func toJSON(v any) string {
 func toJSON(v any) string {
 	data, _ := json.Marshal(v)
 	data, _ := json.Marshal(v)
 	return string(data)
 	return string(data)

+ 22 - 4
service/utils/acp/acp.go

@@ -14,6 +14,7 @@ import (
 	"sync/atomic"
 	"sync/atomic"
 
 
 	"fwh-toolbox-server/utils/filestore"
 	"fwh-toolbox-server/utils/filestore"
+
 	"go.uber.org/zap"
 	"go.uber.org/zap"
 )
 )
 
 
@@ -317,6 +318,8 @@ func (c *Client) NewSession(cwd string) (string, string, error) {
 		return "", "", fmt.Errorf("解析 session/new 响应失败: %w", err)
 		return "", "", fmt.Errorf("解析 session/new 响应失败: %w", err)
 	}
 	}
 
 
+	zap.L().Info("session/new", zap.String("result", string(result)))
+
 	c.sessionId = res.SessionId
 	c.sessionId = res.SessionId
 
 
 	// 提取当前 mode 的 welcomeMessage
 	// 提取当前 mode 的 welcomeMessage
@@ -341,7 +344,7 @@ type ReplayMessage struct {
 }
 }
 
 
 // LoadSession 加载已有会话(恢复历史对话上下文),返回回放的消息列表
 // LoadSession 加载已有会话(恢复历史对话上下文),返回回放的消息列表
-func (c *Client) LoadSession(sessionId string, cwd string) (string, []ReplayMessage, error) {
+func (c *Client) LoadSession(sessionId string, cwd string) (string, string, []ReplayMessage, error) {
 	params := map[string]any{
 	params := map[string]any{
 		"sessionId":  sessionId,
 		"sessionId":  sessionId,
 		"cwd":        cwd,
 		"cwd":        cwd,
@@ -382,14 +385,29 @@ func (c *Client) LoadSession(sessionId string, cwd string) (string, []ReplayMess
 			if numId == id {
 			if numId == id {
 				// session/load 完成
 				// session/load 完成
 				if msg.Error != nil {
 				if msg.Error != nil {
-					return "", nil, fmt.Errorf("session/load 错误: %s", msg.Error.Message)
+					return "", "", nil, fmt.Errorf("session/load 错误: %s", msg.Error.Message)
 				}
 				}
 				c.sessionId = sessionId
 				c.sessionId = sessionId
 				// 追加最后一条未关闭的消息
 				// 追加最后一条未关闭的消息
 				if currentMsg.Content != "" {
 				if currentMsg.Content != "" {
 					replayMessages = append(replayMessages, *currentMsg)
 					replayMessages = append(replayMessages, *currentMsg)
 				}
 				}
-				return sessionId, replayMessages, nil
+
+				// 解析响应中的 welcomeMessage
+				var welcomeMsg string
+				if len(msg.Result) > 0 {
+					var res NewSessionResult
+					if err := json.Unmarshal(msg.Result, &res); err == nil && res.Modes != nil {
+						for _, mode := range res.Modes.AvailableModes {
+							if mode.Id == res.Modes.CurrentModeId && mode.Meta != nil {
+								welcomeMsg = mode.Meta.WelcomeMessage
+								break
+							}
+						}
+					}
+				}
+
+				return sessionId, welcomeMsg, replayMessages, nil
 			}
 			}
 		}
 		}
 
 
@@ -444,7 +462,7 @@ func (c *Client) LoadSession(sessionId string, cwd string) (string, []ReplayMess
 		// 忽略其他通知(kiro 扩展等)
 		// 忽略其他通知(kiro 扩展等)
 	}
 	}
 
 
-	return "", nil, fmt.Errorf("session/load 未收到响应(进程可能已退出)")
+	return "", "", nil, fmt.Errorf("session/load 未收到响应(进程可能已退出)")
 }
 }
 
 
 // Prompt 发送消息并流式读取事件
 // Prompt 发送消息并流式读取事件

+ 51 - 9
src/layout/header/Actions/AiChat/ChatPanel.vue

@@ -23,12 +23,15 @@ const {
   deleteSession,
   deleteSession,
   handleUploadFile,
   handleUploadFile,
   removePendingFile,
   removePendingFile,
+  renameSession,
 } = useAiChat()
 } = useAiChat()
 
 
 const inputText = ref('')
 const inputText = ref('')
 const scrollRef = ref<InstanceType<typeof NScrollbar>>()
 const scrollRef = ref<InstanceType<typeof NScrollbar>>()
 const showSidebar = ref(false)
 const showSidebar = ref(false)
 const fileInputRef = ref<HTMLInputElement>()
 const fileInputRef = ref<HTMLInputElement>()
+const editingSessionId = ref('')
+const editingTitle = ref('')
 
 
 onMounted(() => {
 onMounted(() => {
   fetchSession()
   fetchSession()
@@ -72,6 +75,25 @@ function onFileSelected(e: Event) {
   input.value = ''
   input.value = ''
 }
 }
 
 
+function startRename(sessionId: string, currentTitle: string) {
+  editingSessionId.value = sessionId
+  editingTitle.value = currentTitle || ''
+}
+
+function confirmRename(sessionId: string) {
+  const title = editingTitle.value.trim()
+  if (title) {
+    renameSession(sessionId, title)
+  }
+  editingSessionId.value = ''
+  editingTitle.value = ''
+}
+
+function cancelRename() {
+  editingSessionId.value = ''
+  editingTitle.value = ''
+}
+
 function handleNewSession() {
 function handleNewSession() {
   createNewSession()
   createNewSession()
   showSidebar.value = false
   showSidebar.value = false
@@ -137,17 +159,37 @@ function formatTime(ts: number): string {
               @click="handleLoadSession(sess.sessionId)"
               @click="handleLoadSession(sess.sessionId)"
             >
             >
               <div class="flex items-center justify-between">
               <div class="flex items-center justify-between">
-                <span class="flex-1 truncate text-neutral-700 dark:text-neutral-300">
+                <!-- 编辑模式 -->
+                <input
+                  v-if="editingSessionId === sess.sessionId"
+                  v-model="editingTitle"
+                  class="flex-1 rounded border border-blue-300 bg-white px-1 text-xs outline-none dark:border-blue-600 dark:bg-neutral-700"
+                  @click.stop
+                  @keydown.enter.stop="confirmRename(sess.sessionId)"
+                  @keydown.escape.stop="cancelRename"
+                  @blur="confirmRename(sess.sessionId)"
+                >
+                <!-- 显示模式 -->
+                <span
+                  v-else
+                  class="flex-1 truncate text-neutral-700 dark:text-neutral-300"
+                  @dblclick.stop="startRename(sess.sessionId, sess.title)"
+                >
                   {{ sess.title || '新对话' }}
                   {{ sess.title || '新对话' }}
                 </span>
                 </span>
-                <NPopconfirm @positive-click.stop="deleteSession(sess.sessionId)">
-                  <template #trigger>
-                    <NButton text size="tiny" class="hidden! group-hover:block!" @click.stop>
-                      <span class="i-ph:trash text-xs text-neutral-400" />
-                    </NButton>
-                  </template>
-                  删除此对话?
-                </NPopconfirm>
+                <div class="hidden items-center gap-0.5 group-hover:flex">
+                  <NButton text size="tiny" title="重命名" @click.stop="startRename(sess.sessionId, sess.title)">
+                    <span class="i-ph:pencil-simple text-xs text-neutral-400" />
+                  </NButton>
+                  <NPopconfirm @positive-click.stop="deleteSession(sess.sessionId)">
+                    <template #trigger>
+                      <NButton text size="tiny" @click.stop>
+                        <span class="i-ph:trash text-xs text-neutral-400" />
+                      </NButton>
+                    </template>
+                    删除此对话?
+                  </NPopconfirm>
+                </div>
               </div>
               </div>
               <div class="mt-0.5 text-neutral-400 text-[10px]">
               <div class="mt-0.5 text-neutral-400 text-[10px]">
                 {{ formatTime(sess.lastActive) }}
                 {{ formatTime(sess.lastActive) }}

+ 53 - 13
src/layout/header/Actions/AiChat/useAiChat.ts

@@ -3,7 +3,6 @@ import type { ChatMessage, ToolCallInfo, SessionListItem, UploadedFile } from '.
 import { useUserStore } from '@/stores/user'
 import { useUserStore } from '@/stores/user'
 import { chatDB } from './chatDB'
 import { chatDB } from './chatDB'
 import {
 import {
-  getAiSession,
   getAiSessionList,
   getAiSessionList,
   createAiSession,
   createAiSession,
   loadAiSession,
   loadAiSession,
@@ -11,6 +10,7 @@ import {
   cancelAiChat,
   cancelAiChat,
   resolveAiPermission,
   resolveAiPermission,
   uploadAiFile,
   uploadAiFile,
+  updateAiSessionTitle,
 } from '@/service/aiChat'
 } from '@/service/aiChat'
 
 
 export function useAiChat() {
 export function useAiChat() {
@@ -54,19 +54,38 @@ export function useAiChat() {
     }
     }
   }
   }
 
 
-  // 获取当前会话信息
+  // 获取当前会话信息(首次打开时调用)
   async function fetchSession() {
   async function fetchSession() {
     isLoadingSession.value = true
     isLoadingSession.value = true
     try {
     try {
-      const data = await getAiSession()
-      if (data?.welcomeMessage) {
-        welcomeMessage.value = data.welcomeMessage
+      // 先查询会话列表
+      const list = await getAiSessionList()
+      if (list) {
+        sessionList.value = list
       }
       }
-      if (data?.sessionId) {
-        currentSessionId.value = data.sessionId
+
+      // 如果存在已激活会话,直接使用
+      const activeSession = list?.find(s => s.isActive)
+      if (activeSession) {
+        currentSessionId.value = activeSession.sessionId
         // 从 IndexedDB 恢复本地消息
         // 从 IndexedDB 恢复本地消息
-        await loadLocalMessages(data.sessionId)
+        await loadLocalMessages(activeSession.sessionId)
+        return
+      }
+
+      // 没有激活会话但列表中有历史会话:加载最近的一个
+      if (list && list.length > 0) {
+        const latest = list[0]
+        currentSessionId.value = latest.sessionId
+        await loadLocalMessages(latest.sessionId)
+        // 后台恢复 Kiro 上下文
+        await loadAiSession(latest.sessionId)
+        await fetchSessionList()
+        return
       }
       }
+
+      // 列表完全为空:创建新会话
+      await createNewSession()
     }
     }
     catch {
     catch {
       // 忽略
       // 忽略
@@ -74,8 +93,6 @@ export function useAiChat() {
     finally {
     finally {
       isLoadingSession.value = false
       isLoadingSession.value = false
     }
     }
-    // 同时获取会话列表
-    await fetchSessionList()
   }
   }
 
 
   // 创建新会话
   // 创建新会话
@@ -122,8 +139,8 @@ export function useAiChat() {
         messages.value = cached.messages
         messages.value = cached.messages
         welcomeMessage.value = ''
         welcomeMessage.value = ''
 
 
-        // 后台静默调用 session/load 恢复 Kiro 上下文(不等待回放结果)
-        loadAiSession(sessionId)
+        // 调用 session/load 恢复 Kiro 上下文,等待完成后再刷新列表
+        await loadAiSession(sessionId)
         await fetchSessionList()
         await fetchSessionList()
       }
       }
       else {
       else {
@@ -138,7 +155,7 @@ export function useAiChat() {
             messages.value = (data as any).replayMessages.map((m: any, i: number) => ({
             messages.value = (data as any).replayMessages.map((m: any, i: number) => ({
               id: m.messageId || `replay_${i}`,
               id: m.messageId || `replay_${i}`,
               role: m.role,
               role: m.role,
-              content: m.content,
+              content: stripContextPrefix(m.content, m.role),
               timestamp: Date.now() - ((data as any).replayMessages.length - i) * 1000,
               timestamp: Date.now() - ((data as any).replayMessages.length - i) * 1000,
             }))
             }))
             // 保存到缓存
             // 保存到缓存
@@ -184,6 +201,21 @@ export function useAiChat() {
     }
     }
   }
   }
 
 
+  // 重命名会话
+  async function renameSession(sessionId: string, title: string) {
+    try {
+      await updateAiSessionTitle(sessionId, title)
+      // 更新本地列表中的标题
+      const item = sessionList.value.find(s => s.sessionId === sessionId)
+      if (item) {
+        item.title = title
+      }
+    }
+    catch {
+      // 忽略
+    }
+  }
+
   // ============ IndexedDB 本地消息管理 ============
   // ============ IndexedDB 本地消息管理 ============
 
 
   async function saveLocalMessages() {
   async function saveLocalMessages() {
@@ -423,6 +455,13 @@ export function useAiChat() {
     }
     }
   }
   }
 
 
+  // 去除回放消息中注入的 [context] 前缀
+  function stripContextPrefix(content: string, role: string): string {
+    if (role !== 'user') return content
+    // 匹配 "[context] userId=xxx, sessionId=xxx\n\n" 前缀
+    return content.replace(/^\[context\] userId=.*?, sessionId=.*?\n\n/, '')
+  }
+
   // 最终扫描:处理跨 chunk 拆分导致实时匹配遗漏的标记
   // 最终扫描:处理跨 chunk 拆分导致实时匹配遗漏的标记
   function extractFileDownloads(aiMessage: ChatMessage) {
   function extractFileDownloads(aiMessage: ChatMessage) {
     const fileRegex = /\$\$FILE_DOWNLOAD:(.*?)\$\$/g
     const fileRegex = /\$\$FILE_DOWNLOAD:(.*?)\$\$/g
@@ -496,5 +535,6 @@ export function useAiChat() {
     deleteSession,
     deleteSession,
     handleUploadFile,
     handleUploadFile,
     removePendingFile,
     removePendingFile,
+    renameSession,
   }
   }
 }
 }

+ 1 - 7
src/service/aiChat/index.ts

@@ -12,11 +12,10 @@
 // /ai/files/download    - GET   下载文件
 // /ai/files/download    - GET   下载文件
 
 
 import http from '@/service/serverReq'
 import http from '@/service/serverReq'
-import type { AIChatSession, SessionListItem } from '@/layout/header/Actions/AiChat/interface'
+import type { SessionListItem } from '@/layout/header/Actions/AiChat/interface'
 
 
 const API = {
 const API = {
   chat: '/ai/chat',
   chat: '/ai/chat',
-  sessions: '/ai/sessions',
   sessionList: '/ai/sessions/list',
   sessionList: '/ai/sessions/list',
   sessionNew: '/ai/sessions/new',
   sessionNew: '/ai/sessions/new',
   sessionLoad: (id: string) => `/ai/sessions/${id}/load`,
   sessionLoad: (id: string) => `/ai/sessions/${id}/load`,
@@ -28,11 +27,6 @@ const API = {
   fileDownload: '/ai/files/download',
   fileDownload: '/ai/files/download',
 }
 }
 
 
-// GET 获取当前会话信息
-export const getAiSession = async (): Promise<AIChatSession> => {
-  return await http.get<AIChatSession>(API.sessions)
-}
-
 // GET 获取会话历史列表
 // GET 获取会话历史列表
 export const getAiSessionList = async (): Promise<SessionListItem[]> => {
 export const getAiSessionList = async (): Promise<SessionListItem[]> => {
   return await http.get<SessionListItem[]>(API.sessionList)
   return await http.get<SessionListItem[]>(API.sessionList)