2026-03-08 16:54:41 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-08 23:02:15 +08:00
|
|
|
|
"io"
|
|
|
|
|
|
"mime"
|
2026-03-08 16:54:41 +08:00
|
|
|
|
"net/http"
|
2026-03-08 23:02:15 +08:00
|
|
|
|
"path/filepath"
|
2026-03-08 16:54:41 +08:00
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
2026-03-08 23:02:15 +08:00
|
|
|
|
"x-agents/server/internal/repository"
|
2026-03-08 16:54:41 +08:00
|
|
|
|
"x-agents/server/internal/service"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type UploadHandler struct {
|
2026-03-08 23:02:15 +08:00
|
|
|
|
uploadService *service.UploadService
|
|
|
|
|
|
knowledgeRepo *repository.KnowledgeRepository
|
2026-03-08 16:54:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 23:02:15 +08:00
|
|
|
|
func NewUploadHandler(uploadService *service.UploadService, knowledgeRepo *repository.KnowledgeRepository) *UploadHandler {
|
|
|
|
|
|
return &UploadHandler{uploadService: uploadService, knowledgeRepo: knowledgeRepo}
|
2026-03-08 16:54:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:26:04 +08:00
|
|
|
|
// @Summary 上传文件
|
|
|
|
|
|
// @Description 上传文件到服务器(本地存储或MinIO)
|
|
|
|
|
|
// @Tags 文件上传
|
|
|
|
|
|
// @Accept multipart/form-data
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param file formData file true "要上传的文件"
|
|
|
|
|
|
// @Success 200 {object} map[string]interface{}
|
|
|
|
|
|
// @Failure 400 {object} map[string]string
|
|
|
|
|
|
// @Failure 500 {object} map[string]string
|
|
|
|
|
|
// @Router /api/file_upload [post]
|
2026-03-08 16:54:41 +08:00
|
|
|
|
func (h *UploadHandler) Upload(c *gin.Context) {
|
|
|
|
|
|
file, err := c.FormFile("file")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "No file uploaded"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查文件大小(最大 100MB)
|
|
|
|
|
|
if file.Size > 100*1024*1024 {
|
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "File too large (max 100MB)"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result, err := h.uploadService.Upload(file)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !result.Success {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, result)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, result)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 14:26:04 +08:00
|
|
|
|
// @Summary 删除文件
|
|
|
|
|
|
// @Description 删除指定文件
|
|
|
|
|
|
// @Tags 文件上传
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce json
|
|
|
|
|
|
// @Param filename path string true "文件名"
|
|
|
|
|
|
// @Success 200 {object} map[string]interface{}
|
|
|
|
|
|
// @Failure 400 {object} map[string]string
|
|
|
|
|
|
// @Failure 500 {object} map[string]string
|
|
|
|
|
|
// @Router /api/file_upload/{filename} [delete]
|
2026-03-08 16:54:41 +08:00
|
|
|
|
func (h *UploadHandler) Delete(c *gin.Context) {
|
|
|
|
|
|
filename := c.Param("filename")
|
|
|
|
|
|
if filename == "" {
|
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "Filename is required"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := h.uploadService.DeleteFile(filename); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "File deleted"})
|
|
|
|
|
|
}
|
2026-03-08 23:02:15 +08:00
|
|
|
|
|
2026-03-11 14:26:04 +08:00
|
|
|
|
// @Summary 代理文件访问
|
|
|
|
|
|
// @Description 代理访问文件,解决 MinIO 内网和 HTTPS 问题
|
|
|
|
|
|
// @Tags 文件上传
|
|
|
|
|
|
// @Accept json
|
|
|
|
|
|
// @Produce octet-stream
|
|
|
|
|
|
// @Param key query string true "文件Key"
|
|
|
|
|
|
// @Param kb_id query string false "知识库ID"
|
|
|
|
|
|
// @Success 200 {file} binary
|
|
|
|
|
|
// @Failure 400 {object} map[string]string
|
|
|
|
|
|
// @Failure 500 {object} map[string]string
|
|
|
|
|
|
// @Router /api/file_proxy [get]
|
2026-03-08 23:02:15 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|