"use client"; import { useEffect, useMemo, useState } from "react"; import { ChevronDown, ChevronUp } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { SEARCH_SOURCE_MODE_OPTIONS, searchSourceModeLabel, type SearchSourceMode, } from "@/lib/search/source-mode"; export type ConnectionPreset = "sync" | "api" | "hybrid" | "custom"; export interface ThreadsConnectionSettingsData { searchViaApi?: boolean; searchSourceMode?: SearchSourceMode; publishViaApi?: boolean; devMode?: boolean; scrapeReplies?: boolean; repliesPerPost?: number; publishHeaded?: boolean; playwrightDebug?: boolean; } interface ThreadsConnectionSettingsProps { settings: ThreadsConnectionSettingsData; accountName?: string | null; onChange: (patch: Partial) => void; } const PRIMARY_SOURCE_MODES: SearchSourceMode[] = [ "mixed", "threads", "brave", "crawler", ]; const COMBO_SOURCE_MODES: SearchSourceMode[] = [ "threads_brave", "threads_crawler", "brave_crawler", ]; function detectPreset(data: ThreadsConnectionSettingsData): ConnectionPreset { const searchApi = data.searchViaApi ?? false; const publishApi = data.publishViaApi ?? false; const dev = data.devMode ?? false; if (searchApi && publishApi && !dev) return "api"; if (!searchApi && !publishApi && dev) return "sync"; if (searchApi && dev) return "hybrid"; return "custom"; } function presetPatch(preset: ConnectionPreset): Partial { switch (preset) { case "api": return { searchViaApi: true, publishViaApi: true, devMode: false, scrapeReplies: false, searchSourceMode: "threads", }; case "sync": return { searchViaApi: false, publishViaApi: false, devMode: true, scrapeReplies: true, searchSourceMode: "crawler", }; case "hybrid": return { searchViaApi: true, publishViaApi: false, devMode: true, scrapeReplies: true, searchSourceMode: "mixed", }; default: return {}; } } function sourceModeHint(mode: SearchSourceMode): string { return SEARCH_SOURCE_MODE_OPTIONS.find((o) => o.value === mode)?.hint ?? ""; } function FlowSummary({ settings }: { settings: ThreadsConnectionSettingsData }) { const mode = settings.searchSourceMode ?? "mixed"; const lines = useMemo(() => { const sourceLine = `海巡來源:${searchSourceModeLabel(mode)}(${sourceModeHint(mode)})`; const search = settings.searchViaApi ? "Threads API:已開啟(需 OAuth + keyword search 權限)" : settings.devMode ? "Threads API:關閉,改走下方選定的來源" : "Threads API:關閉"; const replies = settings.scrapeReplies && settings.devMode ? `留言:瀏覽器爬取(每篇最多 ${settings.repliesPerPost ?? 10} 則)` : settings.scrapeReplies ? "留言:已開啟但 devMode 關閉,實際不會爬留言" : "留言:不抓取(僅用貼文與互動數)"; const publish = settings.publishViaApi ? "發文:Meta API 優先,失敗時改瀏覽器" : "發文:Playwright 瀏覽器(需 Chrome 同步 session)"; return [sourceLine, search, replies, publish]; }, [settings, mode]); return (

目前流程預覽

    {lines.map((line) => (
  • {line}
  • ))}
); } export function ThreadsConnectionSettings({ settings, accountName, onChange, }: ThreadsConnectionSettingsProps) { const [preset, setPreset] = useState("sync"); const [showAdvanced, setShowAdvanced] = useState(false); const [showComboModes, setShowComboModes] = useState(false); const currentMode = settings.searchSourceMode ?? "mixed"; useEffect(() => { setPreset(detectPreset(settings)); setShowComboModes(COMBO_SOURCE_MODES.includes(currentMode)); }, [settings, currentMode]); function applyPreset(next: ConnectionPreset) { setPreset(next); if (next !== "custom") { onChange(presetPatch(next)); } } function applySourceMode(mode: SearchSourceMode) { setPreset("custom"); const patch: Partial = { searchSourceMode: mode }; if (mode === "threads" || mode === "threads_brave" || mode === "threads_crawler") { patch.searchViaApi = true; } if (mode === "crawler" || mode === "threads_crawler" || mode === "brave_crawler") { patch.devMode = true; } if (mode === "mixed") { patch.searchViaApi = true; patch.devMode = true; } onChange(patch); setShowComboModes(COMBO_SOURCE_MODES.includes(mode)); } return (
海巡搜尋來源 選擇要用哪個 provider,或混合模式。Brave 需設定 BRAVE_SEARCH_API_KEY;爬蟲需 Chrome 同步。
{SEARCH_SOURCE_MODE_OPTIONS.filter((o) => PRIMARY_SOURCE_MODES.includes(o.value) ).map((option) => ( ))}
{showComboModes && (
{SEARCH_SOURCE_MODE_OPTIONS.filter((o) => COMBO_SOURCE_MODES.includes(o.value) ).map((option) => ( ))}
)}
連線預設 {accountName ? `目前帳號「${accountName}」的抓取策略。側欄切換帳號會載入各自的設定。` : "請先在側欄建立並選擇經營帳號。"}
{( [ ["sync", "Chrome 同步", "全爬蟲:海巡 + 留言 + 發文"], ["api", "API Key 優先", "全官方 API:不爬留言"], ["hybrid", "混合模式", "API 海巡 + 瀏覽器留言/發文"], ["custom", "自訂", "手動調整下方開關"], ] as const ).map(([value, label, hint]) => ( ))}
{showAdvanced && ( {( [ { key: "searchViaApi" as const, title: "海巡走官方 API", desc: "關閉則用 Playwright 瀏覽器搜尋", }, { key: "publishViaApi" as const, title: "發文走官方 API", desc: "關閉則只用瀏覽器發文", }, { key: "devMode" as const, title: "瀏覽器模式(devMode)", desc: "開啟才允許瀏覽器海巡與爬留言", }, { key: "scrapeReplies" as const, title: "抓取他人貼文留言", desc: "需 devMode 開啟才會實際執行", }, { key: "publishHeaded" as const, title: "發文時顯示瀏覽器視窗", desc: "除錯用,server 上通常保持關閉", }, { key: "playwrightDebug" as const, title: "Playwright 除錯截圖", desc: "卡住時保留瀏覽器截圖", }, ] as const ).map((item) => (

{item.title}

{item.desc}

{ setPreset("custom"); onChange({ [item.key]: checked }); }} />
))}
onChange({ repliesPerPost: parseInt(e.target.value, 10) || 10 }) } />
)}
); }