thread-master/apps/backend/internal/module/scout/usecase/planner.go

129 lines
4.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package usecase
import (
"crypto/sha256"
"encoding/hex"
"net/url"
"strconv"
"strings"
"apps/backend/internal/module/scout/domain"
)
type classifiedPost struct {
classification string
score int
reason string
}
func planScanTerms(brief *domain.RunBrief) []string {
return dedupeTerms(brief.Pains, brief.Tags, brief.Periphery, []string{brief.Intent})
}
func dedupeTerms(groups ...[]string) []string {
seen := make(map[string]struct{})
var out []string
for _, group := range groups {
for _, term := range group {
term = strings.Join(strings.Fields(strings.TrimSpace(term)), " ")
key := strings.ToLower(term)
if term == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, term)
}
}
return out
}
func classifyPost(mode, text string, terms []string) classifiedPost {
lower := strings.ToLower(text)
signals := matchedSignals(lower, terms)
if hasAny(lower, "giveaway", "抽獎", "follow for follow", "互追", "crypto", "賺錢") {
return classifiedPost{domain.ClassificationNoise, 0, "noise signal"}
}
if mode == domain.ModeActivity {
return classifyActivity(lower, signals)
}
if hasAny(lower, "dm me", "私訊我", "服務洽詢", "立即購買", "限時優惠", "團購", "業配") {
return scored(domain.ClassificationProviderOffer, 35, signals, "provider-offer signal")
}
if hasAny(lower, "推薦", "求推", "有沒有推薦", "any recommendation", "what do you recommend") {
return scored(domain.ClassificationSeekingRecommendation, 60, signals, "recommendation signal")
}
if strings.Contains(lower, "?") || strings.Contains(lower, "") || hasAny(lower, "怎麼", "如何", "請問", "求助", "help") {
return scored(domain.ClassificationSeekingHelp, 55, signals, "help-seeking signal")
}
return scored(domain.ClassificationDiscussion, 40, signals, "discussion signal")
}
func classifyActivity(text string, signals []string) classifiedPost {
if hasAny(text, "dm me", "私訊我", "服務洽詢", "立即購買", "限時優惠", "團購", "業配") {
return scored(domain.ClassificationProviderOffer, 35, signals, "provider-offer signal; recency unavailable (neutral)")
}
if strings.Contains(text, "?") || strings.Contains(text, "") || hasAny(text, "請問", "怎麼", "如何", "有人知道", "求") {
return scored(domain.ClassificationAsking, 60, signals, "asking signal; recency unavailable (neutral)")
}
if hasAny(text, "活動", "event", "開幕", "launch", "發布", "報名", "登記", "公告") {
return scored(domain.ClassificationAnnouncement, 50, signals, "announcement signal; recency unavailable (neutral)")
}
return scored(domain.ClassificationDiscussion, 40, signals, "discussion signal; recency unavailable (neutral)")
}
func scored(classification string, base int, signals []string, label string) classifiedPost {
score := base + len(signals)*15
if score > 100 {
score = 100
}
reason := label
if len(signals) > 0 {
reason += "; matched: " + strings.Join(signals, ", ")
}
return classifiedPost{classification, score, reason}
}
func matchedSignals(text string, terms []string) []string {
var signals []string
for _, term := range dedupeTerms(terms) {
if strings.Contains(text, strings.ToLower(term)) {
signals = append(signals, term)
}
}
return signals
}
func hasAny(text string, signals ...string) bool {
for _, signal := range signals {
if strings.Contains(text, signal) {
return true
}
}
return false
}
func canonicalPermalink(raw string) string {
u, err := url.Parse(strings.TrimSpace(raw))
if err != nil || u.Host == "" {
return ""
}
u.Scheme = "https"
u.Host = strings.ToLower(u.Host)
u.RawQuery = ""
u.Fragment = ""
u.Path = strings.TrimRight(u.Path, "/")
return u.String()
}
func permalinkID(ownerUID int64, permalink string) string {
sum := sha256.Sum256([]byte(strings.TrimSpace(permalink) + "|" + formatOwnerUID(ownerUID)))
return "sp_" + hex.EncodeToString(sum[:])[:20]
}
func formatOwnerUID(ownerUID int64) string {
return strconv.FormatInt(ownerUID, 10)
}