package acp import ( "context" "fmt" "io" "path/filepath" "strings" "sync" "time" "github.com/google/uuid" "github.com/minio/minio-go/v7" minioUtil "fwh-toolbox-server/utils/minio" "go.uber.org/zap" ) const AIFileBucket = "fwh-toolbox-ai-file" const AIFileExpireDays = 7 // FileRecord 文件记录 type FileRecord struct { Key string `json:"key"` // MinIO object key FileName string `json:"fileName"` // 原始文件名 Path string `json:"path"` // Agent 请求的路径 Size int64 `json:"size"` // 文件大小 CreatedAt int64 `json:"createdAt"` // 创建时间戳 } // FileStore 会话级别的虚拟文件系统 type FileStore struct { mu sync.RWMutex files map[string]*FileRecord // path → FileRecord(同路径覆盖更新) } // NewFileStore 创建文件存储 func NewFileStore() *FileStore { return &FileStore{ files: make(map[string]*FileRecord), } } // WriteFile 将内容上传到 MinIO,返回文件记录 func (fs *FileStore) WriteFile(userId, sessionId, path, content string) (*FileRecord, error) { fileName := filepath.Base(path) uniqueId := uuid.New().String()[:8] objectKey := fmt.Sprintf("%s/%s/%s_%s", userId, sessionId, uniqueId, fileName) // 上传到 MinIO(带7天过期) reader := strings.NewReader(content) ctx := context.Background() _, err := minioUtil.Client.PutObject(ctx, AIFileBucket, objectKey, reader, int64(len(content)), minio.PutObjectOptions{ ContentType: detectContentType(fileName), Expires: time.Now().Add(AIFileExpireDays * 24 * time.Hour), }) if err != nil { return nil, fmt.Errorf("上传文件到MinIO失败: %w", err) } record := &FileRecord{ Key: objectKey, FileName: fileName, Path: path, Size: int64(len(content)), CreatedAt: time.Now().Unix(), } // 存入内存映射(同路径覆盖) fs.mu.Lock() fs.files[path] = record fs.mu.Unlock() zap.L().Info("[ACP-FileStore] 文件已保存到MinIO", zap.String("path", path), zap.String("key", objectKey), zap.String("fileName", fileName), ) return record, nil } // ReadFile 从 MinIO 读取文件内容 func (fs *FileStore) ReadFile(path string) (string, error) { fs.mu.RLock() record, ok := fs.files[path] fs.mu.RUnlock() if !ok { return "", fmt.Errorf("文件不存在: %s", path) } ctx := context.Background() obj, err := minioUtil.Client.GetObject(ctx, AIFileBucket, record.Key, minio.GetObjectOptions{}) if err != nil { return "", fmt.Errorf("从MinIO读取文件失败: %w", err) } defer obj.Close() data, err := io.ReadAll(obj) if err != nil { return "", fmt.Errorf("读取文件内容失败: %w", err) } return string(data), nil } // HasFile 检查路径是否有文件记录 func (fs *FileStore) HasFile(path string) bool { fs.mu.RLock() defer fs.mu.RUnlock() _, ok := fs.files[path] return ok } // GetFiles 获取所有文件记录 func (fs *FileStore) GetFiles() []FileRecord { fs.mu.RLock() defer fs.mu.RUnlock() result := make([]FileRecord, 0, len(fs.files)) for _, record := range fs.files { result = append(result, *record) } return result } // GetFileByKey 按 MinIO key 查找文件记录 func (fs *FileStore) GetFileByKey(key string) *FileRecord { fs.mu.RLock() defer fs.mu.RUnlock() for _, record := range fs.files { if record.Key == key { return record } } return nil } // detectContentType 根据文件扩展名推断 Content-Type func detectContentType(fileName string) string { ext := strings.ToLower(filepath.Ext(fileName)) switch ext { case ".txt": return "text/plain; charset=utf-8" case ".json": return "application/json" case ".html", ".htm": return "text/html" case ".css": return "text/css" case ".js": return "application/javascript" case ".ts": return "application/typescript" case ".go": return "text/x-go" case ".py": return "text/x-python" case ".md": return "text/markdown" case ".csv": return "text/csv" case ".xml": return "application/xml" case ".yaml", ".yml": return "text/yaml" case ".sh": return "text/x-shellscript" default: return "application/octet-stream" } }