aiChatServices.go 18 KB

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