package viral import ( "context" "encoding/xml" "html" "net/http" "net/url" "regexp" "strings" "time" ) var rssTagRE = regexp.MustCompile(`<[^>]+>`) type freeRSS struct { Channel struct { Items []freeRSSItem `xml:"item"` } `xml:"channel"` } type freeRSSItem struct { Title string `xml:"title"` Description string `xml:"description"` Link string `xml:"link"` } func CollectFreeTopicTrends(ctx context.Context, keyword, personaBrief string) []MissionInspireTrendSnippet { queries := freeTrendFeedQueries(keyword, personaBrief) client := &http.Client{Timeout: 7 * time.Second} out := make([]MissionInspireTrendSnippet, 0, 12) seen := map[string]struct{}{} add := func(query, title, snippet, link string) { title = cleanRSSField(title) snippet = cleanRSSField(snippet) link = strings.TrimSpace(link) if title == "" && snippet == "" { return } key := strings.ToLower(title + "|" + snippet) if _, ok := seen[key]; ok { return } seen[key] = struct{}{} out = append(out, MissionInspireTrendSnippet{Query: query, Title: title, Snippet: snippet, URL: link}) } for _, feed := range queries { if len(out) >= 12 { break } req, err := http.NewRequestWithContext(ctx, http.MethodGet, feed.url, nil) if err != nil { continue } req.Header.Set("User-Agent", "HaixunTopicBot/1.0") resp, err := client.Do(req) if err != nil || resp == nil { continue } func() { defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return } var parsed freeRSS if err := xml.NewDecoder(resp.Body).Decode(&parsed); err != nil { return } for _, item := range parsed.Channel.Items { if len(out) >= 12 { break } add(feed.label, item.Title, item.Description, item.Link) } }() } return out } type freeTrendFeed struct { label string url string } func freeTrendFeedQueries(keyword, personaBrief string) []freeTrendFeed { feeds := []freeTrendFeed{ {label: "Google Trends 台灣每日趨勢", url: "https://trends.google.com/trends/trendingsearches/daily/rss?geo=TW"}, {label: "Google Trends 台灣即時趨勢", url: "https://trends.google.com/trends/trendingsearches/realtime/rss?geo=TW&category=all"}, } terms := []string{strings.TrimSpace(keyword)} brief := strings.TrimSpace(personaBrief) if brief != "" { if len([]rune(brief)) > 18 { brief = string([]rune(brief)[:18]) } terms = append(terms, brief) } for _, term := range terms { if term == "" { continue } q := url.QueryEscape(term + " 共鳴 OR 痛點 OR 趨勢") feeds = append(feeds, freeTrendFeed{ label: "Google News 搜尋:" + term, url: "https://news.google.com/rss/search?q=" + q + "&hl=zh-TW&gl=TW&ceid=TW:zh-Hant", }) } return feeds } func cleanRSSField(raw string) string { raw = html.UnescapeString(raw) raw = rssTagRE.ReplaceAllString(raw, " ") raw = strings.Join(strings.Fields(raw), " ") if len([]rune(raw)) > 220 { return string([]rune(raw)[:220]) + "…" } return raw }