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) }