aiChatServices.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. package services
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "fwh-toolbox-server/repositories"
  6. "fwh-toolbox-server/utils/acp"
  7. "os/exec"
  8. "sync"
  9. "time"
  10. "go.uber.org/zap"
  11. )
  12. // AIChatEvent SSE 事件(输出给前端)
  13. type AIChatEvent struct {
  14. Type string `json:"type"` // text, tool_call, tool_update, done, error
  15. Content string `json:"content,omitempty"` // 文本内容
  16. ToolName string `json:"name,omitempty"` // 工具名
  17. Status string `json:"status,omitempty"` // 工具状态
  18. SessionId string `json:"sessionId,omitempty"` // 会话ID
  19. StopReason string `json:"stopReason,omitempty"`
  20. }
  21. // ACPSession 单个用户的 ACP 会话
  22. type ACPSession struct {
  23. UserId string
  24. SessionId string
  25. Title string
  26. WelcomeMessage string
  27. ReplayMessages []acp.ReplayMessage // session/load 回放的历史消息
  28. Client *acp.Client
  29. Mutex sync.Mutex
  30. LastActive time.Time
  31. CreatedAt time.Time
  32. }
  33. // SessionListItem 会话列表项(返回给前端)
  34. type SessionListItem struct {
  35. SessionId string `json:"sessionId"`
  36. Title string `json:"title"`
  37. CreatedAt int64 `json:"createdAt"`
  38. LastActive int64 `json:"lastActive"`
  39. IsActive bool `json:"isActive"`
  40. }
  41. // ACPManager 管理所有用户的 ACP 会话
  42. type ACPManager struct {
  43. sessions map[string]*ACPSession // key: userId -> 当前活跃会话(持有进程引用)
  44. mu sync.Mutex
  45. serviceDir string
  46. acpConfig acp.ClientConfig
  47. }
  48. // NewACPManager 创建 ACP 管理器
  49. func NewACPManager(serviceDir string, env []string) *ACPManager {
  50. return &ACPManager{
  51. sessions: make(map[string]*ACPSession),
  52. serviceDir: serviceDir,
  53. acpConfig: acp.ClientConfig{
  54. Command: "kiro-cli",
  55. Args: []string{"acp", "--agent", "fwh-toolbox-agent"},
  56. WorkingDir: serviceDir,
  57. Env: env,
  58. },
  59. }
  60. }
  61. // ============ 会话生命周期 ============
  62. // GetOrCreateSession 获取或创建用户的 ACP 会话
  63. // 优先级:内存活跃会话 → Redis 持久化恢复 → 创建新会话
  64. func (m *ACPManager) GetOrCreateSession(userId string) (*ACPSession, error) {
  65. m.mu.Lock()
  66. defer m.mu.Unlock()
  67. // 1. 检查内存中是否有活跃会话
  68. if sess, ok := m.sessions[userId]; ok {
  69. if sess.Client.IsAlive() {
  70. sess.LastActive = time.Now()
  71. return sess, nil
  72. }
  73. sess.Client.Close()
  74. delete(m.sessions, userId)
  75. }
  76. // 2. 尝试从 Redis 恢复会话
  77. savedSession, err := repositories.GetAiSession(userId)
  78. if err == nil && savedSession != nil {
  79. sess, err := m.startAndLoadSession(userId, savedSession)
  80. if err == nil {
  81. zap.L().Info("[AIChat] 从 Redis 恢复会话成功", zap.String("userId", userId), zap.String("sessionId", savedSession.SessionId))
  82. return sess, nil
  83. }
  84. zap.L().Warn("[AIChat] 从 Redis 恢复会话失败,将创建新会话", zap.String("userId", userId), zap.Error(err))
  85. repositories.DeleteAiSession(userId)
  86. }
  87. // 3. 创建新会话
  88. return m.createSession(userId)
  89. }
  90. // CreateNewSession 强制创建一个新会话(归档旧会话到 Redis 历史)
  91. func (m *ACPManager) CreateNewSession(userId string) (*ACPSession, error) {
  92. m.mu.Lock()
  93. defer m.mu.Unlock()
  94. // 归档当前活跃会话
  95. if sess, ok := m.sessions[userId]; ok {
  96. m.archiveSession(userId, sess)
  97. sess.Client.Close()
  98. delete(m.sessions, userId)
  99. }
  100. return m.createSession(userId)
  101. }
  102. // LoadSession 加载历史会话(恢复 AI 上下文)
  103. func (m *ACPManager) LoadSession(userId string, sessionId string) (*ACPSession, error) {
  104. m.mu.Lock()
  105. defer m.mu.Unlock()
  106. // 如果已经是当前会话,直接返回
  107. if sess, ok := m.sessions[userId]; ok {
  108. if sess.SessionId == sessionId {
  109. sess.LastActive = time.Now()
  110. return sess, nil
  111. }
  112. // 归档当前活跃会话
  113. m.archiveSession(userId, sess)
  114. sess.Client.Close()
  115. delete(m.sessions, userId)
  116. }
  117. // 从 Redis 历史列表中获取标题
  118. item := m.getSessionListFromRedisHistory(userId, sessionId)
  119. if item == nil {
  120. return nil, fmt.Errorf("redis session/load 失败")
  121. }
  122. // 从 Redis 历史列表中移除该会话(因为它即将变为活跃)
  123. m.removeFromRedisHistory(userId, sessionId)
  124. // 启动 ACP 进程并 load 会话
  125. client, err := acp.NewClient(m.acpConfig)
  126. if err != nil {
  127. return nil, err
  128. }
  129. if err := client.Initialize(); err != nil {
  130. client.Close()
  131. return nil, err
  132. }
  133. loadedId, welcomeMsg, replayMessages, err := client.LoadSession(sessionId, m.serviceDir)
  134. if err != nil {
  135. client.Close()
  136. return nil, fmt.Errorf("session/load 失败: %w", err)
  137. }
  138. zap.L().Info("[AIChat] session/load 回放完成",
  139. zap.String("sessionId", loadedId),
  140. zap.String("welcomeMsg", welcomeMsg),
  141. zap.Int("replayCount", len(replayMessages)),
  142. )
  143. sess := &ACPSession{
  144. UserId: userId,
  145. SessionId: sessionId,
  146. Title: item.Title,
  147. WelcomeMessage: welcomeMsg,
  148. ReplayMessages: replayMessages,
  149. Client: client,
  150. LastActive: time.Now(),
  151. CreatedAt: time.UnixMilli(item.CreatedAt),
  152. }
  153. client.UserId = userId
  154. m.sessions[userId] = sess
  155. // 持久化
  156. m.activateSession(userId, sess)
  157. zap.L().Info("[AIChat] 会话加载成功", zap.String("userId", userId), zap.String("sessionId", sessionId))
  158. return sess, nil
  159. }
  160. // ============ 会话列表查询 ============
  161. // GetSessionList 获取用户的会话列表(当前活跃 + Redis 历史)
  162. func (m *ACPManager) GetSessionList(userId string) []SessionListItem {
  163. m.mu.Lock()
  164. defer m.mu.Unlock()
  165. list := make([]SessionListItem, 0)
  166. // 当前活跃会话排在最前
  167. if sess, ok := m.sessions[userId]; ok {
  168. list = append(list, SessionListItem{
  169. SessionId: sess.SessionId,
  170. Title: sess.Title,
  171. CreatedAt: sess.CreatedAt.UnixMilli(),
  172. LastActive: sess.LastActive.UnixMilli(),
  173. IsActive: true,
  174. })
  175. } else {
  176. // 内存无活跃会话(后端重启),从 Redis 恢复完整会话到内存
  177. activeSess, err := repositories.GetAiSession(userId)
  178. if err == nil && activeSess != nil {
  179. sess, err := m.startAndLoadSession(userId, activeSess)
  180. if err == nil {
  181. list = append(list, SessionListItem{
  182. SessionId: sess.SessionId,
  183. Title: sess.Title,
  184. CreatedAt: sess.CreatedAt.UnixMilli(),
  185. LastActive: sess.LastActive.UnixMilli(),
  186. IsActive: true,
  187. })
  188. zap.L().Info("[AIChat] GetSessionList 恢复会话到内存", zap.String("userId", userId), zap.String("sessionId", sess.SessionId))
  189. }
  190. }
  191. }
  192. // 从 Redis 获取历史列表
  193. redisItems, err := repositories.GetAiSessionList(userId)
  194. if err == nil && len(redisItems) > 0 {
  195. for _, item := range redisItems {
  196. list = append(list, SessionListItem{
  197. SessionId: item.SessionId,
  198. Title: item.Title,
  199. CreatedAt: item.CreatedAt,
  200. LastActive: item.LastActive,
  201. IsActive: false,
  202. })
  203. }
  204. }
  205. return list
  206. }
  207. // ============ 会话操作 ============
  208. // UpdateSessionTitle 更新会话标题
  209. func (m *ACPManager) UpdateSessionTitle(userId string, sessionId string, title string) {
  210. m.mu.Lock()
  211. defer m.mu.Unlock()
  212. // 检查当前活跃会话
  213. if sess, ok := m.sessions[userId]; ok && sess.SessionId == sessionId {
  214. sess.Title = title
  215. m.saveSessionTitle(userId, sess)
  216. return
  217. }
  218. // 历史会话
  219. m.saveHistorySessionTitle(userId, sessionId, title)
  220. }
  221. // DeleteSessionFromHistory 删除某会话
  222. func (m *ACPManager) DeleteSessionFromHistory(userId string, sessionId string) {
  223. m.mu.Lock()
  224. defer m.mu.Unlock()
  225. // 如果是当前活跃会话,关闭它
  226. if sess, ok := m.sessions[userId]; ok && sess.SessionId == sessionId {
  227. sess.Client.Close()
  228. delete(m.sessions, userId)
  229. m.removeSession(userId, sessionId, true)
  230. return
  231. }
  232. // 历史会话
  233. m.removeSession(userId, sessionId, false)
  234. }
  235. // DeleteSession 删除当前活跃会话
  236. func (m *ACPManager) DeleteSession(userId string) {
  237. m.mu.Lock()
  238. defer m.mu.Unlock()
  239. if sess, ok := m.sessions[userId]; ok {
  240. sess.Client.Close()
  241. delete(m.sessions, userId)
  242. m.removeSession(userId, sess.SessionId, true)
  243. zap.L().Info("[AIChat] 会话已删除", zap.String("userId", userId))
  244. }
  245. }
  246. // GetSessionInfo 获取用户当前活跃会话信息
  247. func (m *ACPManager) GetSessionInfo(userId string) *ACPSession {
  248. m.mu.Lock()
  249. defer m.mu.Unlock()
  250. return m.sessions[userId]
  251. }
  252. // Cancel 取消当前操作
  253. func (m *ACPManager) Cancel(sess *ACPSession) {
  254. sess.Client.Cancel(sess.SessionId)
  255. }
  256. // ResolvePermission 前端回传权限选择
  257. func (m *ACPManager) ResolvePermission(userId string, optionId string) error {
  258. sess := m.GetSessionInfo(userId)
  259. if sess == nil {
  260. return fmt.Errorf("没有活跃的会话")
  261. }
  262. sess.Client.ResolvePermission(optionId)
  263. return nil
  264. }
  265. // ============ 消息发送 ============
  266. // Prompt 发送消息并流式读取响应
  267. func (m *ACPManager) Prompt(sess *ACPSession, message string, eventCh chan<- AIChatEvent) {
  268. sess.Mutex.Lock()
  269. defer sess.Mutex.Unlock()
  270. sess.LastActive = time.Now()
  271. // 首次发消息时自动设置标题(取消息前 30 字)
  272. if sess.Title == "" && message != "" {
  273. title := message
  274. if len([]rune(title)) > 30 {
  275. title = string([]rune(title)[:30]) + "..."
  276. }
  277. sess.Title = title
  278. }
  279. // 注入会话上下文(让 AI 在调用 MCP Tool 时能传入正确的 userId 和 sessionId)
  280. contextMessage := fmt.Sprintf("[context] userId=%s, sessionId=%s\n\n%s", sess.UserId, sess.SessionId, message)
  281. // 用 acp.Client.Prompt 流式读取
  282. acpEvents := make(chan acp.Event, 50)
  283. go sess.Client.Prompt(sess.SessionId, contextMessage, 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 "done":
  296. eventCh <- AIChatEvent{Type: "done", SessionId: event.SessionId, StopReason: event.StopReason}
  297. case "error":
  298. eventCh <- AIChatEvent{Type: "error", Content: event.Content}
  299. }
  300. }
  301. close(eventCh)
  302. }
  303. // ============ 内部方法 ============
  304. // createSession 创建新 ACP 会话
  305. func (m *ACPManager) createSession(userId string) (*ACPSession, error) {
  306. client, err := acp.NewClient(m.acpConfig)
  307. if err != nil {
  308. return nil, err
  309. }
  310. if err := client.Initialize(); err != nil {
  311. client.Close()
  312. return nil, err
  313. }
  314. sessionId, welcomeMsg, err := client.NewSession(m.serviceDir)
  315. if err != nil {
  316. client.Close()
  317. return nil, err
  318. }
  319. now := time.Now()
  320. sess := &ACPSession{
  321. UserId: userId,
  322. SessionId: sessionId,
  323. Title: "",
  324. WelcomeMessage: welcomeMsg,
  325. Client: client,
  326. LastActive: now,
  327. CreatedAt: now,
  328. }
  329. client.UserId = userId
  330. m.sessions[userId] = sess
  331. // 持久化
  332. m.saveNewSession(userId, sess)
  333. zap.L().Info("[AIChat] 会话创建成功", zap.String("userId", userId), zap.String("sessionId", sessionId))
  334. return sess, nil
  335. }
  336. // startAndLoadSession 启动 ACP 进程并 load 指定会话(用于 Redis 恢复)
  337. func (m *ACPManager) startAndLoadSession(userId string, active *repositories.AiActiveSession) (*ACPSession, error) {
  338. client, err := acp.NewClient(m.acpConfig)
  339. if err != nil {
  340. return nil, err
  341. }
  342. if err := client.Initialize(); err != nil {
  343. client.Close()
  344. return nil, err
  345. }
  346. _, welcomeMsg, _, err := client.LoadSession(active.SessionId, m.serviceDir)
  347. if err != nil {
  348. client.Close()
  349. return nil, fmt.Errorf("session/load 失败: %w", err)
  350. }
  351. now := time.Now()
  352. sess := &ACPSession{
  353. UserId: userId,
  354. SessionId: active.SessionId,
  355. Title: active.Title,
  356. WelcomeMessage: welcomeMsg,
  357. Client: client,
  358. LastActive: now,
  359. CreatedAt: time.UnixMilli(active.CreatedAt),
  360. }
  361. client.UserId = userId
  362. m.sessions[userId] = sess
  363. return sess, nil
  364. }
  365. // ============ 内部方法:会话存储 ============
  366. // persistNewSession 持久化新创建的活跃会话(Redis + MongoDB)
  367. func (m *ACPManager) saveNewSession(userId string, sess *ACPSession) {
  368. repositories.SaveAiSession(userId, repositories.AiActiveSession{
  369. SessionId: sess.SessionId,
  370. Title: sess.Title,
  371. CreatedAt: sess.CreatedAt.UnixMilli(),
  372. LastActive: sess.LastActive.UnixMilli(),
  373. })
  374. repositories.DeactivateAllAiChatSessions(userId)
  375. repositories.UpsertAiChatSession(repositories.AiChatSession{
  376. UserId: userId,
  377. SessionId: sess.SessionId,
  378. Title: sess.Title,
  379. IsActive: true,
  380. CreatedAt: sess.CreatedAt,
  381. LastActive: sess.LastActive,
  382. })
  383. }
  384. // persistActivateSession 持久化会话激活(Redis + MongoDB)
  385. func (m *ACPManager) activateSession(userId string, sess *ACPSession) {
  386. repositories.SaveAiSession(userId, repositories.AiActiveSession{
  387. SessionId: sess.SessionId,
  388. Title: sess.Title,
  389. CreatedAt: sess.CreatedAt.UnixMilli(),
  390. LastActive: sess.LastActive.UnixMilli(),
  391. })
  392. repositories.DeactivateAllAiChatSessions(userId)
  393. repositories.UpdateAiChatSessionActive(userId, sess.SessionId, true)
  394. }
  395. // persistArchiveSession 持久化会话归档(Redis + MongoDB)
  396. func (m *ACPManager) archiveSession(userId string, sess *ACPSession) {
  397. item := repositories.AiSessionItem{
  398. SessionId: sess.SessionId,
  399. Title: sess.Title,
  400. CreatedAt: sess.CreatedAt.UnixMilli(),
  401. LastActive: sess.LastActive.UnixMilli(),
  402. }
  403. list, _ := repositories.GetAiSessionList(userId)
  404. list = append([]repositories.AiSessionItem{item}, list...)
  405. if len(list) > 50 {
  406. overflow := list[50:]
  407. list = list[:50]
  408. go func() {
  409. for _, old := range overflow {
  410. m.deleteKiroSession(old.SessionId)
  411. }
  412. }()
  413. }
  414. if err := repositories.SaveAiSessionList(userId, list); err != nil {
  415. zap.L().Warn("[AIChat] 归档会话到Redis失败", zap.String("userId", userId), zap.Error(err))
  416. }
  417. repositories.UpdateAiChatSessionActive(userId, sess.SessionId, false)
  418. }
  419. // persistUpdateTitle 持久化标题更新(Redis + MongoDB)
  420. func (m *ACPManager) saveSessionTitle(userId string, sess *ACPSession) {
  421. repositories.SaveAiSession(userId, repositories.AiActiveSession{
  422. SessionId: sess.SessionId,
  423. Title: sess.Title,
  424. CreatedAt: sess.CreatedAt.UnixMilli(),
  425. LastActive: sess.LastActive.UnixMilli(),
  426. })
  427. repositories.UpdateAiChatSessionTitle(userId, sess.SessionId, sess.Title)
  428. }
  429. // persistUpdateHistoryTitle 持久化历史会话标题更新(Redis + MongoDB)
  430. func (m *ACPManager) saveHistorySessionTitle(userId string, sessionId string, title string) {
  431. repositories.UpdateAiSessionTitle(userId, sessionId, title)
  432. repositories.UpdateAiChatSessionTitle(userId, sessionId, title)
  433. }
  434. // persistDeleteSession 持久化删除会话(Redis + MongoDB)
  435. func (m *ACPManager) removeSession(userId string, sessionId string, isActive bool) {
  436. if isActive {
  437. repositories.DeleteAiSession(userId)
  438. } else {
  439. m.removeFromRedisHistory(userId, sessionId)
  440. }
  441. repositories.DeleteAiChatSession(userId, sessionId)
  442. }
  443. // ============ 内部方法:Redis 操作 ============
  444. func (m *ACPManager) removeFromRedisHistory(userId string, sessionId string) {
  445. list, err := repositories.GetAiSessionList(userId)
  446. if err != nil || len(list) == 0 {
  447. return
  448. }
  449. for i := range list {
  450. if list[i].SessionId == sessionId {
  451. list = append(list[:i], list[i+1:]...)
  452. repositories.SaveAiSessionList(userId, list)
  453. return
  454. }
  455. }
  456. }
  457. // getSessionListFromRedisHistory 从 Redis 历史列表中获取会话
  458. func (m *ACPManager) getSessionListFromRedisHistory(userId string, sessionId string) *repositories.AiSessionItem {
  459. list, err := repositories.GetAiSessionList(userId)
  460. if err != nil {
  461. zap.L().Warn("[AIChat] 从redis读取AiSessionList失败", zap.String("err", err.Error()))
  462. return nil
  463. }
  464. for _, item := range list {
  465. if item.SessionId == sessionId {
  466. return &item
  467. }
  468. }
  469. return nil
  470. }
  471. // deleteKiroSession 通过 kiro-cli 命令删除会话
  472. func (m *ACPManager) deleteKiroSession(sessionId string) {
  473. cmd := exec.Command("kiro-cli", "chat", "--delete-session", sessionId)
  474. cmd.Dir = m.serviceDir
  475. if output, err := cmd.CombinedOutput(); err != nil {
  476. zap.L().Warn("[AIChat] 删除Kiro会话失败",
  477. zap.String("sessionId", sessionId),
  478. zap.Error(err),
  479. zap.String("output", string(output)),
  480. )
  481. } else {
  482. zap.L().Info("[AIChat] 删除Kiro会话成功", zap.String("sessionId", sessionId))
  483. }
  484. }
  485. // ============ 工具函数 ============
  486. func toJSON(v any) string {
  487. data, _ := json.Marshal(v)
  488. return string(data)
  489. }