2026-06-25 08:20:03 +00:00
|
|
|
|
package placement
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ShouldTryCrawlerFirst reports whether discover should attempt Playwright before Threads API
|
|
|
|
|
|
// to minimize official API calls when a browser session is available.
|
|
|
|
|
|
func ShouldTryCrawlerFirst(m MemberContext) bool {
|
|
|
|
|
|
if !m.AllowsCrawler || !m.BrowserConnected || m.CrawlerBlocked() {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
switch m.SearchSourceMode {
|
|
|
|
|
|
case SearchSourceCrawler, SearchSourceThreadsCrawler, SearchSourceBraveCrawler:
|
|
|
|
|
|
return true
|
|
|
|
|
|
case SearchSourceMixed, SearchSourceThreadsBrave:
|
|
|
|
|
|
return true
|
|
|
|
|
|
default:
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CrawlerBlocked returns true when the mode is API-only or web-search-only.
|
|
|
|
|
|
func (m MemberContext) CrawlerBlocked() bool {
|
|
|
|
|
|
switch m.SearchSourceMode {
|
|
|
|
|
|
case SearchSourceThreads, SearchSourceBrave:
|
|
|
|
|
|
return true
|
|
|
|
|
|
default:
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CrawlerFallbackAllowed returns true when crawler may be used after API/web search fails.
|
|
|
|
|
|
func (m MemberContext) CrawlerFallbackAllowed() bool {
|
|
|
|
|
|
if !m.AllowsCrawler || !m.BrowserConnected {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
switch m.SearchSourceMode {
|
|
|
|
|
|
case SearchSourceThreadsCrawler, SearchSourceBraveCrawler, SearchSourceMixed:
|
|
|
|
|
|
return true
|
|
|
|
|
|
default:
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// HasDiscoverPath reports whether at least one discover backend is configured and connected.
|
|
|
|
|
|
func (m MemberContext) HasDiscoverPath() bool {
|
|
|
|
|
|
if m.AllowsCrawler && m.BrowserConnected {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
if m.AllowsThreadsAPI && m.ApiConnected {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
if m.AllowsBrave && m.WebSearchAPIKey() != "" {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// DiscoverPathLabel summarizes the active routing for job progress UI.
|
|
|
|
|
|
func (m MemberContext) DiscoverPathLabel() string {
|
|
|
|
|
|
if ShouldTryCrawlerFirst(m) {
|
|
|
|
|
|
if m.AllowsThreadsAPI && m.ApiConnected {
|
|
|
|
|
|
return "爬蟲優先(不足再 API)"
|
|
|
|
|
|
}
|
|
|
|
|
|
return "爬蟲"
|
|
|
|
|
|
}
|
|
|
|
|
|
if m.SearchSourceMode == SearchSourceCrawler {
|
|
|
|
|
|
return "爬蟲"
|
|
|
|
|
|
}
|
|
|
|
|
|
if m.AllowsThreadsAPI && m.ApiConnected {
|
|
|
|
|
|
return "Threads API"
|
|
|
|
|
|
}
|
|
|
|
|
|
if m.AllowsBrave {
|
|
|
|
|
|
return m.WebSearchProviderLabel()
|
|
|
|
|
|
}
|
|
|
|
|
|
return string(m.SearchSourceMode)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func discoverMissingPathError(m MemberContext) error {
|
|
|
|
|
|
switch m.SearchSourceMode {
|
|
|
|
|
|
case SearchSourceCrawler:
|
|
|
|
|
|
return fmt.Errorf("請先同步 Chrome Session 以使用爬蟲搜尋")
|
|
|
|
|
|
case SearchSourceThreadsCrawler, SearchSourceBraveCrawler:
|
|
|
|
|
|
if !m.BrowserConnected && !m.ApiConnected && m.WebSearchAPIKey() == "" {
|
|
|
|
|
|
return fmt.Errorf("請同步 Chrome Session 或完成 Threads API / Web Search 連線")
|
|
|
|
|
|
}
|
|
|
|
|
|
if !m.BrowserConnected {
|
|
|
|
|
|
return fmt.Errorf("爬蟲優先模式建議先同步 Chrome Session;亦可改用僅 Threads API")
|
|
|
|
|
|
}
|
|
|
|
|
|
return fmt.Errorf("請完成 Threads API 或 Web Search 連線作為備援")
|
|
|
|
|
|
case SearchSourceThreads, SearchSourceThreadsBrave:
|
|
|
|
|
|
return fmt.Errorf("請先完成 Threads API 連線")
|
|
|
|
|
|
case SearchSourceBrave:
|
|
|
|
|
|
return fmt.Errorf("請在設定頁設定 %s Search API key", m.WebSearchProviderLabel())
|
|
|
|
|
|
case SearchSourceMixed:
|
|
|
|
|
|
return fmt.Errorf("請同步 Chrome Session、完成 Threads API 或設定 Web Search API key 至少一項")
|
|
|
|
|
|
default:
|
|
|
|
|
|
return fmt.Errorf("目前搜尋來源模式無可用管道:%s", m.SearchSourceMode)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func runCrawlerDiscover(ctx context.Context, req DiscoverRequest) ([]DiscoverPost, error) {
|
|
|
|
|
|
if req.Crawler == nil {
|
|
|
|
|
|
return nil, fmt.Errorf("crawler search not configured")
|
|
|
|
|
|
}
|
|
|
|
|
|
keyword := CrawlerKeywordFromQuery(req.Query, req.Keyword)
|
|
|
|
|
|
if keyword == "" {
|
|
|
|
|
|
return nil, fmt.Errorf("crawler keyword is empty")
|
|
|
|
|
|
}
|
|
|
|
|
|
return req.Crawler(ctx, req.Member, keyword, req.Limit)
|
2026-06-25 09:34:28 +00:00
|
|
|
|
}
|