aiChatServices.go 17 KB

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