aiChatControllers.go 10 KB

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