package placement import ( "fmt" "strings" "time" libkg "haixun-backend/internal/library/knowledge" "haixun-backend/internal/library/websearch" ) type QueryDimension string const ( QueryRelevance QueryDimension = "relevance" QueryRecency QueryDimension = "recency" ) type TagQuery struct { Tag string Query string Dimension QueryDimension GraphNodeID string ProductFitScore int RecencyDays int // 0 = no after filter; 7 or 30 for recency track } func BuildRelevanceQuery(provider websearch.Provider, tag string) string { if websearch.ParseProvider(string(provider)) == websearch.ProviderExa { full := strings.TrimSpace(libkg.FullPatrolWebSearchTag(tag)) if full == "" { return "" } return fmt.Sprintf("Threads 貼文 繁體中文 %s", full) } core := strings.TrimSpace(libkg.ShortPatrolSearchCore(tag)) if core == "" { core = strings.TrimSpace(tag) } if core == "" { return "" } // Unquoted site search matches more real Threads posts than exact long phrases. return `site:threads.net ` + core + ` 推薦` } func BuildRecencyQuery(provider websearch.Provider, tag string, maxAgeDays int) string { core := strings.TrimSpace(libkg.ShortPatrolSearchCore(tag)) if core == "" { core = strings.TrimSpace(tag) } if core == "" { return "" } if websearch.ParseProvider(string(provider)) == websearch.ProviderExa { return fmt.Sprintf("Threads 近期貼文 繁體中文 %s 請問", core) } after := FormatAfterDate(maxAgeDays, timeNow()) return `site:threads.net "` + core + `" 請問 after:` + after } func PublishedAfterForRecency(provider websearch.Provider, maxAgeDays int) string { if maxAgeDays <= 0 || websearch.ParseProvider(string(provider)) != websearch.ProviderExa { return "" } return FormatPublishedAfterISO(maxAgeDays, timeNow()) } var timeNow = func() time.Time { return time.Now() } // SetTimeNowForTest overrides time source in tests. func SetTimeNowForTest(fn func() time.Time) { timeNow = fn }