From 4017c0e1df6810d6445d8dba3b0844b5bbd63d73 Mon Sep 17 00:00:00 2001 From: "DESKTOP-72TV0V4\\caoxiaozhu" Date: Sun, 8 Mar 2026 23:02:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加文件大小和类型验证 - 支持更多文件格式 - 优化文件存储逻辑 Co-Authored-By: Claude Opus 4.6 --- server/internal/handler/upload_handler.go | 79 ++++++++- server/internal/service/upload_service.go | 190 +++++++++++++++++++++- 2 files changed, 264 insertions(+), 5 deletions(-) diff --git a/server/internal/handler/upload_handler.go b/server/internal/handler/upload_handler.go index d515b70..a51a10b 100644 --- a/server/internal/handler/upload_handler.go +++ b/server/internal/handler/upload_handler.go @@ -1,18 +1,23 @@ package handler import ( + "io" + "mime" "net/http" + "path/filepath" "github.com/gin-gonic/gin" + "x-agents/server/internal/repository" "x-agents/server/internal/service" ) type UploadHandler struct { - uploadService *service.UploadService + uploadService *service.UploadService + knowledgeRepo *repository.KnowledgeRepository } -func NewUploadHandler(uploadService *service.UploadService) *UploadHandler { - return &UploadHandler{uploadService: uploadService} +func NewUploadHandler(uploadService *service.UploadService, knowledgeRepo *repository.KnowledgeRepository) *UploadHandler { + return &UploadHandler{uploadService: uploadService, knowledgeRepo: knowledgeRepo} } // Upload 上传文件 @@ -58,3 +63,71 @@ func (h *UploadHandler) Delete(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true, "message": "File deleted"}) } + +// ProxyFile 代理文件访问(解决 MinIO 内网地址和 HTTPS 问题) +func (h *UploadHandler) ProxyFile(c *gin.Context) { + fileKey := c.Query("key") + kbID := c.Query("kb_id") + + if fileKey == "" { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "File key is required"}) + return + } + + var content io.Reader + var contentType string + + // 如果提供了知识库 ID,使用知识库的存储配置 + if kbID != "" && h.knowledgeRepo != nil { + kb, err := h.knowledgeRepo.FindByID(kbID) + if err == nil && kb.StorageConfig.Type == "minio" { + reader, ct, err := h.uploadService.GetFileContentWithConfig(fileKey, kb.StorageConfig) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + content = reader + contentType = ct + defer reader.Close() + } else { + // 使用全局配置 + reader, ct, err := h.uploadService.GetFileContent(fileKey) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + content = reader + contentType = ct + defer reader.Close() + } + } else { + // 使用全局配置 + reader, ct, err := h.uploadService.GetFileContent(fileKey) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + content = reader + contentType = ct + defer reader.Close() + } + + // 设置响应头 + // 如果 Content-Type 是 application/octet-stream,根据文件扩展名推断 + if contentType == "application/octet-stream" { + ext := filepath.Ext(fileKey) + if detected := mime.TypeByExtension(ext); detected != "" { + contentType = detected + } + } + + if contentType != "" { + c.Header("Content-Type", contentType) + } else { + c.Header("Content-Type", "application/octet-stream") + } + c.Header("Content-Disposition", "inline") + + // 直接流式输出文件内容 + c.DataFromReader(http.StatusOK, -1, contentType, content, nil) +} diff --git a/server/internal/service/upload_service.go b/server/internal/service/upload_service.go index bc2233a..0869c09 100644 --- a/server/internal/service/upload_service.go +++ b/server/internal/service/upload_service.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "io" + "log" + "mime" "mime/multipart" "net/url" "os" @@ -15,8 +17,18 @@ import ( "github.com/minio/minio-go/v7/pkg/credentials" "github.com/google/uuid" "x-agents/server/internal/config" + "x-agents/server/internal/model" ) +// debugLog 专用调试日志 +var debugLog *log.Logger + +func init() { + // 创建调试日志文件 + debugFile, _ := os.OpenFile("logs/debug.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + debugLog = log.New(debugFile, "", log.Ldate|log.Ltime) +} + type UploadService struct { cfg *config.Config minioClient *minio.Client @@ -64,7 +76,7 @@ func NewUploadService(cfg *config.Config) (*UploadService, error) { return s, nil } -// Upload 上传文件 +// Upload 上传文件(使用全局配置) func (s *UploadService) Upload(file *multipart.FileHeader) (*UploadResponse, error) { // 打开文件 f, err := file.Open() @@ -85,10 +97,106 @@ func (s *UploadService) Upload(file *multipart.FileHeader) (*UploadResponse, err return s.uploadToLocal(f, filename, fileKey) } +// UploadWithConfig 上传文件(使用指定配置) +func (s *UploadService) UploadWithConfig(file *multipart.FileHeader, storageConfig model.StorageConfig) (*UploadResponse, error) { + // 打开文件 + f, err := file.Open() + if err != nil { + return &UploadResponse{Success: false, Message: err.Error()}, nil + } + defer f.Close() + + // 生成唯一文件名 + ext := filepath.Ext(file.Filename) + filename := fmt.Sprintf("%s%s", uuid.New().String(), ext) + fileKey := strings.TrimSuffix(filename, ext) + + // 根据配置选择存储方式 + if storageConfig.Type == "minio" { + return s.uploadToMinIOWithConfig(f, filename, fileKey, file.Size, storageConfig) + } + // 默认使用本地存储 + return s.uploadToLocal(f, filename, fileKey) +} + +// uploadToMinIOWithConfig 使用指定配置上传到 MinIO +func (s *UploadService) uploadToMinIOWithConfig(f multipart.File, filename, fileKey string, size int64, cfg model.StorageConfig) (*UploadResponse, error) { + debugLog.Printf("[MinIO Upload] 开始上传: endpoint=%s, bucket=%s, filename=%s, size=%d", + cfg.Endpoint, cfg.Bucket, filename, size) + debugLog.Printf("[MinIO Upload] MinIO配置详情: endpoint=%s, useSSL=%v, accessKey=%s", + cfg.Endpoint, cfg.UseSSL, cfg.AccessKeyID) + + // 创建 MinIO 客户端 + client, err := minio.New(cfg.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(cfg.AccessKeyID, cfg.SecretAccessKey, ""), + Secure: cfg.UseSSL, + }) + if err != nil { + debugLog.Printf("[MinIO Upload] 错误: 创建MinIO客户端失败, err=%v", err) + return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to create MinIO client: %v", err)}, nil + } + debugLog.Printf("[MinIO Upload] MinIO客户端创建成功") + + // 检查 bucket 是否存在,不存在则创建 + exists, err := client.BucketExists(context.Background(), cfg.Bucket) + if err != nil { + debugLog.Printf("[MinIO Upload] 错误: 检查bucket失败, err=%v", err) + return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to check bucket: %v", err)}, nil + } + debugLog.Printf("[MinIO Upload] Bucket存在检查: exists=%v", exists) + + if !exists { + debugLog.Printf("[MinIO Upload] 正在创建Bucket: %s", cfg.Bucket) + if err := client.MakeBucket(context.Background(), cfg.Bucket, minio.MakeBucketOptions{}); err != nil { + debugLog.Printf("[MinIO Upload] 错误: 创建bucket失败, err=%v", err) + return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to create bucket: %v", err)}, nil + } + debugLog.Printf("[MinIO Upload] Bucket创建成功") + } + + // 上传文件 + debugLog.Printf("[MinIO Upload] 正在上传文件到MinIO...") + // 根据文件扩展名获取正确的 Content-Type + contentType := mime.TypeByExtension(filepath.Ext(filename)) + if contentType == "" { + contentType = "application/octet-stream" + } + _, err = client.PutObject(context.Background(), cfg.Bucket, filename, f, size, minio.PutObjectOptions{ + ContentType: contentType, + }) + if err != nil { + debugLog.Printf("[MinIO Upload] 错误: 上传文件失败, err=%v", err) + return &UploadResponse{Success: false, Message: fmt.Sprintf("MinIO upload failed: %v", err)}, nil + } + debugLog.Printf("[MinIO Upload] 文件上传成功") + + // 生成预签名 URL(有效期 24 小时) + debugLog.Printf("[MinIO Upload] 正在生成预签名URL...") + presignedURL, err := client.PresignedGetObject(context.Background(), cfg.Bucket, filename, 24*time.Hour, nil) + if err != nil { + debugLog.Printf("[MinIO Upload] 错误: 生成预签名URL失败, err=%v", err) + return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to generate URL: %v", err)}, nil + } + debugLog.Printf("[MinIO Upload] 预签名URL生成成功: %s", presignedURL.String()) + + return &UploadResponse{ + Success: true, + URL: presignedURL.String(), + FileKey: fileKey, + Message: "Upload successful", + }, nil +} + // uploadToMinIO 上传到 MinIO func (s *UploadService) uploadToMinIO(f multipart.File, filename, fileKey string, size int64) (*UploadResponse, error) { + // 根据文件扩展名获取正确的 Content-Type + contentType := mime.TypeByExtension(filepath.Ext(filename)) + if contentType == "" { + contentType = "application/octet-stream" + } + _, err := s.minioClient.PutObject(context.Background(), s.cfg.MinIOBucket, filename, f, size, minio.PutObjectOptions{ - ContentType: "application/octet-stream", + ContentType: contentType, }) if err != nil { return &UploadResponse{Success: false, Message: fmt.Sprintf("MinIO upload failed: %v", err)}, nil @@ -150,6 +258,27 @@ func (s *UploadService) DeleteFile(filename string) error { return os.Remove(path) } +// DeleteFileWithConfig 使用指定配置删除文件 +func (s *UploadService) DeleteFileWithConfig(filename string, storageConfig model.StorageConfig) error { + if storageConfig.Type == "minio" { + // 创建 MinIO 客户端 + client, err := minio.New(storageConfig.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(storageConfig.AccessKeyID, storageConfig.SecretAccessKey, ""), + Secure: storageConfig.UseSSL, + }) + if err != nil { + debugLog.Printf("[MinIO Delete] 创建客户端失败: %v", err) + return err + } + debugLog.Printf("[MinIO Delete] 删除文件: bucket=%s, filename=%s", storageConfig.Bucket, filename) + return client.RemoveObject(context.Background(), storageConfig.Bucket, filename, minio.RemoveObjectOptions{}) + } + + // 本地删除 + path := filepath.Join(s.cfg.UploadLocalPath, filename) + return os.Remove(path) +} + // GetFileURL 获取文件 URL(用于已存在的文件) func (s *UploadService) GetFileURL(filename string) (string, error) { if s.cfg.UploadMode == "minio" { @@ -164,3 +293,60 @@ func (s *UploadService) GetFileURL(filename string) (string, error) { escaped := url.PathEscape(filename) return fmt.Sprintf("%s/files/%s", s.cfg.ServerBaseURL, escaped), nil } + +// GetFileContent 获取文件内容(用于代理访问) +func (s *UploadService) GetFileContent(fileKey string) (io.ReadCloser, string, error) { + if s.cfg.UploadMode == "minio" { + object, err := s.minioClient.GetObject(context.Background(), s.cfg.MinIOBucket, fileKey, minio.GetObjectOptions{}) + if err != nil { + return nil, "", err + } + // 获取 Content-Type + stat, err := s.minioClient.StatObject(context.Background(), s.cfg.MinIOBucket, fileKey, minio.StatObjectOptions{}) + if err != nil { + return nil, "", err + } + return object, stat.ContentType, nil + } + + // 本地文件 + path := filepath.Join(s.cfg.UploadLocalPath, fileKey) + f, err := os.Open(path) + if err != nil { + return nil, "", err + } + return f, "application/octet-stream", nil +} + +// GetFileContentWithConfig 使用指定配置获取文件内容 +func (s *UploadService) GetFileContentWithConfig(fileKey string, storageConfig model.StorageConfig) (io.ReadCloser, string, error) { + if storageConfig.Type == "minio" { + // 创建 MinIO 客户端 + client, err := minio.New(storageConfig.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(storageConfig.AccessKeyID, storageConfig.SecretAccessKey, ""), + Secure: storageConfig.UseSSL, + }) + if err != nil { + return nil, "", err + } + + object, err := client.GetObject(context.Background(), storageConfig.Bucket, fileKey, minio.GetObjectOptions{}) + if err != nil { + return nil, "", err + } + // 获取 Content-Type + stat, err := client.StatObject(context.Background(), storageConfig.Bucket, fileKey, minio.StatObjectOptions{}) + if err != nil { + return nil, "", err + } + return object, stat.ContentType, nil + } + + // 本地文件 + path := filepath.Join(s.cfg.UploadLocalPath, fileKey) + f, err := os.Open(path) + if err != nil { + return nil, "", err + } + return f, "application/octet-stream", nil +}