diff --git a/internal/app/app.go b/internal/app/app.go index 1901b1d5..ae6d2c60 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -298,7 +298,8 @@ func New(cfg *config.Config, log *logger.Logger, configPath string) (*App, error plantaskBase := filepath.Join(skillsDir, plantaskRel) // Match eino_adk_run_loop: checkpoint_dir is used as configured (relative to process CWD when not absolute). checkpointBase := strings.TrimSpace(cfg.MultiAgent.EinoMiddleware.CheckpointDir) - db.SetEinoConversationDirs(plantaskBase, checkpointBase) + reductionRoot := strings.TrimSpace(cfg.MultiAgent.EinoMiddleware.ReductionRootDir) + db.SetEinoConversationDirs(plantaskBase, checkpointBase, reductionRoot) agent.SetPromptBaseDir(configDir) agentsDir := cfg.AgentsDir diff --git a/internal/database/conversation.go b/internal/database/conversation.go index 1803912a..94458f02 100644 --- a/internal/database/conversation.go +++ b/internal/database/conversation.go @@ -585,12 +585,14 @@ func (db *DB) DeleteConversation(id string) error { // 不返回错误,继续删除对话 } + projectID, _ := db.GetConversationProjectID(id) + // 删除对话(外键CASCADE会自动删除其他相关数据) _, err = db.Exec("DELETE FROM conversations WHERE id = ?", id) if err != nil { return fmt.Errorf("删除对话失败: %w", err) } - db.removeConversationScopedDirs(id) + db.removeConversationScopedDirs(id, projectID) db.logger.Info("对话已删除(漏洞记录已保留)", zap.String("conversationId", id)) return nil @@ -628,13 +630,35 @@ func (db *DB) removeConversationScopedDir(base, conversationID, label string) { } } -func (db *DB) removeConversationScopedDirs(conversationID string) { - // summarization transcript, reduction files, etc. +func (db *DB) einoReductionBaseDir() string { + if db == nil { + return "" + } + if base := strings.TrimSpace(db.einoReductionRootDir); base != "" { + return base + } + return filepath.Join("tmp", "reduction") +} + +func (db *DB) removeConversationScopedDirs(conversationID, projectID string) { + // summarization transcript, etc. db.removeConversationScopedDir(db.conversationArtifactsDir, conversationID, "conversation_artifacts") // Eino plantask JSON boards (skills_dir/.eino/plantask//). db.removeConversationScopedDir(db.einoPlantaskBaseDir, conversationID, "plantask") // Eino ADK runner checkpoints (checkpoint_dir//). db.removeConversationScopedDir(db.einoCheckpointBaseDir, conversationID, "eino_checkpoint") + // Eino reduction persisted tool outputs (tmp/reduction/conversations//). + // Project-bound sessions share projects// — skip on single conversation delete. + if strings.TrimSpace(projectID) == "" { + reductionBase := filepath.Join(db.einoReductionBaseDir(), "conversations") + db.removeConversationScopedDir(reductionBase, conversationID, "reduction") + } +} + +func (db *DB) removeProjectScopedDirs(projectID string) { + // Eino reduction persisted tool outputs (tmp/reduction/projects//). + reductionBase := filepath.Join(db.einoReductionBaseDir(), "projects") + db.removeConversationScopedDir(reductionBase, projectID, "reduction") } // SaveAgentTrace 保存最后一轮代理消息轨迹与助手输出摘要。 diff --git a/internal/database/conversation_cleanup_test.go b/internal/database/conversation_cleanup_test.go index 8a2371ab..77e9cfe9 100644 --- a/internal/database/conversation_cleanup_test.go +++ b/internal/database/conversation_cleanup_test.go @@ -19,7 +19,8 @@ func TestDeleteConversationRemovesEinoScopedDirs(t *testing.T) { plantaskBase := filepath.Join(tmp, "skills", ".eino", "plantask") checkpointBase := filepath.Join(tmp, "eino-checkpoints") - db.SetEinoConversationDirs(plantaskBase, checkpointBase) + reductionBase := filepath.Join(tmp, "reduction") + db.SetEinoConversationDirs(plantaskBase, checkpointBase, reductionBase) conv, err := db.CreateConversation("cleanup test", ConversationCreateMeta{}) if err != nil { @@ -34,6 +35,7 @@ func TestDeleteConversationRemovesEinoScopedDirs(t *testing.T) { {db.conversationArtifactsDir, "transcript.txt"}, {plantaskBase, "task-1.json"}, {checkpointBase, "runner-deep.ckpt"}, + {filepath.Join(reductionBase, "conversations"), "tool-output.txt"}, } { dir := filepath.Join(base.root, seg) if err := os.MkdirAll(dir, 0o755); err != nil { @@ -48,10 +50,45 @@ func TestDeleteConversationRemovesEinoScopedDirs(t *testing.T) { t.Fatalf("DeleteConversation: %v", err) } - for _, base := range []string{db.conversationArtifactsDir, plantaskBase, checkpointBase} { + for _, base := range []string{db.conversationArtifactsDir, plantaskBase, checkpointBase, filepath.Join(reductionBase, "conversations")} { dir := filepath.Join(base, seg) if _, statErr := os.Stat(dir); !os.IsNotExist(statErr) { t.Fatalf("expected removed dir %s, stat err=%v", dir, statErr) } } } + +func TestDeleteProjectRemovesReductionDir(t *testing.T) { + tmp := t.TempDir() + dbPath := filepath.Join(tmp, "conversations.db") + db, err := NewDB(dbPath, zap.NewNop()) + if err != nil { + t.Fatalf("NewDB: %v", err) + } + defer db.Close() + + reductionBase := filepath.Join(tmp, "reduction") + db.SetEinoConversationDirs("", "", reductionBase) + + project, err := db.CreateProject(&Project{Name: "cleanup test"}) + if err != nil { + t.Fatalf("CreateProject: %v", err) + } + seg := sanitizeConversationPathSegment(project.ID) + reductionDir := filepath.Join(reductionBase, "projects", seg, "clear") + if err := os.MkdirAll(reductionDir, 0o755); err != nil { + t.Fatalf("mkdir %s: %v", reductionDir, err) + } + if err := os.WriteFile(filepath.Join(reductionDir, "call-1.txt"), []byte("x"), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + + if err := db.DeleteProject(project.ID); err != nil { + t.Fatalf("DeleteProject: %v", err) + } + + projectReductionDir := filepath.Join(reductionBase, "projects", seg) + if _, statErr := os.Stat(projectReductionDir); !os.IsNotExist(statErr) { + t.Fatalf("expected removed dir %s, stat err=%v", projectReductionDir, statErr) + } +} diff --git a/internal/database/database.go b/internal/database/database.go index 89246101..0ffbd2b8 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -51,6 +51,7 @@ type DB struct { conversationArtifactsDir string einoPlantaskBaseDir string // skills_dir + plantask_rel_dir (per-conversation subdirs) einoCheckpointBaseDir string // checkpoint_dir root (per-conversation subdirs) + einoReductionRootDir string // reduction_root_dir or default tmp/reduction (conversations/ subdirs) checkpointLoopName string checkpointStop chan struct{} checkpointDone chan struct{} @@ -159,12 +160,14 @@ func NewDB(dbPath string, logger *zap.Logger) (*DB, error) { // SetEinoConversationDirs configures best-effort filesystem cleanup on DeleteConversation. // plantaskBase is skills_root/plantask_rel (no conversation id); checkpointBase is checkpoint_dir root. -func (db *DB) SetEinoConversationDirs(plantaskBase, checkpointBase string) { +// reductionRoot is reduction_root_dir from config; empty uses tmp/reduction (conversation-scoped subdirs only). +func (db *DB) SetEinoConversationDirs(plantaskBase, checkpointBase, reductionRoot string) { if db == nil { return } db.einoPlantaskBaseDir = strings.TrimSpace(plantaskBase) db.einoCheckpointBaseDir = strings.TrimSpace(checkpointBase) + db.einoReductionRootDir = strings.TrimSpace(reductionRoot) } // initTables 初始化数据库表 diff --git a/internal/database/project.go b/internal/database/project.go index d0524be8..701389a7 100644 --- a/internal/database/project.go +++ b/internal/database/project.go @@ -195,6 +195,7 @@ func (db *DB) DeleteProject(id string) error { if err != nil { return fmt.Errorf("删除项目失败: %w", err) } + db.removeProjectScopedDirs(id) return nil }