// Package agentfiles 提供 MCP Agent 本地落盘目录(/tmp/fwh-toolbox-agent)的清理能力。 // // 背景:MCP 工具的查询/导出结果会无条件落盘到 results/ 目录,storage_file_read_to_local // 下载的文件会落到 downloads/ 目录。这些都是会话内的中间产物,需要定期清理避免无限堆积。 // // 本包被设计为独立、无外部依赖(仅标准库 + zap 日志),以便: // - MCP 进程(cmd/mcp)在启动时调用一次(A 方案:进程启动清理); // - 后端主服务的 cron scheduler 定时调用(B 方案:定时清理)。 // // MCP 是后端通过 kiro-cli 拉起的本机子进程,与后端共享同一 /tmp 文件系统, // 因此后端 cron 能清理到 MCP 写入的这些目录。 package agentfiles import ( "os" "path/filepath" "time" "go.uber.org/zap" ) // AgentBaseDir 是 MCP Agent 本地落盘的根目录。 // 注意:需与 cmd/mcp/tools/helper.AgentBaseDir 保持一致。 const AgentBaseDir = "/tmp/fwh-toolbox-agent" // DefaultMaxAge 默认过期阈值:文件 mtime 超过此时长即视为可清理。 // 取 24h 以覆盖单个会话的生命周期,避免清掉会话内稍后仍需读回的落盘文件。 const DefaultMaxAge = 24 * time.Hour // cleanupSubDirs 需要清理的子目录(相对 AgentBaseDir)。 // results:查询/导出结果落盘;downloads:read_to_local 下载中转。 var cleanupSubDirs = []string{"results", "downloads"} // CleanupResult 描述一次清理的结果统计。 type CleanupResult struct { Scanned int // 扫描到的文件总数 Removed int // 成功删除的文件数 Failed int // 删除失败的文件数 Freed int64 // 释放的字节数(仅统计成功删除的文件) } // Cleanup 清理 AgentBaseDir 下各子目录中 mtime 超过 maxAge 的文件。 // maxAge <= 0 时使用 DefaultMaxAge。 // // 清理为"尽力而为":单个文件删除失败只记 warn 并继续,不返回错误、不中断调用方。 // 仅删除普通文件,跳过子目录(当前落盘不产生嵌套目录)。 func Cleanup(maxAge time.Duration) CleanupResult { if maxAge <= 0 { maxAge = DefaultMaxAge } cutoff := time.Now().Add(-maxAge) var res CleanupResult for _, sub := range cleanupSubDirs { dir := filepath.Join(AgentBaseDir, sub) entries, err := os.ReadDir(dir) if err != nil { // 目录不存在是正常情况(尚未产生过任何落盘文件),忽略。 if !os.IsNotExist(err) { zap.L().Warn("[agentfiles] 读取清理目录失败", zap.String("dir", dir), zap.Error(err)) } continue } for _, e := range entries { if e.IsDir() { continue } info, err := e.Info() if err != nil { zap.L().Warn("[agentfiles] 获取文件信息失败", zap.String("dir", dir), zap.String("name", e.Name()), zap.Error(err)) continue } res.Scanned++ if info.ModTime().After(cutoff) { continue // 未过期,保留 } path := filepath.Join(dir, e.Name()) if err := os.Remove(path); err != nil { res.Failed++ zap.L().Warn("[agentfiles] 删除过期文件失败", zap.String("path", path), zap.Error(err)) continue } res.Removed++ res.Freed += info.Size() } } if res.Scanned > 0 { zap.L().Info("[agentfiles] 清理完成", zap.Int("scanned", res.Scanned), zap.Int("removed", res.Removed), zap.Int("failed", res.Failed), zap.Int64("freedBytes", res.Freed), zap.Duration("maxAge", maxAge), ) } return res }