import { useCallback, useEffect, useState } from 'react'; import { AlertTriangle, Braces, Check, Cookie, ExternalLink, Gauge, Network, Radio, RefreshCw, UserRoundCog, X, } from 'lucide-react'; import { browser } from 'wxt/browser'; import { YakMark } from '@/components/brand/Brand'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipProvider } from '@/components/ui/tooltip'; import { HANDOFF_REASON_LABELS, waitingHandoff } from '@/features/handoff/presentation'; import { isStateStorageChange } from '@/protocol/storage'; import type { ActiveTabInfo, BridgeStatus, ExtensionState, UserAgentResolution } from '@/types/models'; import { errorMessage, request } from '@/platform/messaging/runtime'; import { CookieQuickView } from './views/CookieQuickView'; import { OverviewQuickView } from './views/OverviewQuickView'; import { ProxyQuickView } from './views/ProxyQuickView'; import { UserAgentQuickView } from './views/UserAgentQuickView'; import './App.css'; type PopupView = 'home' | 'proxy' | 'cookies' | 'user-agent'; const FULL_VIEW_TARGETS: Record = { home: { section: 'overview', label: '打开完整工作台' }, proxy: { section: 'rules', label: '打开代理策略' }, cookies: { section: 'cookies', label: '打开完整 Cookie Editor' }, 'user-agent': { section: 'user-agent', label: '打开 User-Agent 管理' }, }; function engineStatusLabel(state: ExtensionState, bridge: BridgeStatus): string { if (bridge.state === 'connected') return '引擎在线'; if (bridge.state === 'connecting') return '正在连接引擎'; if (bridge.state === 'negotiating') return '正在验证引擎身份'; if (bridge.state === 'error') return bridge.message || '引擎连接失败'; return state.bridge.pairedEngine ? bridge.message || '引擎离线' : '尚未配对引擎'; } function App() { const [state, setState] = useState(); const [tab, setTab] = useState(); const [bridge, setBridge] = useState({ state: 'disconnected', message: '未连接引擎' }); const [busy, setBusy] = useState(false); const [notice, setNotice] = useState(''); const [view, setView] = useState('home'); const [cookieCount, setCookieCount] = useState(0); const [uaResolution, setUaResolution] = useState(); const load = useCallback(async () => { const [nextState, nextTab, nextBridge] = await Promise.all([ request('state.get'), request('tab.active').catch(() => undefined), request('bridge.status'), ]); setState(nextState); setTab(nextTab); setBridge(nextBridge); if (nextTab?.url?.startsWith('http')) { const [cookies, resolution] = await Promise.all([ request('cookie.list', { url: nextTab.url, tabId: nextTab.id }).catch(() => []), request('ua.resolve', { url: nextTab.url }).catch(() => undefined), ]); setCookieCount(cookies.length); setUaResolution(resolution); } else { setCookieCount(0); setUaResolution(undefined); } }, []); useEffect(() => { void load(); const listener = (message: { action?: string; payload?: BridgeStatus }) => { if (message.action === 'bridge.status.changed' && message.payload) setBridge(message.payload); }; browser.runtime.onMessage.addListener(listener); const onStorageChange = (changes: Record) => { if (isStateStorageChange(changes)) void load(); }; browser.storage.onChanged.addListener(onStorageChange); return () => { browser.runtime.onMessage.removeListener(listener); browser.storage.onChanged.removeListener(onStorageChange); }; }, [load]); useEffect(() => { if (!notice) return undefined; const timer = globalThis.setTimeout(() => setNotice(''), 2_400); return () => globalThis.clearTimeout(timer); }, [notice]); const grantActive = Boolean(state?.activeGrant && state.activeGrant.expiresAt > Date.now() && tab && state.activeGrant.targets.some((target) => target.tabId === tab.id)); const handoff = waitingHandoff(state?.handoff); const run = async (task: () => Promise, success?: string) => { setBusy(true); setNotice(''); try { await task(); if (success) setNotice(success); } catch (error) { setNotice(errorMessage(error)); } finally { setBusy(false); } }; const openTool = (tool: string) => { const target = tab ? `?tabId=${tab.id}` : ''; return browser.tabs.create({ url: browser.runtime.getURL(`/options.html${target}#${tool}`) }); }; const toggleEngine = () => run(async () => { if (!state!.bridge.pairedEngine) { await request('bridge.pair'); await openTool('engine'); return; } if (bridge.state === 'connected') await request('bridge.disconnect'); else await request('bridge.connect'); setBridge(await request('bridge.status')); }); const capture = () => run(async () => { const context = await request('context.capture', { includeDom: true, includeStorage: true, includeCookies: true, tabId: tab?.id, }); await navigator.clipboard.writeText(JSON.stringify(context, null, 2)); setNotice('页面上下文已复制'); }); if (!state) { return
正在读取浏览器状态
; } const engineBusy = bridge.state === 'connecting' || bridge.state === 'negotiating'; const currentHost = (() => { try { return tab?.url ? new URL(tab.url).host : ''; } catch { return ''; } })(); const statusLabel = engineStatusLabel(state, bridge); const statusActionLabel = bridge.state === 'connected' ? `${statusLabel},点击断开` : state.bridge.pairedEngine ? `${statusLabel},点击连接` : `${statusLabel},点击配对`; const fullViewTarget = FULL_VIEW_TARGETS[view]; return (
{tab?.favIconUrl ? : } {tab?.title || '当前页面不可访问'}
{currentHost || '无法读取当前标签页'}