cleanup.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. // Package agentfiles 提供 MCP Agent 本地落盘目录(/tmp/fwh-toolbox-agent)的清理能力。
  2. //
  3. // 背景:MCP 工具的查询/导出结果会无条件落盘到 results/ 目录,storage_file_read_to_local
  4. // 下载的文件会落到 downloads/ 目录。这些都是会话内的中间产物,需要定期清理避免无限堆积。
  5. //
  6. // 本包被设计为独立、无外部依赖(仅标准库 + zap 日志),以便:
  7. // - MCP 进程(cmd/mcp)在启动时调用一次(A 方案:进程启动清理);
  8. // - 后端主服务的 cron scheduler 定时调用(B 方案:定时清理)。
  9. //
  10. // MCP 是后端通过 kiro-cli 拉起的本机子进程,与后端共享同一 /tmp 文件系统,
  11. // 因此后端 cron 能清理到 MCP 写入的这些目录。
  12. package agentfiles
  13. import (
  14. "os"
  15. "path/filepath"
  16. "time"
  17. "go.uber.org/zap"
  18. )
  19. // AgentBaseDir 是 MCP Agent 本地落盘的根目录。
  20. // 注意:需与 cmd/mcp/tools/helper.AgentBaseDir 保持一致。
  21. const AgentBaseDir = "/tmp/fwh-toolbox-agent"
  22. // DefaultMaxAge 默认过期阈值:文件 mtime 超过此时长即视为可清理。
  23. // 取 24h 以覆盖单个会话的生命周期,避免清掉会话内稍后仍需读回的落盘文件。
  24. const DefaultMaxAge = 24 * time.Hour
  25. // cleanupSubDirs 需要清理的子目录(相对 AgentBaseDir)。
  26. // results:查询/导出结果落盘;downloads:read_to_local 下载中转。
  27. var cleanupSubDirs = []string{"results", "downloads"}
  28. // CleanupResult 描述一次清理的结果统计。
  29. type CleanupResult struct {
  30. Scanned int // 扫描到的文件总数
  31. Removed int // 成功删除的文件数
  32. Failed int // 删除失败的文件数
  33. Freed int64 // 释放的字节数(仅统计成功删除的文件)
  34. }
  35. // Cleanup 清理 AgentBaseDir 下各子目录中 mtime 超过 maxAge 的文件。
  36. // maxAge <= 0 时使用 DefaultMaxAge。
  37. //
  38. // 清理为"尽力而为":单个文件删除失败只记 warn 并继续,不返回错误、不中断调用方。
  39. // 仅删除普通文件,跳过子目录(当前落盘不产生嵌套目录)。
  40. func Cleanup(maxAge time.Duration) CleanupResult {
  41. if maxAge <= 0 {
  42. maxAge = DefaultMaxAge
  43. }
  44. cutoff := time.Now().Add(-maxAge)
  45. var res CleanupResult
  46. for _, sub := range cleanupSubDirs {
  47. dir := filepath.Join(AgentBaseDir, sub)
  48. entries, err := os.ReadDir(dir)
  49. if err != nil {
  50. // 目录不存在是正常情况(尚未产生过任何落盘文件),忽略。
  51. if !os.IsNotExist(err) {
  52. zap.L().Warn("[agentfiles] 读取清理目录失败",
  53. zap.String("dir", dir), zap.Error(err))
  54. }
  55. continue
  56. }
  57. for _, e := range entries {
  58. if e.IsDir() {
  59. continue
  60. }
  61. info, err := e.Info()
  62. if err != nil {
  63. zap.L().Warn("[agentfiles] 获取文件信息失败",
  64. zap.String("dir", dir), zap.String("name", e.Name()), zap.Error(err))
  65. continue
  66. }
  67. res.Scanned++
  68. if info.ModTime().After(cutoff) {
  69. continue // 未过期,保留
  70. }
  71. path := filepath.Join(dir, e.Name())
  72. if err := os.Remove(path); err != nil {
  73. res.Failed++
  74. zap.L().Warn("[agentfiles] 删除过期文件失败",
  75. zap.String("path", path), zap.Error(err))
  76. continue
  77. }
  78. res.Removed++
  79. res.Freed += info.Size()
  80. }
  81. }
  82. if res.Scanned > 0 {
  83. zap.L().Info("[agentfiles] 清理完成",
  84. zap.Int("scanned", res.Scanned),
  85. zap.Int("removed", res.Removed),
  86. zap.Int("failed", res.Failed),
  87. zap.Int64("freedBytes", res.Freed),
  88. zap.Duration("maxAge", maxAge),
  89. )
  90. }
  91. return res
  92. }