aiChatServices.go 19 KB

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