package placement import ( "strings" libkg "haixun-backend/internal/library/knowledge" "haixun-backend/internal/library/websearch" ) const defaultPatrolProductFit = 78 // CollectPatrolTagQueries builds dual-track crawl jobs from user-edited patrol keywords only. func CollectPatrolTagQueries(keywords []string, nodes []libkg.Node, provider websearch.Provider) []TagQuery { keywords = libkg.NormalizePatrolKeywordList(keywords) if len(keywords) == 0 { return nil } out := make([]TagQuery, 0, len(keywords)*3) for _, tag := range keywords { fit := productFitForPatrolTag(tag, nodes) if q := BuildRelevanceQuery(provider, tag); q != "" { out = append(out, TagQuery{ Tag: tag, Query: q, Dimension: QueryRelevance, ProductFitScore: fit, }) } if q7 := BuildRecencyQuery(provider, tag, IdealMaxPostAgeDays); q7 != "" { out = append(out, TagQuery{ Tag: tag, Query: q7, Dimension: QueryRecency, ProductFitScore: fit, RecencyDays: IdealMaxPostAgeDays, }) } if q30 := BuildRecencyQuery(provider, tag, MaxPostAgeDays); q30 != "" { if q7 := BuildRecencyQuery(provider, tag, IdealMaxPostAgeDays); q30 != q7 { out = append(out, TagQuery{ Tag: tag, Query: q30, Dimension: QueryRecency, ProductFitScore: fit, RecencyDays: MaxPostAgeDays, }) } } } return out } func productFitForPatrolTag(tag string, nodes []libkg.Node) int { tagKey := patrolTagMatchKey(tag) best := 0 bestNodeID := "" for _, node := range nodes { score := node.ProductFitScore if score <= 0 { continue } matched := false for _, candidate := range append(append([]string{}, node.DerivedTags.Relevance...), node.DerivedTags.Recency...) { if patrolTagMatchKey(candidate) == tagKey { matched = true break } } if !matched && patrolTagMatchKey(node.Label) != tagKey { continue } if score > best { best = score bestNodeID = node.ID } } if best > 0 { return best } _ = bestNodeID return defaultPatrolProductFit } func patrolTagMatchKey(tag string) string { tag = strings.TrimSpace(tag) for _, suffix := range []string{" 推薦", " 請問", " 怎麼辦", " 好用嗎", " 有人用過嗎", " 有推薦嗎", " 請益"} { if strings.HasSuffix(tag, suffix) { tag = strings.TrimSuffix(tag, suffix) break } } return strings.TrimSpace(tag) } // ResolveTagQueries prefers explicit patrol keywords over graph node selection. func ResolveTagQueries(nodes []libkg.Node, patrolKeywords []string, provider websearch.Provider) []TagQuery { if len(patrolKeywords) > 0 { return CollectPatrolTagQueries(patrolKeywords, nodes, provider) } return CollectTagQueries(nodes, provider) }