package store import ( "context" "time" "gorm.io/gorm" ) // 定时任务(JARVIS 调度能力):让用户能说「每天早上九点帮我看看昨天的任务情况」。 // 存的是**自然语言指令**而非编排图——到点由 JARVIS 自己按这句话去调工具办事, // 与语音链路同一条执行路径(见 handler/schedule_tick.go)。 // // 触发语义刻意做成"绝对时刻推进":NextRunAt 到点即跑,跑完按 IntervalSec 顺推。 // 进程停机期间错过的**不补跑**(补跑一堆历史提醒是骚扰,不是可靠性)——直接顺推到下一个未来时刻。 type Schedule struct { ID string `gorm:"primaryKey;size:32"` Owner string `gorm:"size:32;index"` // 雪花 user.id TenantID string `gorm:"size:32;index"` // 计费租户(到点提交任务时按它计费) SessionID string `gorm:"size:64"` // 创建时的会话(供上下文续聊) Title string `gorm:"size:200"` // 人读的名字,如「每日任务巡检」 Prompt string `gorm:"type:text"` // 到点要 JARVIS 做的事(自然语言) IntervalSec int64 `gorm:"not null"` // 周期秒数;0 = 只跑一次 Enabled bool `gorm:"default:true;index"` NextRunAt time.Time `gorm:"index"` LastRunAt *time.Time RunCount int64 CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` } func (Schedule) TableName() string { return "sundynix_schedule" } // CreateSchedule 建一条定时任务。 func (p *Postgres) CreateSchedule(ctx context.Context, s *Schedule) error { if p.db == nil { return nil // 降级模式(无 PG):与其它 store 写方法同约定,静默不持久化 } if s.ID == "" { s.ID = NewID() } return p.db.WithContext(ctx).Create(s).Error } // ListSchedules 列某用户的定时任务(含已停用,供展示/管理)。 func (p *Postgres) ListSchedules(ctx context.Context, owner string) []Schedule { if p.db == nil { return nil } var out []Schedule p.db.WithContext(ctx).Where("owner = ?", owner).Order("next_run_at asc").Limit(50).Find(&out) return out } // CancelSchedule 停用某条定时任务(软停用而非删除,保留痕迹)。归属校验在调用方。 func (p *Postgres) CancelSchedule(ctx context.Context, owner, id string) error { if p.db == nil { return nil } return p.db.WithContext(ctx).Model(&Schedule{}). Where("id = ? AND owner = ?", id, owner). Update("enabled", false).Error } // DueSchedules 取所有到期待跑的任务(系统级扫描,跨租户——必须 WithoutTenant, // 否则租户插件会把定时器自己的扫描限死在空租户上,一条都扫不到)。 func (p *Postgres) DueSchedules(ctx context.Context, limit int) []Schedule { if p.db == nil { return nil } var out []Schedule p.db.WithContext(WithoutTenant(ctx)). Where("enabled = true AND next_run_at <= ?", time.Now()). Order("next_run_at asc").Limit(limit).Find(&out) return out } // AdvanceSchedule 一条任务跑完后推进到下一次:周期任务顺推到**下一个未来时刻** // (跳过停机期间错过的,不补跑);一次性任务直接停用。 func (p *Postgres) AdvanceSchedule(ctx context.Context, s *Schedule) error { if p.db == nil { return nil } now := time.Now() updates := map[string]any{ "last_run_at": now, "run_count": gorm.Expr("run_count + 1"), } if s.IntervalSec <= 0 { updates["enabled"] = false // 一次性:跑完即停 } else { next := s.NextRunAt step := time.Duration(s.IntervalSec) * time.Second for !next.After(now) { next = next.Add(step) // 跳过错过的周期,直接到下一个未来时刻 } updates["next_run_at"] = next } return p.db.WithContext(WithoutTenant(ctx)).Model(&Schedule{}). Where("id = ?", s.ID).Updates(updates).Error }