|
@@ -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)
|