|
|
@@ -1,4 +1,4 @@
|
|
|
-package acp
|
|
|
+package filestore
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
@@ -29,29 +29,33 @@ type FileRecord struct {
|
|
|
|
|
|
// FileStore 会话级别的虚拟文件系统
|
|
|
type FileStore struct {
|
|
|
- mu sync.RWMutex
|
|
|
- files map[string]*FileRecord // path → FileRecord(同路径覆盖更新)
|
|
|
+ prefix string // "acp" 或 "mcp",用于区分文件来源
|
|
|
+ mu sync.RWMutex
|
|
|
+ files map[string]*FileRecord // path → FileRecord(同路径覆盖更新)
|
|
|
}
|
|
|
|
|
|
// NewFileStore 创建文件存储
|
|
|
-func NewFileStore() *FileStore {
|
|
|
+// prefix: "acp" 或 "mcp",对应 MinIO 中的子目录前缀
|
|
|
+func NewFileStore(prefix string) *FileStore {
|
|
|
return &FileStore{
|
|
|
- files: make(map[string]*FileRecord),
|
|
|
+ prefix: prefix,
|
|
|
+ files: make(map[string]*FileRecord),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// WriteFile 将内容上传到 MinIO,返回文件记录
|
|
|
+// objectKey 格式: {prefix}/{userId}/{sessionId}/{uuid}_{fileName}
|
|
|
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)
|
|
|
+ objectKey := fmt.Sprintf("%s/%s/%s/%s_%s", fs.prefix, 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),
|
|
|
+ ContentType: DetectContentType(fileName),
|
|
|
Expires: time.Now().Add(AIFileExpireDays * 24 * time.Hour),
|
|
|
})
|
|
|
if err != nil {
|
|
|
@@ -71,7 +75,8 @@ func (fs *FileStore) WriteFile(userId, sessionId, path, content string) (*FileRe
|
|
|
fs.files[path] = record
|
|
|
fs.mu.Unlock()
|
|
|
|
|
|
- zap.L().Info("[ACP-FileStore] 文件已保存到MinIO",
|
|
|
+ zap.L().Info("[FileStore] 文件已保存到MinIO",
|
|
|
+ zap.String("prefix", fs.prefix),
|
|
|
zap.String("path", path),
|
|
|
zap.String("key", objectKey),
|
|
|
zap.String("fileName", fileName),
|
|
|
@@ -80,6 +85,54 @@ func (fs *FileStore) WriteFile(userId, sessionId, path, content string) (*FileRe
|
|
|
return record, nil
|
|
|
}
|
|
|
|
|
|
+// WriteBytes 将字节内容上传到 MinIO(支持二进制文件)
|
|
|
+// objectKey 格式: {prefix}/{userId}/{timestamp}_{uuid}_{fileName}
|
|
|
+func (fs *FileStore) WriteBytes(userId, fileName string, data []byte, contentType string) (*FileRecord, error) {
|
|
|
+ uniqueId := uuid.New().String()[:8]
|
|
|
+ timestamp := time.Now().Unix()
|
|
|
+ objectKey := fmt.Sprintf("%s/%s/%d_%s_%s", fs.prefix, userId, timestamp, uniqueId, fileName)
|
|
|
+
|
|
|
+ // 虚拟路径(供 Agent readTextFile 使用)
|
|
|
+ virtualPath := fmt.Sprintf("/uploads/%s", fileName)
|
|
|
+
|
|
|
+ reader := strings.NewReader(string(data))
|
|
|
+ ctx := context.Background()
|
|
|
+
|
|
|
+ if contentType == "" {
|
|
|
+ contentType = DetectContentType(fileName)
|
|
|
+ }
|
|
|
+
|
|
|
+ _, err := minioUtil.Client.PutObject(ctx, AIFileBucket, objectKey, reader, int64(len(data)), minio.PutObjectOptions{
|
|
|
+ ContentType: contentType,
|
|
|
+ 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: virtualPath,
|
|
|
+ Size: int64(len(data)),
|
|
|
+ CreatedAt: time.Now().Unix(),
|
|
|
+ }
|
|
|
+
|
|
|
+ // 存入内存映射
|
|
|
+ fs.mu.Lock()
|
|
|
+ fs.files[virtualPath] = record
|
|
|
+ fs.mu.Unlock()
|
|
|
+
|
|
|
+ zap.L().Info("[FileStore] 文件已保存到MinIO",
|
|
|
+ zap.String("prefix", fs.prefix),
|
|
|
+ zap.String("path", virtualPath),
|
|
|
+ zap.String("key", objectKey),
|
|
|
+ zap.String("fileName", fileName),
|
|
|
+ )
|
|
|
+
|
|
|
+ return record, nil
|
|
|
+}
|
|
|
+
|
|
|
// ReadFile 从 MinIO 读取文件内容
|
|
|
func (fs *FileStore) ReadFile(path string) (string, error) {
|
|
|
fs.mu.RLock()
|
|
|
@@ -138,8 +191,15 @@ func (fs *FileStore) GetFileByKey(key string) *FileRecord {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
-// detectContentType 根据文件扩展名推断 Content-Type
|
|
|
-func detectContentType(fileName string) string {
|
|
|
+// Clear 清除所有文件记录(不删除 MinIO 文件)
|
|
|
+func (fs *FileStore) Clear() {
|
|
|
+ fs.mu.Lock()
|
|
|
+ fs.files = make(map[string]*FileRecord)
|
|
|
+ fs.mu.Unlock()
|
|
|
+}
|
|
|
+
|
|
|
+// DetectContentType 根据文件扩展名推断 Content-Type
|
|
|
+func DetectContentType(fileName string) string {
|
|
|
ext := strings.ToLower(filepath.Ext(fileName))
|
|
|
switch ext {
|
|
|
case ".txt":
|
|
|
@@ -168,6 +228,12 @@ func detectContentType(fileName string) string {
|
|
|
return "text/yaml"
|
|
|
case ".sh":
|
|
|
return "text/x-shellscript"
|
|
|
+ case ".vue":
|
|
|
+ return "text/x-vue"
|
|
|
+ case ".sql":
|
|
|
+ return "text/x-sql"
|
|
|
+ case ".log":
|
|
|
+ return "text/plain; charset=utf-8"
|
|
|
default:
|
|
|
return "application/octet-stream"
|
|
|
}
|