import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; import { browser, type Browser } from 'wxt/browser'; import { Activity, AlertTriangle, Bot, Braces, Check, ChevronRight, CircleGauge, Cookie, Copy, Database, Download, Eye, EyeOff, GripVertical, History, KeyRound, MousePointer2, Network, Play, Plus, Power, Radio, RefreshCw, Route, Save, Search, Send, Server, ShieldCheck, Square, Trash2, Upload, UserRoundCog, X, } from 'lucide-react'; import { v7 as uuidv7 } from 'uuid'; import { ProductBrand, YakitMark } from '@/components/brand/Brand'; import { Button } from '@/components/ui/button'; import { Field } from '@/components/ui/field'; import { Switch } from '@/components/ui/switch'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { AUDIT_CATEGORY_LABELS, AUDIT_OUTCOME_LABELS, HANDOFF_REASON_LABELS, waitingHandoff, } from '@/features/handoff/presentation'; import { CAPABILITY_LABELS, CONTROL_CAPABILITY_SCOPES, READ_CAPABILITY_SCOPES, isControlScopeSet } from '@/protocol/capabilities'; import { AGENT_RUNTIME_STORAGE_KEY, AUDIT_STORAGE_KEY, isStateStorageChange } from '@/protocol/storage'; import type { ActiveTabInfo, AgentRuntime, AuditEvent, BridgePairingStatus, BridgeStatus, BrowserCookie, BrowserRequestAnalysisBundle, CookieInput, CookieTransferFormat, EnterprisePolicyStatus, ExtensionState, HumanHandoff, NetworkCaptureStatus, NetworkRequestExport, NetworkRequestRecord, PageContext, PageEvalResult, PageFrameSummary, PageNodeDetails, PageNodeSummary, PageObservationRecord, PageObservationStatus, ProxyConfiguration, ProxyProfile, ProxyRule, ProxyRulePreview, ProxyRuleStats, UserAgentRule, YakPocGenerateResult, } from '@/types/models'; import { errorMessage, request } from '@/platform/messaging/runtime'; import { APPEARANCE_STORAGE_KEY, getAppearance, setThemePreference, type ThemePreference } from '@/platform/storage/appearance'; import './App.css'; type Section = 'overview' | 'proxies' | 'rules' | 'cookies' | 'user-agent' | 'network' | 'context' | 'engine' | 'activity'; const FIREFOX_AMO_BUILD = import.meta.env.FIREFOX && import.meta.env.MODE === 'store'; const SECTIONS: Array<{ id: Section; label: string; icon: ReactNode }> = [ { id: 'overview', label: '运行概览', icon: }, { id: 'proxies', label: '代理配置', icon: }, { id: 'rules', label: '代理规则', icon: }, { id: 'cookies', label: 'Cookie Editor', icon: }, { id: 'user-agent', label: 'UA 请求头', icon: }, { id: 'network', label: '网络活动', icon: }, { id: 'context', label: '登录态工作区', icon: }, { id: 'engine', label: '引擎连接', icon: }, { id: 'activity', label: '操作记录', icon: }, ]; const UA_PRESETS = [ ['Chrome / Windows', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'], ['Safari / iPhone', 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1'], ['Googlebot', 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'], ] as const; const CONTEXT_SECTION_LABELS: Record = { capture_options: '采集范围', document: '文档', authentication: '认证', forms: '表单', interactive: '可操作元素', storage: 'Storage', cookies: 'Cookie', }; function Empty({ children }: { children: ReactNode }) { return
{children}
; } function App() { const initialHash = location.hash.slice(1) as Section; const [section, setSection] = useState
(SECTIONS.some((item) => item.id === initialHash) ? initialHash : 'overview'); const [state, setState] = useState(); const [tab, setTab] = useState(); const [tabs, setTabs] = useState([]); const [bridge, setBridge] = useState({ state: 'disconnected', message: '未连接引擎' }); const [busy, setBusy] = useState(false); const [notice, setNotice] = useState<{ kind: 'ok' | 'error'; text: string }>(); const [theme, setTheme] = useState('system'); const load = useCallback(async () => { const requestedTabId = Number(new URLSearchParams(location.search).get('tabId')); const [nextState, nextTab, nextTabs, nextBridge] = await Promise.all([ request('state.get'), Number.isSafeInteger(requestedTabId) && requestedTabId > 0 ? request('tab.get', { tabId: requestedTabId }).catch(() => request('tab.active').catch(() => undefined)) : request('tab.active').catch(() => undefined), request('tab.list'), request('bridge.status'), ]); setState(nextState); setTab(nextTab); setTabs(nextTabs); setBridge(nextBridge); }, []); const refreshTabs = useCallback(async () => { const nextTabs = await request('tab.list'); setTabs(nextTabs); setTab((current) => current ? nextTabs.find((item) => item.id === current.id) : current); }, []); useEffect(() => { void load(); }, [load]); useEffect(() => { let timer: ReturnType | undefined; const scheduleRefresh = () => { if (timer) globalThis.clearTimeout(timer); timer = globalThis.setTimeout(() => void refreshTabs().catch(() => undefined), 80); }; const onCreated = () => scheduleRefresh(); const onUpdated = (_tabId: number, change: Browser.tabs.OnUpdatedInfo) => { if (change.url !== undefined || change.title !== undefined || change.status === 'complete') scheduleRefresh(); }; const onRemoved = () => scheduleRefresh(); browser.tabs.onCreated.addListener(onCreated); browser.tabs.onUpdated.addListener(onUpdated); browser.tabs.onRemoved.addListener(onRemoved); return () => { if (timer) globalThis.clearTimeout(timer); browser.tabs.onCreated.removeListener(onCreated); browser.tabs.onUpdated.removeListener(onUpdated); browser.tabs.onRemoved.removeListener(onRemoved); }; }, [refreshTabs]); useEffect(() => { const listener = (changes: Record) => { if (isStateStorageChange(changes)) void request('state.get').then(setState).catch(() => undefined); }; browser.storage.onChanged.addListener(listener); return () => browser.storage.onChanged.removeListener(listener); }, []); useEffect(() => { const listener = (message: unknown) => { const input = message as { action?: string; payload?: BridgeStatus }; if (input?.action === 'bridge.status.changed' && input.payload) setBridge(input.payload); }; browser.runtime.onMessage.addListener(listener); return () => browser.runtime.onMessage.removeListener(listener); }, []); useEffect(() => { void getAppearance().then((appearance) => setTheme(appearance.theme)); const listener = (changes: Record, area: string) => { if (area !== 'local' || !(APPEARANCE_STORAGE_KEY in changes)) return; const next = (changes[APPEARANCE_STORAGE_KEY] as { newValue?: { theme?: ThemePreference } })?.newValue; setTheme(next?.theme && ['system', 'light', 'dark'].includes(next.theme) ? next.theme : 'system'); }; browser.storage.onChanged.addListener(listener); return () => browser.storage.onChanged.removeListener(listener); }, []); useEffect(() => { const onHash = () => { const value = location.hash.slice(1) as Section; if (SECTIONS.some((item) => item.id === value)) setSection(value); }; window.addEventListener('hashchange', onHash); return () => window.removeEventListener('hashchange', onHash); }, []); const navigate = (next: Section) => { setSection(next); history.replaceState(null, '', `#${next}`); }; const selectTab = async (tabId: number) => { const next = await request('tab.get', { tabId }); setTab(next); const url = new URL(location.href); url.searchParams.set('tabId', String(tabId)); history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`); }; const run = async (task: () => Promise, success?: string) => { setBusy(true); setNotice(undefined); try { await task(); if (success) setNotice({ kind: 'ok', text: success }); } catch (error) { setNotice({ kind: 'error', text: errorMessage(error) }); } finally { setBusy(false); } }; if (!state) return
正在初始化 Yakit Browser Agent
; const handoff = waitingHandoff(state.handoff); return (
{tab?.favIconUrl ? : }
{state.activeGrant ? `${isControlScopeSet(state.activeGrant.scopes) ? '控制' : '只读'}会话` : '未共享'}
{handoff && }
{section === 'overview' && } {section === 'proxies' && } {section === 'rules' && } {section === 'cookies' && } {section === 'user-agent' && } {section === 'network' && } {section === 'context' && } {section === 'engine' && } {section === 'activity' && }
{notice &&
{notice.kind === 'ok' ? : }{notice.text}
}
); } function HandoffBanner({ handoff, setState, run, busy }: { handoff: HumanHandoff; setState: (state: ExtensionState) => void; run: (task: () => Promise, success?: string) => Promise; busy: boolean }) { const resolve = (outcome: 'completed' | 'cancelled') => run( async () => setState(await request('handoff.resolve', { id: handoff.id, outcome })), outcome === 'completed' ? '已通知 Agent 继续执行' : '人工接管已取消', ); return
{HANDOFF_REASON_LABELS[handoff.reason]} {handoff.message} {handoff.target.title} · {handoff.target.origin}
; } function ActivityLog({ run, busy }: { run: (task: () => Promise, success?: string) => Promise; busy: boolean }) { const [events, setEvents] = useState([]); const [runtime, setRuntime] = useState({ state: 'idle', updatedAt: Date.now(), actions: [] }); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(''); const loadEvents = useCallback(async () => { try { setLoadError(''); setEvents(await request('audit.list', { limit: 200 })); } catch (error) { setLoadError(errorMessage(error)); } finally { setLoading(false); } }, []); const loadRuntime = useCallback(() => request('agent.runtime.get').then(setRuntime), []); useEffect(() => { void Promise.all([loadEvents(), loadRuntime()]); const listener = (changes: Record) => { if (AUDIT_STORAGE_KEY in changes) void loadEvents(); if (AGENT_RUNTIME_STORAGE_KEY in changes) void loadRuntime(); }; browser.storage.onChanged.addListener(listener); return () => browser.storage.onChanged.removeListener(listener); }, [loadEvents, loadRuntime]); const runtimeLabel = { idle: '无活动任务', running: 'Agent 运行中', paused: '已暂停', waiting_for_human: '等待用户', revoked: '授权已撤销', expired: '授权已过期', }[runtime.state]; const downloadDiagnostics = () => run(async () => { const bundle = await request('diagnostics.export'); const url = URL.createObjectURL(new Blob([JSON.stringify(bundle, null, 2)], { type: 'application/json' })); const link = document.createElement('a'); link.href = url; link.download = `yakit-browser-agent-diagnostics-${new Date().toISOString().replaceAll(':', '-')}.json`; link.click(); URL.revokeObjectURL(url); }, '脱敏诊断包已导出'); return

Agent 操作时间线

实时动作保存在浏览器 session;长期审计只保存脱敏摘要,不记录参数、页面内容、Cookie 或执行结果。

{runtimeLabel}
当前任务{runtime.taskId || '未共享'}{runtime.grantId ? `Grant ${runtime.grantId.slice(0, 8)}` : '没有活动授权'}
最近更新{new Date(runtime.updatedAt).toLocaleTimeString()}{runtime.actions.length} 条 session 动作
{runtime.state === 'running' || runtime.state === 'waiting_for_human' ? : runtime.state === 'paused' ? : null}{runtime.grantId && !['revoked', 'expired'].includes(runtime.state) && }
{runtime.actions.length === 0 ?
当前 session 尚无 Agent 能力调用。
:
{[...runtime.actions].reverse().slice(0, 50).map((action) =>
{action.method}{action.targetTabId ? `Tab ${action.targetTabId}` : '扩展本机'}{action.state}{action.durationMs === undefined ? '进行中' : `${action.durationMs} ms`}
)}
}

持久化脱敏审计

最近 500 条授权、Bridge、接管与能力结果。

{loading ?
正在读取记录
: loadError ?
{loadError}
: events.length === 0 ? 还没有操作记录。 :
时间类型动作目标 / 摘要结果耗时
{events.map((event) =>
{AUDIT_CATEGORY_LABELS[event.category]} {event.action} {event.summary || (event.targetTabId ? `标签页 ${event.targetTabId}` : event.taskId ? `任务 ${event.taskId}` : '扩展本机')} {AUDIT_OUTCOME_LABELS[event.outcome]} {event.durationMs === undefined ? '—' : `${event.durationMs} ms`}
)}
}
; } function Overview({ state, bridge, tab, navigate, run, busy }: { state: ExtensionState; bridge: BridgeStatus; tab?: ActiveTabInfo; navigate: (value: Section) => void; run: (task: () => Promise, success?: string) => Promise; busy: boolean }) { const activeProxy = state.proxyProfiles.find((profile) => profile.id === state.activeProxyId)?.name || (state.activeProxyId === 'rules' ? '按规则分流' : '未知'); const [runtime, setRuntime] = useState({ state: 'idle', updatedAt: Date.now(), actions: [] }); const [network, setNetwork] = useState(); const [loginContext, setLoginContext] = useState(); useEffect(() => { void request('agent.runtime.get').then(setRuntime).catch(() => undefined); if (tab) void request('network.capture.status', { tabId: tab.id }).then(setNetwork).catch(() => setNetwork(undefined)); const listener = (changes: Record) => { if (AGENT_RUNTIME_STORAGE_KEY in changes) void request('agent.runtime.get').then(setRuntime).catch(() => undefined); }; browser.storage.onChanged.addListener(listener); return () => browser.storage.onChanged.removeListener(listener); }, [tab?.id]); const site = tab?.url ? new URL(tab.url) : undefined; const latestAction = [...runtime.actions].reverse()[0]; const captureLoginEnvironment = () => run(async () => { if (!tab) throw new Error('请先选择 HTTP(S) 标签页'); setLoginContext(await request('context.capture', { tabId: tab.id, includeDom: true, includeStorage: true, includeCookies: true, })); }, '登录环境已采集'); const startCapture = () => run(async () => { if (!tab) throw new Error('请先选择 HTTP(S) 标签页'); setNetwork(await request('network.capture.start', { tabId: tab.id, captureHeaders: false, captureBody: false })); navigate('network'); }, '网络元数据捕获已启动'); return

运行概览

{tab?.title || '选择一个 HTTP(S) 标签页,建立浏览器现场。'}

{bridge.state === 'connected' ? `Yak ${bridge.engineVersion || '引擎'} 在线` : 'Yak 引擎离线'}
{loginContext?.authentication.status === 'authenticated' ? '检测到登录环境' : loginContext?.authentication.status === 'unauthenticated' ? '未检测到登录态' : '登录环境待采集'}{site ? `${site.protocol.replace(':', '').toUpperCase()} · ${site.origin}` : '当前页面不可访问'}
浏览器现场{loginContext ? `${loginContext.document?.forms.length || 0} 表单 / ${loginContext.document?.interactive.length || 0} 节点` : '尚未采集'}{loginContext?.authentication.evidence[0] || 'Cookie、Storage 与认证信号仅在用户点击后读取'}
代理与流量{activeProxy}{network?.active ? `${network.count} 条请求,${network.droppedCount} 条丢弃` : `${state.proxyRules.filter((rule) => rule.enabled).length} 条分流规则 · 捕获未启动`}
Agent 会话{state.activeGrant ? `${isControlScopeSet(state.activeGrant.scopes) ? '控制' : '只读'} · ${runtime.state}` : '未共享'}{state.activeGrant ? `${state.activeGrant.targets.length} 个 frame · ${new Date(state.activeGrant.expiresAt).toLocaleTimeString()} 到期` : '创建 task-bound grant 后才允许远程读取'}
需要用户处理{state.handoff?.state === 'waiting_for_user' ? HANDOFF_REASON_LABELS[state.handoff.reason] : runtime.state === 'paused' ? 'Agent 已暂停' : '没有待办步骤'}{state.handoff?.state === 'waiting_for_user' ? state.handoff.message : latestAction ? `最近 ${latestAction.method} · ${latestAction.state}` : '二维码、MFA 与 CAPTCHA 会在这里出现'}
; } function ProxyProfiles({ state, setState, run, busy }: { state: ExtensionState; setState: (state: ExtensionState) => void; run: (task: () => Promise, success?: string) => Promise; busy: boolean }) { const empty: ProxyProfile = { id: '', name: '', kind: 'fixed_servers', scheme: 'http', host: '127.0.0.1', port: 8083, bypass: ['localhost', '127.0.0.1', ''] }; const [draft, setDraft] = useState(); const [authPassword, setAuthPassword] = useState(''); const [authConfigured, setAuthConfigured] = useState(false); const selectDraft = (profile: ProxyProfile) => { setDraft(profile); setAuthPassword(''); void request('proxy.auth.status', { profileId: profile.id }).then((result) => setAuthConfigured(result.configured)); }; const saveProfile = () => run(async () => { if (!draft) return; setState(await request('proxy.save', draft)); if (draft.authEnabled && authPassword) { const result = await request('proxy.auth.set', { profileId: draft.id, password: authPassword }); setAuthConfigured(result.configured); setAuthPassword(''); } else if (!draft.authEnabled) { await request('proxy.auth.set', { profileId: draft.id, password: '' }); setAuthConfigured(false); } }, '代理配置已保存'); return

代理配置

固定代理、SOCKS、PAC 与会话级认证出口。

{state.proxyProfiles.map((profile) => )}
{draft ? <>

{draft.builtin ? '内置代理' : '编辑代理'}

{draft.id}

setDraft({ ...draft, name: event.target.value })} /> {draft.kind === 'fixed_servers' && <> setDraft({ ...draft, host: event.target.value })} /> setDraft({ ...draft, port: Number(event.target.value) })} />