file.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. package storage
  2. import (
  3. "context"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "github.com/mark3labs/mcp-go/mcp"
  8. "github.com/mark3labs/mcp-go/server"
  9. "fwh-toolbox-server/cmd/mcp/tools/helper"
  10. "fwh-toolbox-server/utils/filestore"
  11. )
  12. // mcpFileStore MCP 专用的文件存储实例
  13. var mcpFileStore *filestore.FileStore
  14. // InitFileStore 初始化 MCP FileStore(由 main.go 调用)
  15. func InitFileStore(fs *filestore.FileStore) {
  16. mcpFileStore = fs
  17. }
  18. // GetFileStore 获取 MCP FileStore 实例
  19. func GetFileStore() *filestore.FileStore {
  20. return mcpFileStore
  21. }
  22. // localTempDir 用于存放从 MinIO 下载的临时文件
  23. const localTempDir = "/tmp/fwh-toolbox-agent/downloads"
  24. func registerFileTools(s *server.MCPServer) {
  25. s.AddTool(
  26. mcp.NewTool("storage_file_read",
  27. mcp.WithDescription("读取用户上传的文本文件内容(返回纯文本)。用户通过前端上传的文件存储在 /uploads/ 路径下。仅适用于文本类文件。"),
  28. mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}")),
  29. ),
  30. handleFileRead,
  31. )
  32. s.AddTool(
  33. mcp.NewTool("storage_file_read_to_local",
  34. mcp.WithDescription("将用户上传的文件下载到本地临时路径。支持任意文件类型(图片、PDF、压缩包等)。返回本地文件路径,可直接用于后续处理或分析。"),
  35. mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}")),
  36. ),
  37. handleFileReadToLocal,
  38. )
  39. s.AddTool(
  40. mcp.NewTool("storage_file_write",
  41. mcp.WithDescription("将文本内容写入文件存储(MinIO)。可用于保存分析结果、生成报告等。最大 20MB。文件路径会自动添加 /uploads/ 前缀。如需上传本地文件请使用 storage_file_upload。"),
  42. mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}(如 /uploads/report.csv)")),
  43. mcp.WithString("content", mcp.Required(), mcp.Description("要写入的文本内容(最大 20MB)")),
  44. mcp.WithString("userId", mcp.Description("用户标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-agent)")),
  45. mcp.WithString("sessionId", mcp.Description("会话标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-session)")),
  46. ),
  47. handleFileWrite,
  48. )
  49. s.AddTool(
  50. mcp.NewTool("storage_file_list",
  51. mcp.WithDescription("列出当前会话中用户上传的所有文件,返回文件名、路径、大小等信息。"),
  52. ),
  53. handleFileList,
  54. )
  55. s.AddTool(
  56. mcp.NewTool("storage_file_upload",
  57. mcp.WithDescription("将本地文件上传到文件存储(MinIO)。支持任意文件类型(图片、PDF、压缩包、Excel 等)。最大 100MB。直接读取本地文件路径,无需 base64 编码。"),
  58. mcp.WithString("localPath", mcp.Required(), mcp.Description("本地文件的绝对路径(如 /tmp/data.zip、/home/user/report.pdf)")),
  59. mcp.WithString("fileName", mcp.Description("上传后的文件名(不传则使用本地文件名)")),
  60. mcp.WithString("userId", mcp.Description("用户标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-agent)")),
  61. mcp.WithString("sessionId", mcp.Description("会话标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-session)")),
  62. ),
  63. handleFileUpload,
  64. )
  65. }
  66. func handleFileRead(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  67. path, err := req.RequireString("path")
  68. if err != nil {
  69. return helper.ParamError("path"), nil
  70. }
  71. if mcpFileStore == nil {
  72. return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
  73. }
  74. if !mcpFileStore.HasFile(path) {
  75. return helper.EmptyResult(fmt.Sprintf("文件不存在: %s", path)), nil
  76. }
  77. content, err := mcpFileStore.ReadFile(path)
  78. if err != nil {
  79. return helper.ServiceError("读取文件", err), nil
  80. }
  81. return helper.TextResult(content), nil
  82. }
  83. func handleFileReadToLocal(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  84. path, err := req.RequireString("path")
  85. if err != nil {
  86. return helper.ParamError("path"), nil
  87. }
  88. if mcpFileStore == nil {
  89. return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
  90. }
  91. if !mcpFileStore.HasFile(path) {
  92. return helper.EmptyResult(fmt.Sprintf("文件不存在: %s", path)), nil
  93. }
  94. data, err := mcpFileStore.ReadFileBytes(path)
  95. if err != nil {
  96. return helper.ServiceError("读取文件", err), nil
  97. }
  98. // 确保临时目录存在
  99. if err := os.MkdirAll(localTempDir, 0755); err != nil {
  100. return helper.ServiceError("创建临时目录", err), nil
  101. }
  102. // 写入本地临时文件
  103. fileName := filepath.Base(path)
  104. localPath := filepath.Join(localTempDir, fileName)
  105. if err := os.WriteFile(localPath, data, 0644); err != nil {
  106. return helper.ServiceError("写入本地文件", err), nil
  107. }
  108. return helper.JSONResult(map[string]any{
  109. "message": "文件已下载到本地",
  110. "localPath": localPath,
  111. "fileName": fileName,
  112. "size": len(data),
  113. }), nil
  114. }
  115. func handleFileList(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  116. if mcpFileStore == nil {
  117. return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
  118. }
  119. files := mcpFileStore.GetFiles()
  120. if len(files) == 0 {
  121. return helper.EmptyResult("当前没有上传的文件"), nil
  122. }
  123. return helper.JSONResult(files), nil
  124. }
  125. func handleFileWrite(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  126. path, err := req.RequireString("path")
  127. if err != nil {
  128. return helper.ParamError("path"), nil
  129. }
  130. content, err := req.RequireString("content")
  131. if err != nil {
  132. return helper.ParamError("content"), nil
  133. }
  134. // 限制文本内容大小(最大 20MB)
  135. const maxSize = 20 * 1024 * 1024
  136. if len(content) > maxSize {
  137. return helper.ParamInvalidError("content", fmt.Sprintf("内容大小 %d bytes 超过 20MB 限制", len(content))), nil
  138. }
  139. if mcpFileStore == nil {
  140. return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
  141. }
  142. userId := req.GetString("userId", "mcp-agent")
  143. sessionId := req.GetString("sessionId", "mcp-session")
  144. record, err := mcpFileStore.WriteFile(userId, sessionId, path, content)
  145. if err != nil {
  146. return helper.ServiceError("写入文件", err), nil
  147. }
  148. return helper.JSONResult(map[string]any{
  149. "message": "文件写入成功",
  150. "path": record.Path,
  151. "fileName": record.FileName,
  152. "size": record.Size,
  153. "key": record.Key,
  154. }), nil
  155. }
  156. func handleFileUpload(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  157. localPath, err := req.RequireString("localPath")
  158. if err != nil {
  159. return helper.ParamError("localPath"), nil
  160. }
  161. if mcpFileStore == nil {
  162. return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
  163. }
  164. // 检查本地文件是否存在
  165. info, err := os.Stat(localPath)
  166. if err != nil {
  167. if os.IsNotExist(err) {
  168. return helper.ParamInvalidError("localPath", fmt.Sprintf("文件不存在: %s", localPath)), nil
  169. }
  170. return helper.ServiceError("检查文件", err), nil
  171. }
  172. if info.IsDir() {
  173. return helper.ParamInvalidError("localPath", "路径是目录,不是文件"), nil
  174. }
  175. // 限制文件大小(最大 100MB)
  176. const maxSize = 100 * 1024 * 1024
  177. if info.Size() > maxSize {
  178. return helper.ParamInvalidError("localPath", fmt.Sprintf("文件大小 %d bytes 超过 100MB 限制", info.Size())), nil
  179. }
  180. // 读取本地文件
  181. data, err := os.ReadFile(localPath)
  182. if err != nil {
  183. return helper.ServiceError("读取本地文件", err), nil
  184. }
  185. // 确定文件名
  186. fileName := req.GetString("fileName", "")
  187. if fileName == "" {
  188. fileName = filepath.Base(localPath)
  189. }
  190. userId := req.GetString("userId", "mcp-agent")
  191. sessionId := req.GetString("sessionId", "mcp-session")
  192. virtualPath := fmt.Sprintf("/uploads/%s", filepath.Base(fileName))
  193. record, err := mcpFileStore.WriteBytesWithSession(userId, sessionId, virtualPath, data)
  194. if err != nil {
  195. return helper.ServiceError("上传文件", err), nil
  196. }
  197. return helper.JSONResult(map[string]any{
  198. "message": "文件上传成功",
  199. "path": record.Path,
  200. "fileName": record.FileName,
  201. "size": record.Size,
  202. "key": record.Key,
  203. }), nil
  204. }