Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,41 @@ audit:
# MCP 状态监控执行记录保留(tool_executions 表)
monitor:
retention_days: 90 # 省略时默认 90;0 表示不自动清理
# 运行空间垃圾清理(系统设置 -> 存储清理)
# 覆盖 Agent 工作区、工具输出缓存、C2 产物、对话上传件等磁盘产物的保留策略。
# auto_clean 默认关闭:升级后不会在管理员不知情的情况下删除既有数据;
# 关闭时仍可在「存储清理」页手动预览并执行清理。
storage:
auto_clean: false # true 开启后台定时清理
interval_minutes: 60 # 后台清理间隔(分钟);最小 5
orphan_grace_days: 1 # 会话/项目已删除但目录残留时,闲置多少天后回收
active_grace_hours: 24 # 最近有活动的会话一律跳过,避免误删正在跑的任务数据
# 各类别 retention_days:省略时使用下方默认值;0 表示不按保留期清理(孤儿目录仍会回收)
categories:
workspace: # Agent 工作区 tmp/workspace;默认 30 天
enabled: true
retention_days: 30
reduction: # 超长工具输出落盘 tmp/reduction;默认 7 天(纯派生数据)
enabled: true
retention_days: 7
conversation_artifacts: # 摘要与超长输入台账 data/conversation_artifacts;默认 30 天
enabled: true
retention_days: 30
plantask: # 多代理计划看板 skills/.eino/plantask;默认 30 天(纯派生数据)
enabled: true
retention_days: 30
c2_artifacts: # C2 回传截图/上传件/下发文件/payload tmp/c2;默认 30 天
enabled: true # 注意:清理 payload 后对应下载链接会失效
retention_days: 30
chat_uploads: # 对话上传附件 chat_uploads;默认 90 天
enabled: true
retention_days: 90
workflow_checkpoints: # 工作流断点 data/workflow-checkpoints;默认 7 天
enabled: true
retention_days: 7
diagnostic_logs: # 诊断日志 log/diagnostic-*.log;默认 14 天
enabled: true # 与 log.diagnostic_retention_days 各自独立生效,取先到者
retention_days: 14
# ============================================
# 对话相关配置
# ============================================
Expand Down
32 changes: 32 additions & 0 deletions docs/en-US/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,38 @@ When debugging, inspect both the child config and the fallback parent.
| `similarity_threshold` | 0.35-0.45 | 0.5+ | recall vs precision |
| `audit.retention_days` | 15-30 | 90+ | compliance and disk |

## Runtime storage cleanup

```yaml
storage:
auto_clean: false
interval_minutes: 60
orphan_grace_days: 1
active_grace_hours: 24
categories:
workspace:
enabled: true
retention_days: 30
```

Reclaims disk space used by runtime artifacts. Manage it from **System settings → Storage cleanup**, or trigger it with `POST /api/storage/cleanup`.

- `auto_clean` is off by default: upgrading never deletes existing data behind an administrator's back. Manual preview and cleanup still work while it is off.
- `interval_minutes` is the background sweep interval (floor 5). `orphan_grace_days` is the minimum age before a directory whose conversation/project no longer exists is reclaimed. Sessions active within `active_grace_hours` are always skipped.
- A category `retention_days` of 0 disables age-based cleanup, but **orphaned directories are still reclaimed** — a directory whose session is gone has no retention value.
- Deletion is irreversible. The API requires `dry_run=false` together with `confirm=true` for a real cleanup; omitting `dry_run` is treated as a preview.

