import { useEffect, useState } from "react"; import { api, ThreadsConnection } from "../api/haixun"; import { getAccessToken } from "../api/client"; import { readActiveThreadsAccountId } from "../lib/activeAccount"; import { isExtensionBridgePresent, pingExtensionBridge, requestExtensionSync, waitForExtensionBridge, } from "../lib/extensionSync"; import { ExtensionInstallCard } from "./ExtensionInstallCard"; import { Badge, Button, Card, Field, Textarea } from "./ui"; type DevToolsPanelProps = { accountId: string; connection: ThreadsConnection; onConnectionChange: (data: ThreadsConnection) => void; onMessage: (message: string) => void; onError: (message: string) => void; }; export function DevToolsPanel({ accountId, connection, onConnectionChange, onMessage, onError, }: DevToolsPanelProps) { const [open, setOpen] = useState(true); const [extensionReady, setExtensionReady] = useState(false); const [syncBusy, setSyncBusy] = useState(false); const [manualState, setManualState] = useState(""); const [importBusy, setImportBusy] = useState(false); const [showManualImport, setShowManualImport] = useState(false); useEffect(() => { let cancelled = false; const onBridgeMessage = (event: MessageEvent) => { if (event.source !== window) return; if (event.data?.type === "HAIXUN_EXTENSION_READY") setExtensionReady(true); }; window.addEventListener("message", onBridgeMessage); const timer = window.setInterval(() => { if (cancelled) return; if (isExtensionBridgePresent()) { setExtensionReady(true); return; } pingExtensionBridge(); }, 1500); pingExtensionBridge(); void waitForExtensionBridge(6000).then((ready) => { if (!cancelled && ready) setExtensionReady(true); }); return () => { cancelled = true; window.removeEventListener("message", onBridgeMessage); window.clearInterval(timer); }; }, []); const reloadConnection = async () => { const data = await api.getThreadsConnection(accountId); onConnectionChange(data); return data; }; const syncChromeSession = async () => { setSyncBusy(true); onError(""); onMessage(""); try { const ready = extensionReady || isExtensionBridgePresent() || (await waitForExtensionBridge(4000)); if (!ready) { throw new Error( "找不到巡樓 Chrome 擴充。請到 chrome://extensions 載入 extension/haixun-threads-sync,按「重新載入」後刷新此頁(F5)", ); } setExtensionReady(true); await api.memberMe(); const resolvedAccountId = accountId || readActiveThreadsAccountId(); if (!resolvedAccountId) { throw new Error("請先在頂部選擇經營帳號,或到 Threads 帳號頁設為使用中後再同步"); } const result = await requestExtensionSync({ accountId: resolvedAccountId, serverUrl: window.location.origin, }); if (result.success === false || result.valid === false) { throw new Error(result.message || "Chrome session 同步失敗"); } onMessage(result.message || "Chrome session 已同步"); await reloadConnection(); } catch (err) { onError(err instanceof Error ? err.message : "Chrome session 同步失敗"); } finally { setSyncBusy(false); } }; const importManualSession = async () => { const trimmed = manualState.trim(); if (!trimmed) { onError("請貼上 Playwright storage state JSON"); return; } setImportBusy(true); onError(""); onMessage(""); try { JSON.parse(trimmed); const data = await api.importThreadsAccountSession(accountId, trimmed); if (!data.valid) throw new Error(data.message || "Session 驗證失敗"); setManualState(""); onMessage(data.message || "Session 已匯入"); await reloadConnection(); } catch (err) { if (err instanceof SyntaxError) { onError("JSON 格式不正確,請貼上完整的 Playwright storage state"); } else { onError(err instanceof Error ? err.message : "匯入失敗"); } } finally { setImportBusy(false); } }; return (
{open ? (
{connection.browser_connected ? "Chrome Session 已同步" : "Chrome Session 未同步"} {extensionReady ? "擴充已偵測" : "尚未偵測擴充"}
{showManualImport ? (