aiChatServices.go 18 KB

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