| Category | Default retention | Target directory |
| --- | --- | --- |
| `workspace` | 30 days | agent workspaces, `tmp/workspace` |
| `reduction` | 7 days | oversized tool-output spill, `tmp/reduction` |
| `conversation_artifacts` | 30 days | summaries and user-input ledger, `data/conversation_artifacts` |
| `plantask` | 30 days | multi-agent plan boards, `skills/.eino/plantask` |
| `c2_artifacts` | 30 days | C2 results/uploads/downstream/payloads, `tmp/c2` (cleaning a payload invalidates its download link) |
| `chat_uploads` | 90 days | conversation attachments, `chat_uploads` |
| `workflow_checkpoints` | 7 days | workflow run checkpoints, `data/workflow-checkpoints` |
| `diagnostic_logs` | 14 days | diagnostic logs, `log/diagnostic-*.log` |

## Change Template

Before changing config, write down:
Expand Down
32 changes: 32 additions & 0 deletions docs/zh-CN/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,38 @@ monitor:
- `audit` 记录平台操作,不记录对话正文和每次工具调用正文。
- `monitor` 管理工具执行记录保留时间。

## 存储清理

```yaml
storage:
auto_clean: false
interval_minutes: 60
orphan_grace_days: 1
active_grace_hours: 24
categories:
workspace:
enabled: true
retention_days: 30
```

清理运行期间产生的磁盘垃圾,入口在「系统设置 → 存储清理」,也可用 `POST /api/storage/cleanup` 触发。

- `auto_clean` 默认关闭:升级后不会在管理员不知情的情况下删除既有数据;关闭时仍可手动预览与清理。
- `interval_minutes` 后台清理间隔,最小 5;`orphan_grace_days` 是会话/项目已删除但目录残留时的最小保留天数;`active_grace_hours` 内的活动会话一律跳过。
- 各类别 `retention_days` 为 0 表示不按保留期清理,但**孤儿目录仍会回收**(会话已删除的目录没有保留价值)。
- 删除不可逆。API 层要求真实删除必须同时传 `dry_run=false` 与 `confirm=true`,省略 `dry_run` 按预览处理。

| 类别 | 默认保留 | 目标目录 |
| --- | --- | --- |
| `workspace` | 30 天 | Agent 工作区 `tmp/workspace` |
| `reduction` | 7 天 | 超长工具输出落盘 `tmp/reduction` |
| `conversation_artifacts` | 30 天 | 摘要与超长输入台账 `data/conversation_artifacts` |
| `plantask` | 30 天 | 多代理计划看板 `skills/.eino/plantask` |
| `c2_artifacts` | 30 天 | C2 回传/上传/下发/payload `tmp/c2`(清理 payload 后下载链接失效) |
| `chat_uploads` | 90 天 | 对话上传附件 `chat_uploads` |
| `workflow_checkpoints` | 7 天 | 工作流断点 `data/workflow-checkpoints` |
| `diagnostic_logs` | 14 天 | 诊断日志 `log/diagnostic-*.log` |

## C2、WebShell、项目

