aiChatControllers.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. package controllers
  2. import (
  3. "bufio"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "path/filepath"
  9. "github.com/gofiber/fiber/v2"
  10. "github.com/gofiber/fiber/v2/utils"
  11. "github.com/minio/minio-go/v7"
  12. "fwh-toolbox-server/middleware"
  13. "fwh-toolbox-server/services"
  14. "fwh-toolbox-server/utils/filestore"
  15. minioUtil "fwh-toolbox-server/utils/minio"
  16. )
  17. var acpManager *services.ACPManager
  18. // InitAIChat 初始化 AI Chat 模块
  19. func InitAIChat(serviceDir string) {
  20. acpManager = services.NewACPManager(serviceDir, []string{"HOME=/home/jerry-remote"})
  21. }
  22. // AiChat 发送消息(SSE 流式响应)
  23. // POST /ai/chat
  24. func AiChat(c *fiber.Ctx) error {
  25. var req struct {
  26. Message string `json:"message"`
  27. SessionId string `json:"sessionId"`
  28. }
  29. if err := c.BodyParser(&req); err != nil {
  30. return Fail(c, "Invalid request body")
  31. }
  32. if req.Message == "" {
  33. return Fail(c, "message 不能为空")
  34. }
  35. userId := middleware.AuthGetUserName(c)
  36. sess, err := acpManager.GetOrCreateSession(userId)
  37. if err != nil {
  38. return Error(c, fmt.Sprintf("AI 会话创建失败: %v", err))
  39. }
  40. // SSE 流式响应
  41. c.Set("Content-Type", "text/event-stream")
  42. c.Set("Cache-Control", "no-cache")
  43. c.Set("Connection", "keep-alive")
  44. c.Set("X-Accel-Buffering", "no")
  45. c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
  46. // 如果有欢迎消息且是新会话,先发送 welcome 事件
  47. if sess.WelcomeMessage != "" {
  48. welcomeEvent, _ := json.Marshal(services.AIChatEvent{Type: "welcome", Content: sess.WelcomeMessage})
  49. fmt.Fprintf(w, "event: welcome\ndata: %s\n\n", string(welcomeEvent))
  50. w.Flush()
  51. }
  52. eventCh := make(chan services.AIChatEvent, 50)
  53. go acpManager.Prompt(sess, req.Message, eventCh)
  54. for event := range eventCh {
  55. data, _ := json.Marshal(event)
  56. fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Type, string(data))
  57. w.Flush()
  58. }
  59. })
  60. return nil
  61. }
  62. // GetAiSessionList 获取用户的会话历史列表
  63. // GET /ai/sessions/list
  64. func GetAiSessionList(c *fiber.Ctx) error {
  65. userId := middleware.AuthGetUserName(c)
  66. list := acpManager.GetSessionList(userId)
  67. return Success(c, list)
  68. }
  69. // CreateAiSession 创建新的 AI 会话(保留旧会话到历史)
  70. // POST /ai/sessions/new
  71. func CreateAiSession(c *fiber.Ctx) error {
  72. userId := middleware.AuthGetUserName(c)
  73. // 创建新会话(旧会话自动归档到历史)
  74. sess, err := acpManager.CreateNewSession(userId)
  75. if err != nil {
  76. return Error(c, fmt.Sprintf("创建会话失败: %v", err))
  77. }
  78. return Success(c, fiber.Map{
  79. "sessionId": sess.SessionId,
  80. "welcomeMessage": sess.WelcomeMessage,
  81. })
  82. }
  83. // SwitchAiSession 切换到历史会话(恢复 AI 上下文,轻量响应)
  84. // POST /ai/sessions/:id
  85. func SwitchAiSession(c *fiber.Ctx) error {
  86. sessionId := utils.CopyString(c.Params("id"))
  87. if sessionId == "" {
  88. return Fail(c, "sessionId 不能为空")
  89. }
  90. userId := middleware.AuthGetUserName(c)
  91. sess, err := acpManager.LoadSession(userId, sessionId)
  92. if err != nil {
  93. return Error(c, fmt.Sprintf("加载会话失败: %v", err))
  94. }
  95. return Success(c, fiber.Map{
  96. "sessionId": sess.SessionId,
  97. "title": sess.Title,
  98. "welcomeMessage": sess.WelcomeMessage,
  99. })
  100. }
  101. // StreamLoadAiSession SSE 流式推送回放消息
  102. // GET /ai/sessions/:id/load
  103. func StreamLoadAiSession(c *fiber.Ctx) error {
  104. sessionId := utils.CopyString(c.Params("id"))
  105. if sessionId == "" {
  106. return Fail(c, "sessionId 不能为空")
  107. }
  108. userId := middleware.AuthGetUserName(c)
  109. // 获取回放消息(会话必须已通过 SwitchAiSession 加载)
  110. replayMessages := acpManager.GetReplayMessages(userId)
  111. if replayMessages == nil {
  112. return Fail(c, "会话未加载,请先调用切换会话接口")
  113. }
  114. // SSE 流式响应
  115. c.Set("Content-Type", "text/event-stream")
  116. c.Set("Cache-Control", "no-cache")
  117. c.Set("Connection", "keep-alive")
  118. c.Set("X-Accel-Buffering", "no")
  119. c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
  120. var totalBytes int
  121. for _, msg := range replayMessages {
  122. data, _ := json.Marshal(msg)
  123. totalBytes += len(msg.Content)
  124. fmt.Fprintf(w, "event: message\ndata: %s\n\n", string(data))
  125. w.Flush()
  126. }
  127. // 发送 done 事件,包含统计信息和建议
  128. suggestNewSession := totalBytes >= 100*1024 // 100KB 以上建议新建
  129. donePayload, _ := json.Marshal(fiber.Map{
  130. "sessionId": sessionId,
  131. "totalMessages": len(replayMessages),
  132. "totalBytes": totalBytes,
  133. "suggestNewSession": suggestNewSession,
  134. })
  135. fmt.Fprintf(w, "event: done\ndata: %s\n\n", string(donePayload))
  136. w.Flush()
  137. })
  138. return nil
  139. }
  140. // UpdateAiSessionTitle 更新会话标题
  141. // PATCH /ai/sessions/:id
  142. func UpdateAiSessionTitle(c *fiber.Ctx) error {
  143. sessionId := utils.CopyString(c.Params("id"))
  144. if sessionId == "" {
  145. return Fail(c, "sessionId 不能为空")
  146. }
  147. var req struct {
  148. Title string `json:"title"`
  149. }
  150. if err := c.BodyParser(&req); err != nil {
  151. return Fail(c, "Invalid request body")
  152. }
  153. userId := middleware.AuthGetUserName(c)
  154. acpManager.UpdateSessionTitle(userId, sessionId, req.Title)
  155. return Success[any](c, nil, "标题已更新")
  156. }
  157. // DeleteAiSession 删除 AI 会话
  158. // DELETE /ai/sessions/:id
  159. func DeleteAiSession(c *fiber.Ctx) error {
  160. sessionId := utils.CopyString(c.Params("id"))
  161. userId := middleware.AuthGetUserName(c)
  162. if sessionId == "" {
  163. // 无 id 参数,删除当前活跃会话
  164. acpManager.DeleteSession(userId)
  165. } else {
  166. // 删除指定会话
  167. acpManager.DeleteSessionFromHistory(userId, sessionId)
  168. }
  169. return Success[any](c, nil, "会话已删除")
  170. }
  171. // CancelAiChat 取消当前 AI 操作
  172. // POST /ai/cancel
  173. func CancelAiChat(c *fiber.Ctx) error {
  174. userId := middleware.AuthGetUserName(c)
  175. sess := acpManager.GetSessionInfo(userId)
  176. if sess == nil {
  177. return Fail(c, "没有活跃的会话")
  178. }
  179. acpManager.Cancel(sess)
  180. return Success[any](c, nil, "已取消")
  181. }
  182. // ResolvePermission 前端回传写权限选择
  183. // POST /ai/permission
  184. func ResolvePermission(c *fiber.Ctx) error {
  185. var req struct {
  186. OptionId string `json:"optionId"`
  187. }
  188. if err := c.BodyParser(&req); err != nil {
  189. return Fail(c, "Invalid request body")
  190. }
  191. if req.OptionId == "" {
  192. return Fail(c, "optionId 不能为空")
  193. }
  194. userId := middleware.AuthGetUserName(c)
  195. if err := acpManager.ResolvePermission(userId, req.OptionId); err != nil {
  196. return Fail(c, err.Error())
  197. }
  198. return Success[any](c, nil, "已回复")
  199. }
  200. // DownloadAiFile 下载 AI 生成的文件
  201. // GET /ai/files/download?key=xxx
  202. func DownloadAiFile(c *fiber.Ctx) error {
  203. key := utils.CopyString(c.Query("key"))
  204. if key == "" {
  205. return Fail(c, "key 不能为空")
  206. }
  207. // 直接从 MinIO 读取文件(key 即 objectKey)
  208. obj, err := minioUtil.Client.GetObject(context.Background(), filestore.AIFileBucket, key, minio.GetObjectOptions{})
  209. if err != nil {
  210. return Fail(c, "文件不存在")
  211. }
  212. defer obj.Close()
  213. // 获取文件信息
  214. stat, err := obj.Stat()
  215. if err != nil {
  216. return Fail(c, fmt.Sprintf("文件不存在: %v", err))
  217. }
  218. // 读取全部内容
  219. data, err := io.ReadAll(obj)
  220. if err != nil {
  221. return Error(c, fmt.Sprintf("读取文件失败: %v", err))
  222. }
  223. // 从 key 中提取文件名(key 格式: prefix/userId/sessionId/uuid_fileName)
  224. fileName := filepath.Base(key)
  225. // 去掉 uuid 前缀(前9个字符: 8位uuid + 下划线)
  226. if len(fileName) > 9 {
  227. fileName = fileName[9:]
  228. }
  229. c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, fileName))
  230. c.Set("Content-Type", stat.ContentType)
  231. return c.Send(data)
  232. }
  233. // UploadAiFile 上传文件给 AI 分析
  234. // POST /ai/files/upload
  235. func UploadAiFile(c *fiber.Ctx) error {
  236. userId := middleware.AuthGetUserName(c)
  237. sess, err := acpManager.GetOrCreateSession(userId)
  238. if err != nil {
  239. return Error(c, fmt.Sprintf("AI 会话创建失败: %v", err))
  240. }
  241. // 读取上传文件
  242. file, err := c.FormFile("file")
  243. if err != nil {
  244. return Fail(c, "请选择要上传的文件")
  245. }
  246. // 限制文件大小(最大 2MB)
  247. if file.Size > 2*1024*1024 {
  248. return Fail(c, "文件大小不能超过 2MB")
  249. }
  250. // 读取文件内容
  251. f, err := file.Open()
  252. if err != nil {
  253. return Error(c, "打开文件失败")
  254. }
  255. defer f.Close()
  256. data, err := io.ReadAll(f)
  257. if err != nil {
  258. return Error(c, "读取文件失败")
  259. }
  260. // 写入 FileStore(存到 ACP 的 FileStore,这样 Agent readTextFile 可以读到)
  261. virtualPath := fmt.Sprintf("/uploads/%s", file.Filename)
  262. record, err := sess.Client.FileStore.WriteFile(userId, sess.SessionId, virtualPath, string(data))
  263. if err != nil {
  264. return Error(c, fmt.Sprintf("保存文件失败: %v", err))
  265. }
  266. return Success(c, fiber.Map{
  267. "key": record.Key,
  268. "fileName": record.FileName,
  269. "path": record.Path,
  270. "size": record.Size,
  271. })
  272. }