file.go 8.1 KB

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