```yaml
Expand Down
50 changes: 50 additions & 0 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"cyberstrike-ai/internal/robot"
"cyberstrike-ai/internal/security"
"cyberstrike-ai/internal/skillpackage"
"cyberstrike-ai/internal/storage"
"cyberstrike-ai/internal/toolguard"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -72,6 +73,7 @@ type App struct {
c2Watchdog *c2.SessionWatchdog // C2 会话看门狗
c2WatchdogCancel context.CancelFunc // 看门狗取消函数
c2Handler *handler.C2Handler // C2 REST(与 Manager 生命周期同步)
storageHandler *handler.StorageHandler // 运行空间占用统计与垃圾清理
auditSvc *audit.Service
}

Expand Down Expand Up @@ -367,6 +369,48 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error
reductionRoot := strings.TrimSpace(cfg.MultiAgent.EinoMiddleware.ReductionRootDir)
workspaceRoot := strings.TrimSpace(cfg.Agent.WorkspaceRootDir)
db.SetEinoConversationDirs(plantaskBase, checkpointBase, reductionRoot, workspaceRoot)

// 运行空间垃圾清理:根目录一律复用上面已解析好的同一批值,
// 避免在 storage 包内重新推导导致「清理的目录」与「实际写入的目录」不一致。
workspaceRootDir := strings.TrimSpace(workspaceRoot)
if workspaceRootDir == "" {
workspaceRootDir = filepath.Join("tmp", "workspace")
}
reductionRootDir := strings.TrimSpace(reductionRoot)
if reductionRootDir == "" {
reductionRootDir = filepath.Join("tmp", "reduction")
}
diagnosticLogDir := strings.TrimSpace(cfg.Log.DiagnosticDir)
if diagnosticLogDir == "" {
diagnosticLogDir = "log"
}
// chat_uploads 与 tmp/c2 目前均为相对进程工作目录的固定路径
// (见 handler.chatUploadsRootDirName 与 app/c2_lifecycle.go 的 c2.NewManager)。
chatUploadsRoot := "chat_uploads"
c2Root := filepath.Join("tmp", "c2")
// 让 DeleteConversation 一并删除上传附件:其 chat_upload_artifacts 行已由
// ON DELETE CASCADE 清除,此前磁盘文件会永久残留。
db.SetChatUploadsDir(chatUploadsRoot)
storageCleaner := storage.NewCleaner(storage.Options{
Config: cfg,
Paths: storage.Paths{
Workspace: workspaceRootDir,
Reduction: reductionRootDir,
ConversationArtifact: db.ConversationArtifactsBaseDir(),
Plantask: plantaskBase,
C2: c2Root,
ChatUploads: chatUploadsRoot,
WorkflowCheckpoints: filepath.Join(filepath.Dir(dbPath), "workflow-checkpoints"),
DiagnosticLogs: diagnosticLogDir,
},
Activity: db,
Logger: log.Logger,
})
storageService := storage.NewService(storageCleaner, cfg, log.Logger)
storage.StartRetentionLoop(storageService, log.Logger)
storageHandler := handler.NewStorageHandler(storageCleaner, cfg, log.Logger)
storageHandler.SetAudit(auditSvc)

agent.SetPromptBaseDir(configDir)

agentsDir := cfg.AgentsDir
Expand Down Expand Up @@ -479,6 +523,7 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error
c2Watchdog: c2Watchdog,
c2WatchdogCancel: watchdogCancel,
c2Handler: c2Handler,
storageHandler: storageHandler,
auditSvc: auditSvc,
}
// 飞书/钉钉长连接(无需公网),启用时在后台启动;后续前端应用配置时会通过 RestartRobotConnections 重启
Expand Down Expand Up @@ -1085,6 +1130,11 @@ func setupRoutes(
protected.GET("/audit/logs/export", auditHandler.ExportLogs)
protected.GET("/audit/logs/:id", auditHandler.GetLog)

// 运行空间占用与垃圾清理
protected.GET("/storage/meta", app.storageHandler.Meta)
protected.GET("/storage/status", app.storageHandler.Status)
protected.POST("/storage/cleanup", app.storageHandler.Cleanup)

// 外部MCP管理
protected.GET("/external-mcp", externalMCPHandler.GetExternalMCPs)
protected.GET("/external-mcp/stats", externalMCPHandler.GetExternalMCPStats)
Expand Down
127 changes: 127 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type Config struct {
Auth AuthConfig `yaml:"auth"`
Audit AuditConfig `yaml:"audit,omitempty" json:"audit,omitempty"`
Monitor MonitorConfig `yaml:"monitor,omitempty" json:"monitor,omitempty"`
Storage StorageConfig `yaml:"storage,omitempty" json:"storage,omitempty"`
ExternalMCP ExternalMCPConfig `yaml:"external_mcp,omitempty"`
Knowledge KnowledgeConfig `yaml:"knowledge,omitempty"`
C2 C2Config `yaml:"c2,omitempty" json:"c2,omitempty"` // 内置 C2 总开关;未配置时默认启用
Expand Down Expand Up @@ -1311,6 +1312,132 @@ func (m MonitorConfig) RetentionDaysEffective() int {
return *m.RetentionDays
}

// 运行空间垃圾清理的类别键。顺序即前端展示顺序,勿依赖 map 迭代顺序。
const (
StorageCategoryWorkspace = "workspace"
StorageCategoryReduction = "reduction"
StorageCategoryConversationArtifact = "conversation_artifacts"
StorageCategoryPlantask = "plantask"
StorageCategoryC2Artifacts = "c2_artifacts"
StorageCategoryChatUploads = "chat_uploads"
StorageCategoryWorkflowCheckpoints = "workflow_checkpoints"
StorageCategoryDiagnosticLogs = "diagnostic_logs"
)

// StorageCategoryOrder 列出全部可清理类别,供 UI 与报表稳定排序。
var StorageCategoryOrder = []string{
StorageCategoryWorkspace,
StorageCategoryReduction,
StorageCategoryConversationArtifact,
StorageCategoryPlantask,
StorageCategoryC2Artifacts,
StorageCategoryChatUploads,
StorageCategoryWorkflowCheckpoints,
StorageCategoryDiagnosticLogs,
}

// StorageCategoryDefaults 各类别默认保留天数。取较短值的是纯派生产物
// (reduction/checkpoint),取较长值的是可能仍需人工回看的上传件。
var StorageCategoryDefaults = map[string]int{
StorageCategoryWorkspace: 30,
StorageCategoryReduction: 7,
StorageCategoryConversationArtifact: 30,
StorageCategoryPlantask: 30,
StorageCategoryC2Artifacts: 30,
StorageCategoryChatUploads: 90,
StorageCategoryWorkflowCheckpoints: 7,
StorageCategoryDiagnosticLogs: 14,
}

// StorageCategoryConfig 单个清理类别的策略覆盖。
type StorageCategoryConfig struct {
// Enabled 省略时默认 true;显式 false 表示该类别既不自动清理也不出现在手动清理范围内。
Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
// RetentionDays 省略时使用 StorageCategoryDefaults;0 表示不按保留期清理(孤儿目录仍会回收)。
RetentionDays *int `yaml:"retention_days,omitempty" json:"retention_days,omitempty"`
}

// StorageConfig 运行空间垃圾清理策略。
// 自动清理默认关闭:与 Argo ttlStrategy / K8s ttlSecondsAfterFinished 的 unset 语义一致,
// 升级后不会在管理员不知情的情况下删除既有数据。
type StorageConfig struct {
// AutoClean 省略或 false 关闭后台自动清理;显式 true 才启用。
AutoClean *bool `yaml:"auto_clean,omitempty" json:"auto_clean,omitempty"`
// IntervalMinutes 后台清理轮询间隔;省略默认 60,最小 5。
IntervalMinutes *int `yaml:"interval_minutes,omitempty" json:"interval_minutes,omitempty"`
// OrphanGraceDays 会话/项目已删除但目录残留时的最小保留天数;省略默认 1。
OrphanGraceDays *int `yaml:"orphan_grace_days,omitempty" json:"orphan_grace_days,omitempty"`
// ActiveGraceHours 最近有活动的会话一律跳过清理;省略默认 24。
ActiveGraceHours *int `yaml:"active_grace_hours,omitempty" json:"active_grace_hours,omitempty"`
// Categories 按类别覆盖策略;未列出的类别使用内置默认值。
Categories map[string]StorageCategoryConfig `yaml:"categories,omitempty" json:"categories,omitempty"`
}

// AutoCleanEffective returns true only when storage.auto_clean is explicitly true.
func (s StorageConfig) AutoCleanEffective() bool {
return s.AutoClean != nil && *s.AutoClean
}

// IntervalMinutesEffective returns the background sweep interval; defaults to 60, floor 5.
func (s StorageConfig) IntervalMinutesEffective() int {
if s.IntervalMinutes == nil {
return 60
}
if *s.IntervalMinutes < 5 {
return 5
}
return *s.IntervalMinutes
}

// OrphanGraceDaysEffective returns the minimum age before an orphaned session dir is reclaimed; defaults to 1.
func (s StorageConfig) OrphanGraceDaysEffective() int {
if s.OrphanGraceDays == nil {
return 1
}
if *s.OrphanGraceDays < 0 {
return 0
}
return *s.OrphanGraceDays
}

// ActiveGraceHoursEffective returns the recent-activity protection window; defaults to 24, floor 1.
func (s StorageConfig) ActiveGraceHoursEffective() int {
if s.ActiveGraceHours == nil {
return 24
}
if *s.ActiveGraceHours < 1 {
return 1
}
return *s.ActiveGraceHours
}

// CategoryEnabled reports whether a category participates in cleanup; unknown keys default to false.
func (s StorageConfig) CategoryEnabled(key string) bool {
if _, known := StorageCategoryDefaults[key]; !known {
return false
}
if c, ok := s.Categories[key]; ok && c.Enabled != nil {
return *c.Enabled
}
return true
}

// CategoryRetentionDays returns the effective retention for a category; unknown keys yield 0 (keep forever).
func (s StorageConfig) CategoryRetentionDays(key string) int {
def, known := StorageCategoryDefaults[key]
if !known {
return 0
}
c, ok := s.Categories[key]
if !ok || c.RetentionDays == nil {
return def
}
if *c.RetentionDays < 0 {
return 0
}
return *c.RetentionDays
}

// AuditConfig platform operation audit log settings (not chat/tool execution bodies).
type AuditConfig struct {
// Enabled nil or true enables persistence; explicit false disables.
Expand Down
29 changes: 29 additions & 0 deletions internal/database/conversation.go
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,8 @@ func (db *DB) removeConversationScopedDirs(conversationID, projectID string) {
db.removeConversationScopedDir(db.einoPlantaskBaseDir, conversationID, "plantask")
// Eino ADK runner checkpoints (checkpoint_dir/<id>/).
db.removeConversationScopedDir(db.einoCheckpointBaseDir, conversationID, "eino_checkpoint")
// 上传附件始终归属单个会话,项目绑定的会话也要删,故放在 projectID 判断之外。
db.removeChatUploadDirs(conversationID)
// Eino reduction persisted tool outputs (tmp/reduction/conversations/<id>/).
// Project-bound sessions share projects/<id>/ — skip on single conversation delete.
if strings.TrimSpace(projectID) == "" {
Expand All @@ -845,6 +847,33 @@ func (db *DB) removeConversationScopedDirs(conversationID, projectID string) {
}
}

// removeChatUploadDirs 删除 chat_uploads/<日期>/<会话ID>/ 下属于该会话的上传目录。
// 该根目录比其他产物多一层日期目录,无法复用 removeConversationScopedDir。
func (db *DB) removeChatUploadDirs(conversationID string) {
base := strings.TrimSpace(db.chatUploadsDir)
if base == "" || strings.TrimSpace(conversationID) == "" {
return
}
seg := sanitizeConversationPathSegment(conversationID)
dates, err := os.ReadDir(base)
if err != nil {
return
}
for _, dateDir := range dates {
if !dateDir.IsDir() {
continue
}
dir := filepath.Join(base, dateDir.Name(), seg)
if rmErr := os.RemoveAll(dir); rmErr != nil && db.logger != nil {
db.logger.Warn("删除会话上传目录失败",
zap.String("conversationId", conversationID),
zap.String("kind", "chat_uploads"),
zap.String("dir", dir),
zap.Error(rmErr))
}
}
}

func (db *DB) removeProjectScopedDirs(projectID string) {
// Eino reduction persisted tool outputs (tmp/reduction/projects/<id>/).
reductionBase := filepath.Join(db.einoReductionBaseDir(), "projects")
Expand Down
Loading