| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- package controllers
- import (
- "bufio"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "path/filepath"
- "github.com/gofiber/fiber/v2"
- "github.com/gofiber/fiber/v2/utils"
- "github.com/minio/minio-go/v7"
- "fwh-toolbox-server/middleware"
- "fwh-toolbox-server/services"
- "fwh-toolbox-server/utils/filestore"
- minioUtil "fwh-toolbox-server/utils/minio"
- )
- var acpManager *services.ACPManager
- // InitAIChat 初始化 AI Chat 模块
- func InitAIChat(serviceDir string) {
- acpManager = services.NewACPManager(serviceDir, []string{"HOME=/home/jerry-remote"})
- }
- // AiChat 发送消息(SSE 流式响应)
- // POST /ai/chat
- func AiChat(c *fiber.Ctx) error {
- var req struct {
- Message string `json:"message"`
- SessionId string `json:"sessionId"`
- }
- if err := c.BodyParser(&req); err != nil {
- return Fail(c, "Invalid request body")
- }
- if req.Message == "" {
- return Fail(c, "message 不能为空")
- }
- userId := middleware.AuthGetUserName(c)
- sess, err := acpManager.GetOrCreateSession(userId)
- if err != nil {
- return Error(c, fmt.Sprintf("AI 会话创建失败: %v", err))
- }
- // SSE 流式响应
- c.Set("Content-Type", "text/event-stream")
- c.Set("Cache-Control", "no-cache")
- c.Set("Connection", "keep-alive")
- c.Set("X-Accel-Buffering", "no")
- c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
- // 如果有欢迎消息且是新会话,先发送 welcome 事件
- if sess.WelcomeMessage != "" {
- welcomeEvent, _ := json.Marshal(services.AIChatEvent{Type: "welcome", Content: sess.WelcomeMessage})
- fmt.Fprintf(w, "event: welcome\ndata: %s\n\n", string(welcomeEvent))
- w.Flush()
- }
- eventCh := make(chan services.AIChatEvent, 50)
- go acpManager.Prompt(sess, req.Message, eventCh)
- for event := range eventCh {
- data, _ := json.Marshal(event)
- fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Type, string(data))
- w.Flush()
- }
- })
- return nil
- }
- // GetAiSessionList 获取用户的会话历史列表
- // GET /ai/sessions/list
- func GetAiSessionList(c *fiber.Ctx) error {
- userId := middleware.AuthGetUserName(c)
- list := acpManager.GetSessionList(userId)
- return Success(c, list)
- }
- // CreateAiSession 创建新的 AI 会话(保留旧会话到历史)
- // POST /ai/sessions/new
- func CreateAiSession(c *fiber.Ctx) error {
- userId := middleware.AuthGetUserName(c)
- // 创建新会话(旧会话自动归档到历史)
- sess, err := acpManager.CreateNewSession(userId)
- if err != nil {
- return Error(c, fmt.Sprintf("创建会话失败: %v", err))
- }
- return Success(c, fiber.Map{
- "sessionId": sess.SessionId,
- "welcomeMessage": sess.WelcomeMessage,
- })
- }
- // LoadAiSession 加载历史会话(恢复 AI 上下文)
- // POST /ai/sessions/:id/load
- func LoadAiSession(c *fiber.Ctx) error {
- sessionId := utils.CopyString(c.Params("id"))
- if sessionId == "" {
- return Fail(c, "sessionId 不能为空")
- }
- userId := middleware.AuthGetUserName(c)
- sess, err := acpManager.LoadSession(userId, sessionId)
- if err != nil {
- return Error(c, fmt.Sprintf("加载会话失败: %v", err))
- }
- return Success(c, fiber.Map{
- "sessionId": sess.SessionId,
- "title": sess.Title,
- "replayMessages": sess.ReplayMessages,
- })
- }
- // UpdateAiSessionTitle 更新会话标题
- // PATCH /ai/sessions/:id
- func UpdateAiSessionTitle(c *fiber.Ctx) error {
- sessionId := utils.CopyString(c.Params("id"))
- if sessionId == "" {
- return Fail(c, "sessionId 不能为空")
- }
- var req struct {
- Title string `json:"title"`
- }
- if err := c.BodyParser(&req); err != nil {
- return Fail(c, "Invalid request body")
- }
- userId := middleware.AuthGetUserName(c)
- acpManager.UpdateSessionTitle(userId, sessionId, req.Title)
- return Success[any](c, nil, "标题已更新")
- }
- // DeleteAiSession 删除 AI 会话
- // DELETE /ai/sessions/:id
- func DeleteAiSession(c *fiber.Ctx) error {
- sessionId := utils.CopyString(c.Params("id"))
- userId := middleware.AuthGetUserName(c)
- if sessionId == "" {
- // 无 id 参数,删除当前活跃会话
- acpManager.DeleteSession(userId)
- } else {
- // 删除指定会话
- acpManager.DeleteSessionFromHistory(userId, sessionId)
- }
- return Success[any](c, nil, "会话已删除")
- }
- // CancelAiChat 取消当前 AI 操作
- // POST /ai/cancel
- func CancelAiChat(c *fiber.Ctx) error {
- userId := middleware.AuthGetUserName(c)
- sess := acpManager.GetSessionInfo(userId)
- if sess == nil {
- return Fail(c, "没有活跃的会话")
- }
- acpManager.Cancel(sess)
- return Success[any](c, nil, "已取消")
- }
- // ResolvePermission 前端回传写权限选择
- // POST /ai/permission
- func ResolvePermission(c *fiber.Ctx) error {
- var req struct {
- OptionId string `json:"optionId"`
- }
- if err := c.BodyParser(&req); err != nil {
- return Fail(c, "Invalid request body")
- }
- if req.OptionId == "" {
- return Fail(c, "optionId 不能为空")
- }
- userId := middleware.AuthGetUserName(c)
- if err := acpManager.ResolvePermission(userId, req.OptionId); err != nil {
- return Fail(c, err.Error())
- }
- return Success[any](c, nil, "已回复")
- }
- // DownloadAiFile 下载 AI 生成的文件
- // GET /ai/files/download?key=xxx
- func DownloadAiFile(c *fiber.Ctx) error {
- key := utils.CopyString(c.Query("key"))
- if key == "" {
- return Fail(c, "key 不能为空")
- }
- // 直接从 MinIO 读取文件(key 即 objectKey)
- obj, err := minioUtil.Client.GetObject(context.Background(), filestore.AIFileBucket, key, minio.GetObjectOptions{})
- if err != nil {
- return Fail(c, "文件不存在")
- }
- defer obj.Close()
- // 获取文件信息
- stat, err := obj.Stat()
- if err != nil {
- return Fail(c, fmt.Sprintf("文件不存在: %v", err))
- }
- // 读取全部内容
- data, err := io.ReadAll(obj)
- if err != nil {
- return Error(c, fmt.Sprintf("读取文件失败: %v", err))
- }
- // 从 key 中提取文件名(key 格式: prefix/userId/sessionId/uuid_fileName)
- fileName := filepath.Base(key)
- // 去掉 uuid 前缀(前9个字符: 8位uuid + 下划线)
- if len(fileName) > 9 {
- fileName = fileName[9:]
- }
- c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, fileName))
- c.Set("Content-Type", stat.ContentType)
- return c.Send(data)
- }
- // UploadAiFile 上传文件给 AI 分析
- // POST /ai/files/upload
- func UploadAiFile(c *fiber.Ctx) error {
- userId := middleware.AuthGetUserName(c)
- sess, err := acpManager.GetOrCreateSession(userId)
- if err != nil {
- return Error(c, fmt.Sprintf("AI 会话创建失败: %v", err))
- }
- // 读取上传文件
- file, err := c.FormFile("file")
- if err != nil {
- return Fail(c, "请选择要上传的文件")
- }
- // 限制文件大小(最大 2MB)
- if file.Size > 2*1024*1024 {
- return Fail(c, "文件大小不能超过 2MB")
- }
- // 读取文件内容
- f, err := file.Open()
- if err != nil {
- return Error(c, "打开文件失败")
- }
- defer f.Close()
- data, err := io.ReadAll(f)
- if err != nil {
- return Error(c, "读取文件失败")
- }
- // 写入 FileStore(存到 ACP 的 FileStore,这样 Agent readTextFile 可以读到)
- virtualPath := fmt.Sprintf("/uploads/%s", file.Filename)
- record, err := sess.Client.FileStore.WriteFile(userId, sess.SessionId, virtualPath, string(data))
- if err != nil {
- return Error(c, fmt.Sprintf("保存文件失败: %v", err))
- }
- return Success(c, fiber.Map{
- "key": record.Key,
- "fileName": record.FileName,
- "path": record.Path,
- "size": record.Size,
- })
- }
|