Forráskód Böngészése

fix:优化数据路径storage_file直接传递文件路径,降低token消耗

Jerry Liang 2 hónapja
szülő
commit
94aaddd99a

+ 13 - 12
service/cmd/mcp/.kiro/steering/file-output-rules.md

@@ -60,16 +60,17 @@ $$FILE_DOWNLOAD:{"key":"download/jerry/sess_abc123/a1b2c3d4_report.csv","fileNam
 - 适用于:CSV、JSON、TXT、Markdown、代码文件等文本类型
 - 返回纯文本内容,可直接用于分析
 
-### storage_file_read_base64(任意文件)
-- 适用于:图片、PDF、压缩包、Excel 等二进制文件
-- 返回 base64 编码内容
-- **图片文件**(png/jpg/gif/webp)读取后可直接用于 AI 视觉分析
-- 其他二进制文件读取后主要用于中转(如重新上传、返回给前端下载)
+### storage_file_read_to_local(任意文件)
+- 适用于:图片、PDF、压缩包、Excel 等任意类型文件
+- 将文件从 MinIO 下载到本地临时路径 `/tmp/fwh-toolbox-agent/downloads/`
+- 返回本地文件路径,可用于后续处理:
+  - 图片文件:通过本地路径读取后用于 AI 视觉分析
+  - Excel/PDF:通过工具解析后提取文本
+  - 任意文件:中转、转发、或作为其他工具的输入
 
 ### 判断标准
 - 用户要求分析文本文件 → `storage_file_read`
-- 用户要求分析图片内容 → `storage_file_read_base64`
-- 需要获取文件原始数据(中转、转发) → `storage_file_read_base64`
+- 用户要求处理非文本文件(图片、PDF、Excel 等) → `storage_file_read_to_local`
 - 不确定文件类型时 → 先用 `storage_file_list` 查看文件信息,再选择合适的读取方式
 
 ## 写文件策略
@@ -86,18 +87,18 @@ $$FILE_DOWNLOAD:{"key":"download/jerry/sess_abc123/a1b2c3d4_report.csv","fileNam
 
 #### 任意类型文件(二进制文件、图片、PDF、压缩包、Excel 等)
 - 使用 `storage_file_upload` 工具上传(最大 100MB)
-- 需要将文件内容 base64 编码后传入 `content_base64` 参数
-- 本地文件可通过 shell 命令 `base64 -w0 <file>` 获取编码内容
+- 传入本地文件的绝对路径(`localPath` 参数),服务端直接读取文件
+- 无需 base64 编码,不占用 token
 - 尽量传入 `userId` 和 `sessionId`
 - 上传成功后同样输出 `$$FILE_DOWNLOAD:...$$` 标记
 
 ### 临时文件(中间处理、缓存等)
-- 写入 `/temp/fwh-toolbox-agent/` 目录
+- 写入 `/tmp/fwh-toolbox-agent/` 目录
 - 处理完成后无需输出下载标记
 
 ### 判断标准
 - 用户说"下载"、"导出"、"生成报告"、"保存为文件" → 使用 storage_file_write 或 storage_file_upload + 输出标记
 - 文本内容直接写入 → `storage_file_write`
-- 本地已有文件需要上传 / 二进制文件 → `storage_file_upload`
-- 中间计算结果、日志 → 写入 /temp
+- 本地已有文件需要上传 / 二进制文件 → `storage_file_upload`(传 localPath)
+- 中间计算结果、日志 → 写入 /tmp
 - 不确定时 → 询问用户

+ 51 - 27
service/cmd/mcp/tools/storage/file.go

@@ -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")