package domain import ( "errors" "time" ) const ( StatusPending = "pending" StatusQueued = "queued" StatusRunning = "running" StatusSucceeded = "succeeded" StatusFailed = "failed" StatusCancelled = "cancelled" TemplateDemo = "demo" TemplateThreadsTokenRenew = "threads_token_renew" TemplatePersonaAnalyzeAccount = "persona_analyze_account" TemplatePersonaAnalyzeText = "persona_analyze_text" TemplateComposeMimic = "compose_mimic" // TemplatePlayGenerateScript — 互回/串場:一次產完整劇本(背景 job,避免 HTTP 卡住) TemplatePlayGenerateScript = "play_generate_script" ) // Job 生命週期契約(所有模板必須遵守,worker / API 入列時): // 1. 建立/入列 → StatusQueued + notify(鈴鐺「已排程」) // 2. ClaimNext → StatusRunning + notify(至少 ~5%) // 3. 執行中每有進度 → MarkRunningProgress → notify(同一 job upsert 一則通知) // 4. 終態 SucceedJob / FailJob → notify(已完成/失敗) // 前端靠全域 JobLive 輪詢列表 + 通知;遠期 run_after 不進頂欄即時條。 var ( ErrNotFound = errors.New("job not found") ErrForbidden = errors.New("job access denied") ErrIllegalStatus = errors.New("illegal job status transition") ) type Job struct { ID string `bson:"_id" json:"id"` OwnerUID int64 `bson:"owner_uid" json:"owner_uid"` TemplateType string `bson:"template_type" json:"template_type"` Status string `bson:"status" json:"status"` ProgressSummary string `bson:"progress_summary" json:"progress_summary"` ProgressPercent int `bson:"progress_percent" json:"progress_percent"` Error string `bson:"error,omitempty" json:"error,omitempty"` WorkerID string `bson:"worker_id,omitempty" json:"worker_id,omitempty"` // RefID e.g. threads account id for renew / persona id for analyze RefID string `bson:"ref_id,omitempty" json:"ref_id,omitempty"` // Payload JSON body for job worker (e.g. {"username":"x"}) Payload string `bson:"payload,omitempty" json:"payload,omitempty"` // RunAfter unix nanoseconds; worker only claims when now >= RunAfter (0 = due now) RunAfter int64 `bson:"run_after,omitempty" json:"run_after,omitempty"` CreatedAt int64 `bson:"created_at" json:"created_at"` UpdatedAt int64 `bson:"updated_at" json:"updated_at"` CompletedAt int64 `bson:"completed_at,omitempty" json:"completed_at,omitempty"` } // DefaultTokenRenewDelayNs — 連帳後約 30 天做第一次 renew(長效 token ~60 天) const DefaultTokenRenewDelayNs = int64(30 * 24 * time.Hour) // TerminalRetentionNs — 終態任務(已完成/失敗/取消)保留多久後自動清除 const TerminalRetentionNs = int64(2 * 24 * time.Hour) func NowNano() int64 { return time.Now().UTC().UnixNano() } // IsTerminal — 不可再轉移的狀態 func IsTerminal(status string) bool { return status == StatusSucceeded || status == StatusFailed || status == StatusCancelled } // TerminalCutoff — 早於此時間的終態任務應被 purge func TerminalCutoff(now int64) int64 { if now <= 0 { now = NowNano() } return now - TerminalRetentionNs } // CanTransition enforces JB-10 guarded status. func CanTransition(from, to string) bool { switch from { case StatusPending: return to == StatusQueued || to == StatusRunning || to == StatusFailed case StatusQueued: return to == StatusRunning || to == StatusFailed || to == StatusCancelled case StatusRunning: return to == StatusSucceeded || to == StatusFailed || to == StatusCancelled default: return false // terminal } } // ApplyProgress enforces monotonic percent (JB-02). func ApplyProgress(j *Job, percent int, summary string) error { if percent < j.ProgressPercent { return ErrIllegalStatus } if percent > 100 { percent = 100 } j.ProgressPercent = percent if summary != "" { j.ProgressSummary = summary } j.UpdatedAt = NowNano() return nil }