thread-master/apps/backend/internal/module/job/domain/job.go

108 lines
3.9 KiB
Go
Raw Normal View History

2026-07-13 01:15:30 +00:00
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
}