2026-06-26 08:37:04 +00:00
|
|
|
package placement
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-07-06 06:13:14 +00:00
|
|
|
libkg "haixun-backend/internal/library/knowledge"
|
2026-06-26 08:37:04 +00:00
|
|
|
"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 {
|
2026-07-06 06:13:14 +00:00
|
|
|
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)
|
2026-06-26 08:37:04 +00:00
|
|
|
}
|
2026-07-06 06:13:14 +00:00
|
|
|
if core == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
// Unquoted site search matches more real Threads posts than exact long phrases.
|
|
|
|
|
return `site:threads.net ` + core + ` 推薦`
|
2026-06-26 08:37:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BuildRecencyQuery(provider websearch.Provider, tag string, maxAgeDays int) string {
|
2026-07-06 06:13:14 +00:00
|
|
|
core := strings.TrimSpace(libkg.ShortPatrolSearchCore(tag))
|
|
|
|
|
if core == "" {
|
|
|
|
|
core = strings.TrimSpace(tag)
|
|
|
|
|
}
|
|
|
|
|
if core == "" {
|
2026-06-26 08:37:04 +00:00
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
if websearch.ParseProvider(string(provider)) == websearch.ProviderExa {
|
2026-07-06 06:13:14 +00:00
|
|
|
return fmt.Sprintf("Threads 近期貼文 繁體中文 %s 請問", core)
|
2026-06-26 08:37:04 +00:00
|
|
|
}
|
|
|
|
|
after := FormatAfterDate(maxAgeDays, timeNow())
|
2026-07-06 06:13:14 +00:00
|
|
|
return `site:threads.net "` + core + `" 請問 after:` + after
|
2026-06-26 08:37:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|