package domain import "strings" const ( SuggestUsageInclude = "include" SuggestUsageExclude = "exclude" SuggestBasisAudience = "audience" SuggestBasisPain = "pain" SuggestBasisContext = "context" SuggestBasisTag = "tag" SuggestBasisCapability = "capability" SuggestBasisExclude = "exclude" // 建議數量上限:清單要能一眼看完並逐條決定,不是丟一大串讓人放棄。 MaxSuggestions = 20 DefaultSuggestions = 8 ) func IsSuggestionBasisKind(kind string) bool { switch strings.ToLower(strings.TrimSpace(kind)) { case SuggestBasisAudience, SuggestBasisPain, SuggestBasisContext, SuggestBasisTag, SuggestBasisCapability, SuggestBasisExclude: return true default: return false } } /* WatchTermSuggestion 是一則關鍵字建議。 Reason 是必填的:使用者要逐條決定採不採用,看不到「為什麼建議這個詞」就只能全採或全不採, 那這個功能就退化成一個猜測產生器。 */ type WatchTermSuggestion struct { Term string `json:"term"` Reason string `json:"reason"` Usage string `json:"usage"` BasisKind string `json:"basis_kind,omitempty"` BasisText string `json:"basis_text,omitempty"` } func NormalizeSuggestUsage(s string) string { if strings.EqualFold(strings.TrimSpace(s), SuggestUsageExclude) { return SuggestUsageExclude } return SuggestUsageInclude } /* CleanSuggestions 收掉空白與重複,丟掉沒有理由的項目,並套用數量上限。 沒有理由的項目直接丟:補一句「AI 建議」等於假裝有理由,比少一則更糟。 include 必須能在 Threads 搜到:已合規的原詞保留;過長或超過兩個 token 的先收成短詞變體, 變體也沒有才丟掉。exclude 仍用較寬的長度界線(訂閱排除詞可能較長)。 */ func CleanSuggestions(in []WatchTermSuggestion, limit int) []WatchTermSuggestion { if limit <= 0 || limit > MaxSuggestions { limit = MaxSuggestions } out := make([]WatchTermSuggestion, 0, len(in)) seen := map[string]bool{} for _, s := range in { term := NormalizeSearchTerm(s.Term) reason := strings.TrimSpace(s.Reason) if term == "" || reason == "" { continue } usage := NormalizeSuggestUsage(s.Usage) basisText := strings.TrimSpace(s.BasisText) if usage == SuggestUsageInclude { if !IsThreadsSearchable(term) { variants := SearchableTermVariants(term, true) if len(variants) == 0 { continue } if basisText == "" { basisText = term } term = variants[0] } } else { // exclude: keep broader length; still reject empty after normalize n := len([]rune(term)) if n < MinTermLen || n > MaxTermLen { continue } } key := strings.ToLower(term) if seen[key] { continue } seen[key] = true out = append(out, WatchTermSuggestion{ Term: term, Reason: reason, Usage: usage, BasisKind: strings.TrimSpace(s.BasisKind), BasisText: basisText, }) if len(out) >= limit { break } } return out }