package domain import ( "fmt" "strings" ) // Sweep fetch path — reuses the dual-path split (api | crawler); no third path. const ( SweepPathAPI = "api" SweepPathCrawler = "crawler" ) /* RadarSweep 是一次每日巡(或手動觸發)的執行紀錄。 計數以累加更新為主:Job 中途失敗時已寫入的進度保留,供續跑判斷 (judged_external_ids 標記已判過的貼文,避免重跑)。 */ type RadarSweep struct { ID string `bson:"_id" json:"id"` OwnerUID int64 `bson:"owner_uid" json:"owner_uid"` WatchID string `bson:"watch_id" json:"watch_id"` JobID string `bson:"job_id,omitempty" json:"job_id,omitempty"` Path string `bson:"path" json:"path"` HitCount int `bson:"hit_count" json:"hit_count"` JudgedCount int `bson:"judged_count" json:"judged_count"` CreatedCount int `bson:"created_count" json:"created_count"` TruncatedCount int `bson:"truncated_count" json:"truncated_count"` MatchEvaluatedCount int `bson:"match_evaluated_count" json:"match_evaluated_count"` MatchMergedCount int `bson:"match_merged_count" json:"match_merged_count"` FitRejectedCount int `bson:"fit_rejected_count" json:"fit_rejected_count"` FailedReason string `bson:"failed_reason,omitempty" json:"failed_reason,omitempty"` CreditsUsed int `bson:"credits_used" json:"credits_used"` DedupedCount int `bson:"deduped_count" json:"deduped_count"` PrefilterPassCount int `bson:"prefilter_pass_count" json:"prefilter_pass_count"` PrefilterReviewCount int `bson:"prefilter_review_count" json:"prefilter_review_count"` PrefilterRejectedCount int `bson:"prefilter_rejected_count" json:"prefilter_rejected_count"` CachedJudgmentCount int `bson:"cached_judgment_count" json:"cached_judgment_count"` TombstoneMatchedCount int `bson:"tombstone_matched_count" json:"tombstone_matched_count"` BudgetDeferredCount int `bson:"budget_deferred_count" json:"budget_deferred_count"` DemandInputVersion string `bson:"demand_input_version,omitempty" json:"demand_input_version,omitempty"` DemandMapVersion int64 `bson:"demand_map_version,omitempty" json:"demand_map_version,omitempty"` CreditSearch int `bson:"credit_search" json:"credit_search"` CreditDemandMap int `bson:"credit_demand_map" json:"credit_demand_map"` CreditJudge int `bson:"credit_judge" json:"credit_judge"` CreditReply int `bson:"credit_reply" json:"credit_reply"` JudgedExternalIDs []string `bson:"judged_external_ids,omitempty" json:"judged_external_ids,omitempty"` StartedAt int64 `bson:"started_at" json:"started_at"` EndedAt int64 `bson:"ended_at,omitempty" json:"ended_at,omitempty"` } // SweepDelta is an incremental progress patch applied with $inc / $addToSet. type SweepDelta struct { HitCount int JudgedCount int CreatedCount int TruncatedCount int MatchEvaluatedCount int MatchMergedCount int FitRejectedCount int CreditsUsed int DedupedCount int PrefilterPassCount int PrefilterReviewCount int PrefilterRejectedCount int CachedJudgmentCount int TombstoneMatchedCount int BudgetDeferredCount int CreditSearch int CreditDemandMap int CreditJudge int CreditReply int JudgedExternalIDs []string FailedReason *string // nil = leave unchanged; non-nil (incl. empty) = set EndedAt int64 // 0 = leave unchanged } // SweepListFilter pages sweeps for an owner, optionally scoped to one watch. type SweepListFilter struct { WatchID string Page int PageSize int } func IsSweepPath(s string) bool { switch s { case SweepPathAPI, SweepPathCrawler: return true } return false } func (s *RadarSweep) Normalize() error { if s.OwnerUID <= 0 { return fmt.Errorf("%w: owner_uid required", ErrValidation) } // WatchID may be empty for on-demand explore (no subscription); required for scheduled sweeps. s.WatchID = strings.TrimSpace(s.WatchID) if s.Path == "" { s.Path = SweepPathAPI } if !IsSweepPath(s.Path) { return fmt.Errorf("%w: unknown sweep path %q", ErrValidation, s.Path) } // failed_reason must never hold tokens; strip obvious bearer-like blobs is out of scope — // callers are responsible. We only reject empty path/owner. if s.FailedReason != "" { s.FailedReason = strings.TrimSpace(s.FailedReason) } s.JudgedExternalIDs = dedupeStrings(s.JudgedExternalIDs) return nil } // MergeJudgedExternalIDs appends new IDs without duplicates (order preserved). func MergeJudgedExternalIDs(existing, add []string) []string { return dedupeStrings(append(append([]string{}, existing...), add...)) }