haixunMaster/haixun-backend/internal/library/placement/discover.go

110 lines
3.0 KiB
Go
Raw Normal View History

2026-06-24 10:02:42 +00:00
package placement
import (
"context"
"fmt"
)
// DiscoverChannel identifies which backend fulfilled a placement discover query.
type DiscoverChannel string
const (
DiscoverThreadsAPI DiscoverChannel = "threads_api"
DiscoverBrave DiscoverChannel = "brave"
2026-06-25 08:20:03 +00:00
DiscoverExa DiscoverChannel = "exa"
2026-06-24 10:02:42 +00:00
DiscoverCrawler DiscoverChannel = "crawler"
)
// DiscoverRequest is used by scan jobs; expand-graph only uses Brave knowledge_expand.
type DiscoverRequest struct {
Query string
Keyword string // plain tag for crawler; optional
Recency bool
Limit int
Member MemberContext
Crawler CrawlerSearchFn
}
type DiscoverPost struct {
2026-06-25 08:20:03 +00:00
Text string
Permalink string
ExternalID string
Author string
PostedAt string
AuthorVerified bool
FollowerCount int
LikeCount int
ReplyCount int
Source DiscoverChannel
2026-06-24 10:02:42 +00:00
}
2026-06-25 08:20:03 +00:00
// Discover runs keyword discovery respecting search_source_mode and available connections.
// Crawler-first modes skip Threads API when the browser session returns posts (saves API quota).
2026-06-24 10:02:42 +00:00
func Discover(ctx context.Context, req DiscoverRequest) ([]DiscoverPost, DiscoverChannel, error) {
m := req.Member
2026-06-25 08:20:03 +00:00
if !m.HasDiscoverPath() {
return nil, "", discoverMissingPathError(m)
}
if ShouldTryCrawlerFirst(m) {
posts, err := runCrawlerDiscover(ctx, req)
if err == nil && len(posts) > 0 {
return posts, DiscoverCrawler, nil
2026-06-24 10:02:42 +00:00
}
2026-06-25 08:20:03 +00:00
if m.SearchSourceMode == SearchSourceCrawler {
if err != nil {
return nil, DiscoverCrawler, err
}
return posts, DiscoverCrawler, nil
2026-06-24 10:02:42 +00:00
}
}
if m.AllowsThreadsAPI {
if !m.ApiConnected {
2026-06-25 08:20:03 +00:00
if !m.CrawlerFallbackAllowed() {
return nil, "", fmt.Errorf("正式模式需先完成 Threads API 連線")
}
} else {
posts, err := keywordSearchViaThreadsAPI(ctx, req)
if err == nil && len(posts) > 0 {
return posts, DiscoverThreadsAPI, nil
}
if err != nil {
if m.CrawlerFallbackAllowed() {
cPosts, cErr := runCrawlerDiscover(ctx, req)
if cErr == nil && len(cPosts) > 0 {
return cPosts, DiscoverCrawler, nil
}
}
if !m.AllowsBrave && !m.CrawlerFallbackAllowed() {
// Optional API field gaps must not fail the whole patrol; return empty for this keyword.
return []DiscoverPost{}, DiscoverThreadsAPI, nil
}
}
2026-06-24 10:02:42 +00:00
}
}
if m.AllowsBrave {
2026-06-25 08:20:03 +00:00
if m.WebSearchAPIKey() == "" {
if m.CrawlerFallbackAllowed() {
posts, err := runCrawlerDiscover(ctx, req)
if err == nil {
return posts, DiscoverCrawler, nil
}
}
return nil, "", fmt.Errorf("請在設定頁設定 %s Search API key跟隨此登入帳號", m.WebSearchProviderLabel())
2026-06-24 10:02:42 +00:00
}
2026-06-25 08:20:03 +00:00
return nil, m.WebSearchDiscoverChannel(), fmt.Errorf("web search threads discover delegated to worker")
}
if m.CrawlerFallbackAllowed() {
posts, err := runCrawlerDiscover(ctx, req)
if err != nil {
return nil, DiscoverCrawler, err
}
return posts, DiscoverCrawler, nil
2026-06-24 10:02:42 +00:00
}
return nil, "", fmt.Errorf("目前搜尋來源模式無可用管道:%s", m.SearchSourceMode)
2026-06-25 09:34:28 +00:00
}