aiChatControllers.go 7.3 KB

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