aiChatServices.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. package services
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "fwh-toolbox-server/utils/acp"
  6. "sync"
  7. "time"
  8. "go.uber.org/zap"
  9. )
  10. // AIChatEvent SSE 事件(输出给前端)
  11. type AIChatEvent struct {
  12. Type string `json:"type"` // text, tool_call, tool_update, done, error
  13. Content string `json:"content,omitempty"` // 文本内容
  14. ToolName string `json:"name,omitempty"` // 工具名
  15. Status string `json:"status,omitempty"` // 工具状态
  16. SessionId string `json:"sessionId,omitempty"` // 会话ID
  17. StopReason string `json:"stopReason,omitempty"`
  18. }
  19. // ACPSession 单个用户的 ACP 会话
  20. type ACPSession struct {
  21. UserId string
  22. SessionId string
  23. Title string
  24. WelcomeMessage string
  25. ReplayMessages []acp.ReplayMessage // session/load 回放的历史消息
  26. Client *acp.Client
  27. Mutex sync.Mutex
  28. LastActive time.Time
  29. CreatedAt time.Time
  30. }
  31. // SessionListItem 会话列表项(返回给前端)
  32. type SessionListItem struct {
  33. SessionId string `json:"sessionId"`
  34. Title string `json:"title"`
  35. CreatedAt int64 `json:"createdAt"`
  36. LastActive int64 `json:"lastActive"`
  37. IsActive bool `json:"isActive"` // 是否为当前激活的会话
  38. }
  39. // ACPManager 管理所有用户的 ACP 会话
  40. type ACPManager struct {
  41. sessions map[string]*ACPSession // key: userId -> 当前活跃会话
  42. history map[string][]SessionListItem // key: userId -> 历史会话列表(元数据)
  43. mu sync.RWMutex
  44. maxIdle time.Duration
  45. serviceDir string
  46. acpConfig acp.ClientConfig
  47. }
  48. // NewACPManager 创建 ACP 管理器
  49. func NewACPManager(serviceDir string, env []string) *ACPManager {
  50. mgr := &ACPManager{
  51. sessions: make(map[string]*ACPSession),
  52. history: make(map[string][]SessionListItem),
  53. maxIdle: 30 * time.Minute,
  54. serviceDir: serviceDir,
  55. acpConfig: acp.ClientConfig{
  56. Command: "kiro-cli",
  57. Args: []string{"acp", "--agent", "fwh-toolbox-agent"},
  58. WorkingDir: serviceDir,
  59. Env: env,
  60. },
  61. }
  62. // 定时清理空闲会话
  63. go func() {
  64. ticker := time.NewTicker(5 * time.Minute)
  65. defer ticker.Stop()
  66. for range ticker.C {
  67. mgr.CleanupIdle()
  68. }
  69. }()
  70. return mgr
  71. }
  72. // GetOrCreateSession 获取或创建用户的 ACP 会话
  73. func (m *ACPManager) GetOrCreateSession(userId string) (*ACPSession, error) {
  74. m.mu.Lock()
  75. defer m.mu.Unlock()
  76. if sess, ok := m.sessions[userId]; ok {
  77. if sess.Client.IsAlive() {
  78. sess.LastActive = time.Now()
  79. return sess, nil
  80. }
  81. // 进程已退出,清理后重新创建
  82. sess.Client.Close()
  83. delete(m.sessions, userId)
  84. }
  85. return m.createNewSessionLocked(userId)
  86. }
  87. // CreateNewSession 强制创建一个新会话(保留旧会话到历史列表)
  88. func (m *ACPManager) CreateNewSession(userId string) (*ACPSession, error) {
  89. m.mu.Lock()
  90. defer m.mu.Unlock()
  91. // 将当前活跃会话存入历史
  92. if sess, ok := m.sessions[userId]; ok {
  93. m.archiveSessionLocked(userId, sess)
  94. sess.Client.Close()
  95. delete(m.sessions, userId)
  96. }
  97. return m.createNewSessionLocked(userId)
  98. }
  99. // LoadSession 加载历史会话(恢复 AI 上下文)
  100. func (m *ACPManager) LoadSession(userId string, sessionId string) (*ACPSession, error) {
  101. m.mu.Lock()
  102. defer m.mu.Unlock()
  103. // 将当前活跃会话存入历史
  104. if sess, ok := m.sessions[userId]; ok {
  105. if sess.SessionId == sessionId {
  106. // 已经是当前会话,直接返回
  107. sess.LastActive = time.Now()
  108. return sess, nil
  109. }
  110. m.archiveSessionLocked(userId, sess)
  111. sess.Client.Close()
  112. delete(m.sessions, userId)
  113. }
  114. // 创建新 ACP Client 并加载历史会话
  115. client, err := acp.NewClient(m.acpConfig)
  116. if err != nil {
  117. return nil, err
  118. }
  119. if err := client.Initialize(); err != nil {
  120. client.Close()
  121. return nil, err
  122. }
  123. // 使用 session/load 恢复会话(含回放历史消息)
  124. loadedId, replayMessages, err := client.LoadSession(sessionId, m.serviceDir)
  125. if err != nil {
  126. client.Close()
  127. return nil, fmt.Errorf("加载会话失败: %w", err)
  128. }
  129. zap.L().Info("[AIChat] session/load 回放完成",
  130. zap.String("sessionId", loadedId),
  131. zap.Int("replayCount", len(replayMessages)),
  132. )
  133. // 从历史中找到标题
  134. title := ""
  135. if userHistory, ok := m.history[userId]; ok {
  136. for i, item := range userHistory {
  137. if item.SessionId == sessionId {
  138. title = item.Title
  139. // 从历史列表中移除(因为已变为活跃)
  140. m.history[userId] = append(userHistory[:i], userHistory[i+1:]...)
  141. break
  142. }
  143. }
  144. }
  145. sess := &ACPSession{
  146. UserId: userId,
  147. SessionId: loadedId,
  148. Title: title,
  149. ReplayMessages: replayMessages,
  150. Client: client,
  151. LastActive: time.Now(),
  152. CreatedAt: time.Now(),
  153. }
  154. client.UserId = userId
  155. m.sessions[userId] = sess
  156. zap.L().Info("[AIChat] 会话加载成功", zap.String("userId", userId), zap.String("sessionId", loadedId))
  157. return sess, nil
  158. }
  159. // GetSessionList 获取用户的会话列表(含当前活跃 + 历史)
  160. func (m *ACPManager) GetSessionList(userId string) []SessionListItem {
  161. m.mu.RLock()
  162. defer m.mu.RUnlock()
  163. list := make([]SessionListItem, 0)
  164. // 当前活跃会话排在最前
  165. if sess, ok := m.sessions[userId]; ok {
  166. list = append(list, SessionListItem{
  167. SessionId: sess.SessionId,
  168. Title: sess.Title,
  169. CreatedAt: sess.CreatedAt.UnixMilli(),
  170. LastActive: sess.LastActive.UnixMilli(),
  171. IsActive: true,
  172. })
  173. }
  174. // 历史会话
  175. if userHistory, ok := m.history[userId]; ok {
  176. list = append(list, userHistory...)
  177. }
  178. return list
  179. }
  180. // UpdateSessionTitle 更新会话标题
  181. func (m *ACPManager) UpdateSessionTitle(userId string, sessionId string, title string) {
  182. m.mu.Lock()
  183. defer m.mu.Unlock()
  184. // 检查当前活跃会话
  185. if sess, ok := m.sessions[userId]; ok && sess.SessionId == sessionId {
  186. sess.Title = title
  187. return
  188. }
  189. // 检查历史会话
  190. if userHistory, ok := m.history[userId]; ok {
  191. for i := range userHistory {
  192. if userHistory[i].SessionId == sessionId {
  193. userHistory[i].Title = title
  194. return
  195. }
  196. }
  197. }
  198. }
  199. // DeleteSessionFromHistory 从历史列表中删除某会话
  200. func (m *ACPManager) DeleteSessionFromHistory(userId string, sessionId string) {
  201. m.mu.Lock()
  202. defer m.mu.Unlock()
  203. // 如果是当前活跃会话,关闭它
  204. if sess, ok := m.sessions[userId]; ok && sess.SessionId == sessionId {
  205. sess.Client.Close()
  206. delete(m.sessions, userId)
  207. return
  208. }
  209. // 从历史列表中删除
  210. if userHistory, ok := m.history[userId]; ok {
  211. for i, item := range userHistory {
  212. if item.SessionId == sessionId {
  213. m.history[userId] = append(userHistory[:i], userHistory[i+1:]...)
  214. return
  215. }
  216. }
  217. }
  218. }
  219. // createNewSessionLocked 内部方法:创建新会话(调用时必须持有 mu 锁)
  220. func (m *ACPManager) createNewSessionLocked(userId string) (*ACPSession, error) {
  221. client, err := acp.NewClient(m.acpConfig)
  222. if err != nil {
  223. return nil, err
  224. }
  225. if err := client.Initialize(); err != nil {
  226. client.Close()
  227. return nil, err
  228. }
  229. sessionId, welcomeMsg, err := client.NewSession(m.serviceDir)
  230. if err != nil {
  231. client.Close()
  232. return nil, err
  233. }
  234. now := time.Now()
  235. sess := &ACPSession{
  236. UserId: userId,
  237. SessionId: sessionId,
  238. Title: "",
  239. WelcomeMessage: welcomeMsg,
  240. Client: client,
  241. LastActive: now,
  242. CreatedAt: now,
  243. }
  244. client.UserId = userId
  245. m.sessions[userId] = sess
  246. zap.L().Info("[AIChat] 会话创建成功", zap.String("userId", userId), zap.String("sessionId", sessionId))
  247. return sess, nil
  248. }
  249. // archiveSessionLocked 内部方法:将会话存入历史(调用时必须持有 mu 锁)
  250. func (m *ACPManager) archiveSessionLocked(userId string, sess *ACPSession) {
  251. item := SessionListItem{
  252. SessionId: sess.SessionId,
  253. Title: sess.Title,
  254. CreatedAt: sess.CreatedAt.UnixMilli(),
  255. LastActive: sess.LastActive.UnixMilli(),
  256. IsActive: false,
  257. }
  258. if _, ok := m.history[userId]; !ok {
  259. m.history[userId] = []SessionListItem{}
  260. }
  261. // 插入到历史列表头部(最近的在前)
  262. m.history[userId] = append([]SessionListItem{item}, m.history[userId]...)
  263. // 限制历史数量(最多保留 50 个)
  264. if len(m.history[userId]) > 50 {
  265. m.history[userId] = m.history[userId][:50]
  266. }
  267. }
  268. // Prompt 发送消息并流式读取响应
  269. func (m *ACPManager) Prompt(sess *ACPSession, message string, eventCh chan<- AIChatEvent) {
  270. sess.Mutex.Lock()
  271. defer sess.Mutex.Unlock()
  272. sess.LastActive = time.Now()
  273. // 首次发消息时自动设置标题(取消息前 30 字)
  274. if sess.Title == "" && message != "" {
  275. title := message
  276. if len([]rune(title)) > 30 {
  277. title = string([]rune(title)[:30]) + "..."
  278. }
  279. sess.Title = title
  280. }
  281. // 用 acp.Client.Prompt 流式读取
  282. acpEvents := make(chan acp.Event, 50)
  283. go sess.Client.Prompt(sess.SessionId, message, acpEvents)
  284. // 转换 acp.Event → AIChatEvent
  285. for event := range acpEvents {
  286. switch event.Type {
  287. case "text":
  288. eventCh <- AIChatEvent{Type: "text", Content: event.Content}
  289. case "tool_call":
  290. eventCh <- AIChatEvent{Type: "tool_call", ToolName: event.ToolName, Status: event.ToolStatus}
  291. case "tool_update":
  292. eventCh <- AIChatEvent{Type: "tool_update", ToolName: event.ToolName, Status: event.ToolStatus}
  293. case "permission":
  294. eventCh <- AIChatEvent{Type: "permission", Content: toJSON(event.Permission)}
  295. case "file_created":
  296. eventCh <- AIChatEvent{Type: "file_created", Content: toJSON(event.File)}
  297. case "done":
  298. eventCh <- AIChatEvent{Type: "done", SessionId: event.SessionId, StopReason: event.StopReason}
  299. case "error":
  300. eventCh <- AIChatEvent{Type: "error", Content: event.Content}
  301. }
  302. }
  303. close(eventCh)
  304. }
  305. // Cancel 取消当前操作
  306. func (m *ACPManager) Cancel(sess *ACPSession) {
  307. sess.Client.Cancel(sess.SessionId)
  308. }
  309. // ResolvePermission 前端回传权限选择
  310. func (m *ACPManager) ResolvePermission(userId string, optionId string) error {
  311. sess := m.GetSessionInfo(userId)
  312. if sess == nil {
  313. return fmt.Errorf("没有活跃的会话")
  314. }
  315. sess.Client.ResolvePermission(optionId)
  316. return nil
  317. }
  318. // DeleteSession 删除指定用户的当前活跃会话
  319. func (m *ACPManager) DeleteSession(userId string) {
  320. m.mu.Lock()
  321. defer m.mu.Unlock()
  322. if sess, ok := m.sessions[userId]; ok {
  323. sess.Client.Close()
  324. delete(m.sessions, userId)
  325. zap.L().Info("[AIChat] 会话已删除", zap.String("userId", userId))
  326. }
  327. }
  328. // GetSessionInfo 获取用户会话信息
  329. func (m *ACPManager) GetSessionInfo(userId string) *ACPSession {
  330. m.mu.RLock()
  331. defer m.mu.RUnlock()
  332. return m.sessions[userId]
  333. }
  334. // CleanupIdle 清理空闲超时的会话
  335. func (m *ACPManager) CleanupIdle() {
  336. m.mu.Lock()
  337. defer m.mu.Unlock()
  338. now := time.Now()
  339. for userId, sess := range m.sessions {
  340. if now.Sub(sess.LastActive) > m.maxIdle {
  341. sess.Client.Close()
  342. delete(m.sessions, userId)
  343. zap.L().Info("[AIChat] 清理空闲会话", zap.String("userId", userId))
  344. }
  345. }
  346. }
  347. func toJSON(v any) string {
  348. data, _ := json.Marshal(v)
  349. return string(data)
  350. }