109 lines
3.0 KiB
Go
109 lines
3.0 KiB
Go
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"
|
||
DiscoverExa DiscoverChannel = "exa"
|
||
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 {
|
||
Text string
|
||
Permalink string
|
||
ExternalID string
|
||
Author string
|
||
PostedAt string
|
||
AuthorVerified bool
|
||
FollowerCount int
|
||
LikeCount int
|
||
ReplyCount int
|
||
Source DiscoverChannel
|
||
}
|
||
|
||
// 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).
|
||
func Discover(ctx context.Context, req DiscoverRequest) ([]DiscoverPost, DiscoverChannel, error) {
|
||
m := req.Member
|
||
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
|
||
}
|
||
if m.SearchSourceMode == SearchSourceCrawler {
|
||
if err != nil {
|
||
return nil, DiscoverCrawler, err
|
||
}
|
||
return posts, DiscoverCrawler, nil
|
||
}
|
||
}
|
||
|
||
if m.AllowsThreadsAPI {
|
||
if !m.ApiConnected {
|
||
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
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if m.AllowsBrave {
|
||
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())
|
||
}
|
||
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
|
||
}
|
||
|
||
return nil, "", fmt.Errorf("目前搜尋來源模式無可用管道:%s", m.SearchSourceMode)
|
||
} |