2026-08-03 05:52:02 +00:00
|
|
|
|
package domain
|
|
|
|
|
|
|
|
|
|
|
|
import "strings"
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
SuggestUsageInclude = "include"
|
|
|
|
|
|
SuggestUsageExclude = "exclude"
|
|
|
|
|
|
|
2026-08-13 02:22:24 +00:00
|
|
|
|
SuggestBasisAudience = "audience"
|
|
|
|
|
|
SuggestBasisPain = "pain"
|
|
|
|
|
|
SuggestBasisContext = "context"
|
|
|
|
|
|
SuggestBasisTag = "tag"
|
|
|
|
|
|
SuggestBasisCapability = "capability"
|
|
|
|
|
|
SuggestBasisExclude = "exclude"
|
|
|
|
|
|
|
2026-08-03 05:52:02 +00:00
|
|
|
|
// 建議數量上限:清單要能一眼看完並逐條決定,不是丟一大串讓人放棄。
|
|
|
|
|
|
MaxSuggestions = 20
|
|
|
|
|
|
DefaultSuggestions = 8
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-13 02:22:24 +00:00
|
|
|
|
func IsSuggestionBasisKind(kind string) bool {
|
|
|
|
|
|
switch strings.ToLower(strings.TrimSpace(kind)) {
|
|
|
|
|
|
case SuggestBasisAudience, SuggestBasisPain, SuggestBasisContext, SuggestBasisTag, SuggestBasisCapability, SuggestBasisExclude:
|
|
|
|
|
|
return true
|
|
|
|
|
|
default:
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-03 05:52:02 +00:00
|
|
|
|
/*
|
|
|
|
|
|
WatchTermSuggestion 是一則關鍵字建議。
|
|
|
|
|
|
|
|
|
|
|
|
Reason 是必填的:使用者要逐條決定採不採用,看不到「為什麼建議這個詞」就只能全採或全不採,
|
|
|
|
|
|
那這個功能就退化成一個猜測產生器。
|
|
|
|
|
|
*/
|
|
|
|
|
|
type WatchTermSuggestion struct {
|
2026-08-13 02:22:24 +00:00
|
|
|
|
Term string `json:"term"`
|
|
|
|
|
|
Reason string `json:"reason"`
|
|
|
|
|
|
Usage string `json:"usage"`
|
|
|
|
|
|
BasisKind string `json:"basis_kind,omitempty"`
|
|
|
|
|
|
BasisText string `json:"basis_text,omitempty"`
|
2026-08-03 05:52:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NormalizeSuggestUsage(s string) string {
|
|
|
|
|
|
if strings.EqualFold(strings.TrimSpace(s), SuggestUsageExclude) {
|
|
|
|
|
|
return SuggestUsageExclude
|
|
|
|
|
|
}
|
|
|
|
|
|
return SuggestUsageInclude
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
CleanSuggestions 收掉空白與重複,丟掉沒有理由的項目,並套用數量上限。
|
|
|
|
|
|
|
|
|
|
|
|
沒有理由的項目直接丟:補一句「AI 建議」等於假裝有理由,比少一則更糟。
|
2026-08-09 07:57:35 +00:00
|
|
|
|
include 關鍵字必須通過 Threads 短詞規則(IsThreadsSearchable);不合規整條丟掉、不截短,
|
|
|
|
|
|
避免產出半截怪詞。exclude 仍用較寬的長度界線(訂閱排除詞可能較長)。
|
2026-08-03 05:52:02 +00:00
|
|
|
|
*/
|
|
|
|
|
|
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 {
|
2026-08-09 07:57:35 +00:00
|
|
|
|
term := NormalizeSearchTerm(s.Term)
|
2026-08-03 05:52:02 +00:00
|
|
|
|
reason := strings.TrimSpace(s.Reason)
|
|
|
|
|
|
if term == "" || reason == "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-08-09 07:57:35 +00:00
|
|
|
|
usage := NormalizeSuggestUsage(s.Usage)
|
|
|
|
|
|
if usage == SuggestUsageInclude {
|
|
|
|
|
|
if !IsThreadsSearchable(term) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// exclude: keep broader length; still reject empty after normalize
|
|
|
|
|
|
n := len([]rune(term))
|
|
|
|
|
|
if n < MinTermLen || n > MaxTermLen {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-08-03 05:52:02 +00:00
|
|
|
|
}
|
2026-08-09 07:57:35 +00:00
|
|
|
|
key := strings.ToLower(term)
|
|
|
|
|
|
if seen[key] {
|
2026-08-03 05:52:02 +00:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-08-09 07:57:35 +00:00
|
|
|
|
seen[key] = true
|
2026-08-13 02:22:24 +00:00
|
|
|
|
out = append(out, WatchTermSuggestion{
|
|
|
|
|
|
Term: term, Reason: reason, Usage: usage,
|
|
|
|
|
|
BasisKind: strings.TrimSpace(s.BasisKind), BasisText: strings.TrimSpace(s.BasisText),
|
|
|
|
|
|
})
|
2026-08-03 05:52:02 +00:00
|
|
|
|
if len(out) >= limit {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|