filestore.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. package acp
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "path/filepath"
  7. "strings"
  8. "sync"
  9. "time"
  10. "github.com/google/uuid"
  11. "github.com/minio/minio-go/v7"
  12. minioUtil "fwh-toolbox-server/utils/minio"
  13. "go.uber.org/zap"
  14. )
  15. const AIFileBucket = "fwh-toolbox-ai-file"
  16. const AIFileExpireDays = 7
  17. // FileRecord 文件记录
  18. type FileRecord struct {
  19. Key string `json:"key"` // MinIO object key
  20. FileName string `json:"fileName"` // 原始文件名
  21. Path string `json:"path"` // Agent 请求的路径
  22. Size int64 `json:"size"` // 文件大小
  23. CreatedAt int64 `json:"createdAt"` // 创建时间戳
  24. }
  25. // FileStore 会话级别的虚拟文件系统
  26. type FileStore struct {
  27. mu sync.RWMutex
  28. files map[string]*FileRecord // path → FileRecord(同路径覆盖更新)
  29. }
  30. // NewFileStore 创建文件存储
  31. func NewFileStore() *FileStore {
  32. return &FileStore{
  33. files: make(map[string]*FileRecord),
  34. }
  35. }
  36. // WriteFile 将内容上传到 MinIO,返回文件记录
  37. func (fs *FileStore) WriteFile(userId, sessionId, path, content string) (*FileRecord, error) {
  38. fileName := filepath.Base(path)
  39. uniqueId := uuid.New().String()[:8]
  40. objectKey := fmt.Sprintf("%s/%s/%s_%s", userId, sessionId, uniqueId, fileName)
  41. // 上传到 MinIO(带7天过期)
  42. reader := strings.NewReader(content)
  43. ctx := context.Background()
  44. _, err := minioUtil.Client.PutObject(ctx, AIFileBucket, objectKey, reader, int64(len(content)), minio.PutObjectOptions{
  45. ContentType: detectContentType(fileName),
  46. Expires: time.Now().Add(AIFileExpireDays * 24 * time.Hour),
  47. })
  48. if err != nil {
  49. return nil, fmt.Errorf("上传文件到MinIO失败: %w", err)
  50. }
  51. record := &FileRecord{
  52. Key: objectKey,
  53. FileName: fileName,
  54. Path: path,
  55. Size: int64(len(content)),
  56. CreatedAt: time.Now().Unix(),
  57. }
  58. // 存入内存映射(同路径覆盖)
  59. fs.mu.Lock()
  60. fs.files[path] = record
  61. fs.mu.Unlock()
  62. zap.L().Info("[ACP-FileStore] 文件已保存到MinIO",
  63. zap.String("path", path),
  64. zap.String("key", objectKey),
  65. zap.String("fileName", fileName),
  66. )
  67. return record, nil
  68. }
  69. // ReadFile 从 MinIO 读取文件内容
  70. func (fs *FileStore) ReadFile(path string) (string, error) {
  71. fs.mu.RLock()
  72. record, ok := fs.files[path]
  73. fs.mu.RUnlock()
  74. if !ok {
  75. return "", fmt.Errorf("文件不存在: %s", path)
  76. }
  77. ctx := context.Background()
  78. obj, err := minioUtil.Client.GetObject(ctx, AIFileBucket, record.Key, minio.GetObjectOptions{})
  79. if err != nil {
  80. return "", fmt.Errorf("从MinIO读取文件失败: %w", err)
  81. }
  82. defer obj.Close()
  83. data, err := io.ReadAll(obj)
  84. if err != nil {
  85. return "", fmt.Errorf("读取文件内容失败: %w", err)
  86. }
  87. return string(data), nil
  88. }
  89. // HasFile 检查路径是否有文件记录
  90. func (fs *FileStore) HasFile(path string) bool {
  91. fs.mu.RLock()
  92. defer fs.mu.RUnlock()
  93. _, ok := fs.files[path]
  94. return ok
  95. }
  96. // GetFiles 获取所有文件记录
  97. func (fs *FileStore) GetFiles() []FileRecord {
  98. fs.mu.RLock()
  99. defer fs.mu.RUnlock()
  100. result := make([]FileRecord, 0, len(fs.files))
  101. for _, record := range fs.files {
  102. result = append(result, *record)
  103. }
  104. return result
  105. }
  106. // GetFileByKey 按 MinIO key 查找文件记录
  107. func (fs *FileStore) GetFileByKey(key string) *FileRecord {
  108. fs.mu.RLock()
  109. defer fs.mu.RUnlock()
  110. for _, record := range fs.files {
  111. if record.Key == key {
  112. return record
  113. }
  114. }
  115. return nil
  116. }
  117. // detectContentType 根据文件扩展名推断 Content-Type
  118. func detectContentType(fileName string) string {
  119. ext := strings.ToLower(filepath.Ext(fileName))
  120. switch ext {
  121. case ".txt":
  122. return "text/plain; charset=utf-8"
  123. case ".json":
  124. return "application/json"
  125. case ".html", ".htm":
  126. return "text/html"
  127. case ".css":
  128. return "text/css"
  129. case ".js":
  130. return "application/javascript"
  131. case ".ts":
  132. return "application/typescript"
  133. case ".go":
  134. return "text/x-go"
  135. case ".py":
  136. return "text/x-python"
  137. case ".md":
  138. return "text/markdown"
  139. case ".csv":
  140. return "text/csv"
  141. case ".xml":
  142. return "application/xml"
  143. case ".yaml", ".yml":
  144. return "text/yaml"
  145. case ".sh":
  146. return "text/x-shellscript"
  147. default:
  148. return "application/octet-stream"
  149. }
  150. }