aiChatControllers.go 7.6 KB

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