aiChatControllers.go 11 KB

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