import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; import { browser, type Browser } from 'wxt/browser'; import { Activity, AlertTriangle, Bot, Braces, Check, ChevronRight, CircleGauge, CloudDownload, Cookie, Copy, Database, Download, Eye, FileKey2, Fingerprint, History, KeyRound, MousePointer2, Network, Play, Power, Radio, RefreshCw, Route, Save, Search, Send, Server, ShieldCheck, Square, Trash2, Upload, UserRoundCog, X, } from 'lucide-react'; 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 { cookieKey, cookieRemovalInput } from '@/features/cookies/presentation'; import { AutoSwitchView } from '@/features/proxy/ui/AutoSwitchView'; import { ProxyProfilesView } from '@/features/proxy/ui/ProxyProfilesView'; import { useProxyStatus } from '@/features/proxy/ui/ProxyStatusBar'; import { RuleSourcesView } from '@/features/proxy/ui/RuleSourcesView'; import { RecordingWorkspace } from '@/features/browser-recording/RecordingWorkspace'; import { AuthorizationTestingWorkspace } from '@/features/authorization-testing/ui/AuthorizationTestingWorkspace'; 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, UserAgentProfile, UserAgentProfileInput, 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' | 'authorization' | 'network' | 'gateway' | 'proxies' | 'rules' | 'sources' | 'cookies' | 'user-agent' | 'context' | 'engine' | 'activity'; const FIREFOX_AMO_BUILD = import.meta.env.FIREFOX && import.meta.env.MODE === 'store'; const NAVIGATION: Array<{ label?: string; items: Array<{ id: Section; label: string; icon: ReactNode }> }> = [ { items: [{ id: 'overview', label: '概览', icon: }], }, { label: '安全测试', items: [{ id: 'authorization', label: '越权测试', icon: }], }, { label: '请求与改写', items: [ { id: 'network', label: '请求捕获', icon: }, { id: 'gateway', label: '明文网关', icon: }, ], }, { label: '代理', items: [ { id: 'proxies', label: '代理设置', icon: }, { id: 'rules', label: '分流规则', icon: }, { id: 'sources', label: '规则订阅', icon: }, ], }, { label: '浏览器工具', items: [ { id: 'context', label: '页面上下文', icon: }, { id: 'cookies', label: 'Cookie 管理', icon: }, { id: 'user-agent', label: 'User-Agent', icon: }, ], }, { label: '系统', items: [ { id: 'engine', label: '引擎连接', icon: }, { id: 'activity', label: '操作记录', icon: }, ], }, ]; const SECTIONS = NAVIGATION.flatMap((group) => group.items); 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 (
{section === 'authorization' ?
授权测试A/B 页面在工作区内选择
:
{tab?.favIconUrl ? : }
}
{bridge.state === 'connected' ? '实例已连接' : '实例离线'}
{handoff && }
{section === 'overview' && } {section === 'authorization' && } {section === 'proxies' && } {section === 'rules' && } {section === 'sources' && } {section === 'cookies' && } {section === 'user-agent' && } {section === 'network' && } {section === 'gateway' && } {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 ? '已接入 Agent' : '等待调用'}{runtime.grantId ? '配对级访问' : '尚无能力调用'}
最近更新{new Date(runtime.updatedAt).toLocaleTimeString()}{runtime.actions.length} 条 session 动作
{runtime.state === 'running' || runtime.state === 'waiting_for_human' ? : runtime.state === 'paused' ? : null}
{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 = useProxyStatus(state).label; 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} 条手动规则 · ${state.proxyRuleSources.filter((source) => source.enabled).length} 个订阅源`}
Agent 连接{bridge.state === 'connected' ? `实例在线 · ${runtime.state}` : '实例离线'}{bridge.state === 'connected' ? '当前浏览器内的 HTTP(S) 页面可直接被引用' : '配对并连接 Yakit 后即可使用,无需逐页授权'}
需要用户处理{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 CookieEditor({ tab, run, busy }: { tab?: ActiveTabInfo; run: (task: () => Promise, success?: string) => Promise; busy: boolean }) { const [url, setUrl] = useState(tab?.url || ''); const [cookies, setCookies] = useState([]); const [query, setQuery] = useState(''); const [filter, setFilter] = useState<'all' | 'session' | 'persistent' | 'httpOnly' | 'partitioned'>('all'); const [sort, setSort] = useState<'name' | 'domain' | 'expires' | 'size'>('name'); const [group, setGroup] = useState<'none' | 'domain' | 'path'>('domain'); const [selected, setSelected] = useState>(new Set()); const [transferFormat, setTransferFormat] = useState('json'); const [includeExportValues, setIncludeExportValues] = useState(false); const [importText, setImportText] = useState(''); const [transferStatus, setTransferStatus] = useState(''); const [draft, setDraft] = useState>({ name: '', value: '', path: '/', secure: url.startsWith('https:'), httpOnly: false, sameSite: 'unspecified', }); const keyOf = cookieKey; const reload = () => run(async () => { if (!tab?.id) throw new Error('请选择目标标签页'); setCookies(await request('cookie.list', { url, tabId: tab.id })); setSelected(new Set()); }); const editCookie = (cookie: BrowserCookie) => { setDraft({ name: cookie.name, value: cookie.value, domain: cookie.hostOnly ? undefined : cookie.domain, path: cookie.path, secure: cookie.secure, httpOnly: cookie.httpOnly, sameSite: cookie.sameSite as CookieInput['sameSite'], expirationDate: cookie.expirationDate, storeId: cookie.storeId, firstPartyDomain: cookie.firstPartyDomain, partitionKey: cookie.partitionKey, }); }; const visibleCookies = cookies.filter((cookie) => { const needle = query.trim().toLowerCase(); const queryMatch = !needle || [cookie.name, cookie.domain, cookie.path].some((value) => value.toLowerCase().includes(needle)); const filterMatch = filter === 'all' || (filter === 'session' && cookie.session) || (filter === 'persistent' && !cookie.session) || (filter === 'httpOnly' && cookie.httpOnly) || (filter === 'partitioned' && Boolean(cookie.partitionKey)); return queryMatch && filterMatch; }).sort((left, right) => { if (sort === 'domain') return `${left.domain}${left.path}${left.name}`.localeCompare(`${right.domain}${right.path}${right.name}`); if (sort === 'expires') return (left.expirationDate || Number.MAX_SAFE_INTEGER) - (right.expirationDate || Number.MAX_SAFE_INTEGER); if (sort === 'size') return right.value.length - left.value.length; return left.name.localeCompare(right.name); }); const groupedCookies = new Map(); for (const cookie of visibleCookies) { const key = group === 'domain' ? cookie.domain : group === 'path' ? cookie.path : '全部 Cookie'; groupedCookies.set(key, [...(groupedCookies.get(key) || []), cookie]); } const removeInputs = (items: BrowserCookie[]) => items.map(cookieRemovalInput); const downloadExport = async () => { if (!tab?.id) throw new Error('请选择目标标签页'); const text = await request('cookie.export', { url, tabId: tab.id, format: transferFormat, includeValues: includeExportValues, }); const blobUrl = URL.createObjectURL(new Blob([text], { type: 'text/plain;charset=utf-8' })); const anchor = document.createElement('a'); anchor.href = blobUrl; anchor.download = `cookies-${new URL(url).hostname}.${transferFormat === 'json' ? 'json' : 'txt'}`; anchor.click(); URL.revokeObjectURL(blobUrl); }; useEffect(() => { if (url.startsWith('http')) void reload(); }, []); return

Cookie Editor

HttpOnly、Cookie Store、CHIPS 分区与多格式交换。

setUrl(event.target.value)} />{cookies.length} cookies
setQuery(event.target.value)} />
0 && visibleCookies.every((cookie) => selected.has(keyOf(cookie)))} onChange={(event) => setSelected(event.target.checked ? new Set(visibleCookies.map(keyOf)) : new Set())} />名称值Domain / Path属性
{visibleCookies.length === 0 ? 没有符合条件的 Cookie。 : [...groupedCookies].map(([groupName, items]) =>
{groupName}{items.length}
{items.map((cookie) => { const cookieKey = keyOf(cookie); return
setSelected((current) => { const next = new Set(current); if (event.target.checked) next.add(cookieKey); else next.delete(cookieKey); return next; })} />{cookie.value}{cookie.domain}{cookie.path}{cookie.httpOnly && HttpOnly}{cookie.secure && Secure}{cookie.partitionKey && Partitioned}{cookie.sameSite && {cookie.sameSite}}{cookie.priority && {cookie.priority}}{cookie.sameParty && SameParty}
; })}
)}

写入 Cookie

setDraft({ ...draft, name: event.target.value })} />