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" 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 LikeCount int ReplyCount int Source DiscoverChannel } // Discover runs keyword discovery respecting the member's connection prefs. // Formal mode (dev_mode=false) never falls back to crawler. func Discover(ctx context.Context, req DiscoverRequest) ([]DiscoverPost, DiscoverChannel, error) { m := req.Member if m.DevMode { if !m.BrowserConnected { return nil, "", fmt.Errorf("開發模式需先同步 Chrome Session") } if req.Crawler == nil { return nil, DiscoverCrawler, fmt.Errorf("crawler search not configured") } keyword := CrawlerKeywordFromQuery(req.Query, req.Keyword) if keyword == "" { return nil, DiscoverCrawler, fmt.Errorf("crawler keyword is empty") } posts, err := req.Crawler(ctx, m, keyword, req.Limit) if err != nil { return nil, DiscoverCrawler, err } return posts, DiscoverCrawler, nil } if m.AllowsThreadsAPI { if !m.ApiConnected { return nil, "", fmt.Errorf("正式模式需先完成 Threads API 連線") } posts, err := keywordSearchViaThreadsAPI(ctx, req) if err == nil && len(posts) > 0 { return posts, DiscoverThreadsAPI, nil } if err != nil && !m.AllowsBrave { return nil, "", err } } if m.AllowsBrave { if m.BraveAPIKey == "" { return nil, "", fmt.Errorf("請在設定頁設定 Brave Search API key(跟隨此登入帳號)") } return nil, DiscoverBrave, fmt.Errorf("brave threads discover delegated to worker") } return nil, "", fmt.Errorf("目前搜尋來源模式無可用管道:%s", m.SearchSourceMode) }