aiChatControllers.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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. // LoadAiSession 加载历史会话(恢复 AI 上下文)
  84. // POST /ai/sessions/:id/load
  85. func LoadAiSession(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. "replayMessages": sess.ReplayMessages,
  99. })
  100. }
  101. // UpdateAiSessionTitle 更新会话标题
  102. // PATCH /ai/sessions/:id
  103. func UpdateAiSessionTitle(c *fiber.Ctx) error {
  104. sessionId := utils.CopyString(c.Params("id"))
  105. if sessionId == "" {
  106. return Fail(c, "sessionId 不能为空")
  107. }
  108. var req struct {
  109. Title string `json:"title"`
  110. }
  111. if err := c.BodyParser(&req); err != nil {
  112. return Fail(c, "Invalid request body")
  113. }
  114. userId := middleware.AuthGetUserName(c)
  115. acpManager.UpdateSessionTitle(userId, sessionId, req.Title)
  116. return Success[any](c, nil, "标题已更新")
  117. }
  118. // DeleteAiSession 删除 AI 会话
  119. // DELETE /ai/sessions/:id
  120. func DeleteAiSession(c *fiber.Ctx) error {
  121. sessionId := utils.CopyString(c.Params("id"))
  122. userId := middleware.AuthGetUserName(c)
  123. if sessionId == "" {
  124. // 无 id 参数,删除当前活跃会话
  125. acpManager.DeleteSession(userId)
  126. } else {
  127. // 删除指定会话
  128. acpManager.DeleteSessionFromHistory(userId, sessionId)
  129. }
  130. return Success[any](c, nil, "会话已删除")
  131. }
  132. // CancelAiChat 取消当前 AI 操作
  133. // POST /ai/cancel
  134. func CancelAiChat(c *fiber.Ctx) error {
  135. userId := middleware.AuthGetUserName(c)
  136. sess := acpManager.GetSessionInfo(userId)
  137. if sess == nil {
  138. return Fail(c, "没有活跃的会话")
  139. }
  140. acpManager.Cancel(sess)
  141. return Success[any](c, nil, "已取消")
  142. }
  143. // ResolvePermission 前端回传写权限选择
  144. // POST /ai/permission
  145. func ResolvePermission(c *fiber.Ctx) error {
  146. var req struct {
  147. OptionId string `json:"optionId"`
  148. }
  149. if err := c.BodyParser(&req); err != nil {
  150. return Fail(c, "Invalid request body")
  151. }
  152. if req.OptionId == "" {
  153. return Fail(c, "optionId 不能为空")
  154. }
  155. userId := middleware.AuthGetUserName(c)
  156. if err := acpManager.ResolvePermission(userId, req.OptionId); err != nil {
  157. return Fail(c, err.Error())
  158. }
  159. return Success[any](c, nil, "已回复")
  160. }
  161. // DownloadAiFile 下载 AI 生成的文件
  162. // GET /ai/files/download?key=xxx
  163. func DownloadAiFile(c *fiber.Ctx) error {
  164. key := utils.CopyString(c.Query("key"))
  165. if key == "" {
  166. return Fail(c, "key 不能为空")
  167. }
  168. // 直接从 MinIO 读取文件(key 即 objectKey)
  169. obj, err := minioUtil.Client.GetObject(context.Background(), filestore.AIFileBucket, key, minio.GetObjectOptions{})
  170. if err != nil {
  171. return Fail(c, "文件不存在")
  172. }
  173. defer obj.Close()
  174. // 获取文件信息
  175. stat, err := obj.Stat()
  176. if err != nil {
  177. return Fail(c, fmt.Sprintf("文件不存在: %v", err))
  178. }
  179. // 读取全部内容
  180. data, err := io.ReadAll(obj)
  181. if err != nil {
  182. return Error(c, fmt.Sprintf("读取文件失败: %v", err))
  183. }
  184. // 从 key 中提取文件名(key 格式: prefix/userId/sessionId/uuid_fileName)
  185. fileName := filepath.Base(key)
  186. // 去掉 uuid 前缀(前9个字符: 8位uuid + 下划线)
  187. if len(fileName) > 9 {
  188. fileName = fileName[9:]
  189. }
  190. c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, fileName))
  191. c.Set("Content-Type", stat.ContentType)
  192. return c.Send(data)
  193. }
  194. // UploadAiFile 上传文件给 AI 分析
  195. // POST /ai/files/upload
  196. func UploadAiFile(c *fiber.Ctx) error {
  197. userId := middleware.AuthGetUserName(c)
  198. sess, err := acpManager.GetOrCreateSession(userId)
  199. if err != nil {
  200. return Error(c, fmt.Sprintf("AI 会话创建失败: %v", err))
  201. }
  202. // 读取上传文件
  203. file, err := c.FormFile("file")
  204. if err != nil {
  205. return Fail(c, "请选择要上传的文件")
  206. }
  207. // 限制文件大小(最大 2MB)
  208. if file.Size > 2*1024*1024 {
  209. return Fail(c, "文件大小不能超过 2MB")
  210. }
  211. // 读取文件内容
  212. f, err := file.Open()
  213. if err != nil {
  214. return Error(c, "打开文件失败")
  215. }
  216. defer f.Close()
  217. data, err := io.ReadAll(f)
  218. if err != nil {
  219. return Error(c, "读取文件失败")
  220. }
  221. // 写入 FileStore(存到 ACP 的 FileStore,这样 Agent readTextFile 可以读到)
  222. virtualPath := fmt.Sprintf("/uploads/%s", file.Filename)
  223. record, err := sess.Client.FileStore.WriteFile(userId, sess.SessionId, virtualPath, string(data))
  224. if err != nil {
  225. return Error(c, fmt.Sprintf("保存文件失败: %v", err))
  226. }
  227. return Success(c, fiber.Map{
  228. "key": record.Key,
  229. "fileName": record.FileName,
  230. "path": record.Path,
  231. "size": record.Size,
  232. })
  233. }