|
|
@@ -2,8 +2,8 @@ package storage
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
- "encoding/base64"
|
|
|
"fmt"
|
|
|
+ "os"
|
|
|
"path/filepath"
|
|
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
|
@@ -26,6 +26,9 @@ func GetFileStore() *filestore.FileStore {
|
|
|
return mcpFileStore
|
|
|
}
|
|
|
|
|
|
+// localTempDir 用于存放从 MinIO 下载的临时文件
|
|
|
+const localTempDir = "/tmp/fwh-toolbox-agent/downloads"
|
|
|
+
|
|
|
func registerFileTools(s *server.MCPServer) {
|
|
|
s.AddTool(
|
|
|
mcp.NewTool("storage_file_read",
|
|
|
@@ -36,16 +39,16 @@ func registerFileTools(s *server.MCPServer) {
|
|
|
)
|
|
|
|
|
|
s.AddTool(
|
|
|
- mcp.NewTool("storage_file_read_base64",
|
|
|
- mcp.WithDescription("读取用户上传的文件内容并返回 base64 编码。支持任意文件类型(图片、PDF、压缩包等)。图片文件读取后可直接用于 AI 视觉分析。"),
|
|
|
+ mcp.NewTool("storage_file_read_to_local",
|
|
|
+ mcp.WithDescription("将用户上传的文件下载到本地临时路径。支持任意文件类型(图片、PDF、压缩包等)。返回本地文件路径,可直接用于后续处理或分析。"),
|
|
|
mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}")),
|
|
|
),
|
|
|
- handleFileReadBase64,
|
|
|
+ handleFileReadToLocal,
|
|
|
)
|
|
|
|
|
|
s.AddTool(
|
|
|
mcp.NewTool("storage_file_write",
|
|
|
- mcp.WithDescription("将文本内容写入文件存储(MinIO)。可用于保存分析结果、生成报告等。最大 20MB。文件路径会自动添加 /uploads/ 前缀。如需上传二进制文件请使用 storage_file_upload。"),
|
|
|
+ mcp.WithDescription("将文本内容写入文件存储(MinIO)。可用于保存分析结果、生成报告等。最大 20MB。文件路径会自动添加 /uploads/ 前缀。如需上传本地文件请使用 storage_file_upload。"),
|
|
|
mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}(如 /uploads/report.csv)")),
|
|
|
mcp.WithString("content", mcp.Required(), mcp.Description("要写入的文本内容(最大 20MB)")),
|
|
|
mcp.WithString("userId", mcp.Description("用户标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-agent)")),
|
|
|
@@ -63,9 +66,9 @@ func registerFileTools(s *server.MCPServer) {
|
|
|
|
|
|
s.AddTool(
|
|
|
mcp.NewTool("storage_file_upload",
|
|
|
- mcp.WithDescription("将任意类型文件上传到文件存储(MinIO)。内容需使用 base64 编码传入。支持二进制文件(图片、PDF、压缩包、Excel 等)和文本文件。最大 100MB。"),
|
|
|
- mcp.WithString("fileName", mcp.Required(), mcp.Description("目标文件名(如 data.zip、report.pdf、image.png)")),
|
|
|
- mcp.WithString("content_base64", mcp.Required(), mcp.Description("文件内容的 base64 编码字符串(可通过读取本地文件后 base64 编码获得)")),
|
|
|
+ mcp.WithDescription("将本地文件上传到文件存储(MinIO)。支持任意文件类型(图片、PDF、压缩包、Excel 等)。最大 100MB。直接读取本地文件路径,无需 base64 编码。"),
|
|
|
+ mcp.WithString("localPath", mcp.Required(), mcp.Description("本地文件的绝对路径(如 /tmp/data.zip、/home/user/report.pdf)")),
|
|
|
+ mcp.WithString("fileName", mcp.Description("上传后的文件名(不传则使用本地文件名)")),
|
|
|
mcp.WithString("userId", mcp.Description("用户标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-agent)")),
|
|
|
mcp.WithString("sessionId", mcp.Description("会话标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-session)")),
|
|
|
),
|
|
|
@@ -95,7 +98,7 @@ func handleFileRead(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
|
|
|
return helper.TextResult(content), nil
|
|
|
}
|
|
|
|
|
|
-func handleFileReadBase64(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
|
+func handleFileReadToLocal(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
|
path, err := req.RequireString("path")
|
|
|
if err != nil {
|
|
|
return helper.ParamError("path"), nil
|
|
|
@@ -114,12 +117,23 @@ func handleFileReadBase64(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
|
|
|
return helper.ServiceError("读取文件", err), nil
|
|
|
}
|
|
|
|
|
|
- encoded := base64.StdEncoding.EncodeToString(data)
|
|
|
+ // 确保临时目录存在
|
|
|
+ if err := os.MkdirAll(localTempDir, 0755); err != nil {
|
|
|
+ return helper.ServiceError("创建临时目录", err), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // 写入本地临时文件
|
|
|
+ fileName := filepath.Base(path)
|
|
|
+ localPath := filepath.Join(localTempDir, fileName)
|
|
|
+ if err := os.WriteFile(localPath, data, 0644); err != nil {
|
|
|
+ return helper.ServiceError("写入本地文件", err), nil
|
|
|
+ }
|
|
|
|
|
|
return helper.JSONResult(map[string]any{
|
|
|
- "path": path,
|
|
|
- "size": len(data),
|
|
|
- "content_base64": encoded,
|
|
|
+ "message": "文件已下载到本地",
|
|
|
+ "localPath": localPath,
|
|
|
+ "fileName": fileName,
|
|
|
+ "size": len(data),
|
|
|
}), nil
|
|
|
}
|
|
|
|
|
|
@@ -174,33 +188,43 @@ func handleFileWrite(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
|
|
|
}
|
|
|
|
|
|
func handleFileUpload(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
|
- fileName, err := req.RequireString("fileName")
|
|
|
- if err != nil {
|
|
|
- return helper.ParamError("fileName"), nil
|
|
|
- }
|
|
|
- contentBase64, err := req.RequireString("content_base64")
|
|
|
+ localPath, err := req.RequireString("localPath")
|
|
|
if err != nil {
|
|
|
- return helper.ParamError("content_base64"), nil
|
|
|
+ return helper.ParamError("localPath"), nil
|
|
|
}
|
|
|
|
|
|
if mcpFileStore == nil {
|
|
|
return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
|
|
|
}
|
|
|
|
|
|
- // base64 解码
|
|
|
- data, err := base64.StdEncoding.DecodeString(contentBase64)
|
|
|
+ // 检查本地文件是否存在
|
|
|
+ info, err := os.Stat(localPath)
|
|
|
if err != nil {
|
|
|
- // 尝试 RawStdEncoding(无 padding)
|
|
|
- data, err = base64.RawStdEncoding.DecodeString(contentBase64)
|
|
|
- if err != nil {
|
|
|
- return helper.ParamInvalidError("content_base64", "base64 解码失败"), nil
|
|
|
+ if os.IsNotExist(err) {
|
|
|
+ return helper.ParamInvalidError("localPath", fmt.Sprintf("文件不存在: %s", localPath)), nil
|
|
|
}
|
|
|
+ return helper.ServiceError("检查文件", err), nil
|
|
|
+ }
|
|
|
+ if info.IsDir() {
|
|
|
+ return helper.ParamInvalidError("localPath", "路径是目录,不是文件"), nil
|
|
|
}
|
|
|
|
|
|
// 限制文件大小(最大 100MB)
|
|
|
const maxSize = 100 * 1024 * 1024
|
|
|
- if len(data) > maxSize {
|
|
|
- return helper.ParamInvalidError("content_base64", fmt.Sprintf("文件大小 %d bytes 超过 100MB 限制", len(data))), nil
|
|
|
+ if info.Size() > maxSize {
|
|
|
+ return helper.ParamInvalidError("localPath", fmt.Sprintf("文件大小 %d bytes 超过 100MB 限制", info.Size())), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取本地文件
|
|
|
+ data, err := os.ReadFile(localPath)
|
|
|
+ if err != nil {
|
|
|
+ return helper.ServiceError("读取本地文件", err), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确定文件名
|
|
|
+ fileName := req.GetString("fileName", "")
|
|
|
+ if fileName == "" {
|
|
|
+ fileName = filepath.Base(localPath)
|
|
|
}
|
|
|
|
|
|
userId := req.GetString("userId", "mcp-agent")
|