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

129 lines
4.0 KiB
Go
Raw Permalink Normal View History

2026-07-13 08:59:13 +00:00
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)
}