import { useCallback, useEffect, useState } from 'react'; import { AlertTriangle, ArrowRight, Check, CircleCheck, Fingerprint, Link2, Play, RefreshCw, RotateCcw, ShieldAlert, Square, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { errorMessage, request } from '@/platform/messaging/runtime'; import type { ActiveTabInfo, BridgeStatus, BrowserAuthorizationInstance, NetworkCaptureStatus, } from '@/types/models'; import { runBrowserAuthorizationTask, type BrowserAuthorizationPair, type BrowserAuthorizationPairInspection, type BrowserAuthorizationRequest, type BrowserAuthorizationResult, type BrowserAuthorizationSide, } from '../engine'; import { IdentitySlot } from './IdentitySlot'; import './authorization-testing-workspace.css'; interface AuthorizationTestingWorkspaceProps { bridge: BridgeStatus; run: (task: () => Promise, success?: string) => Promise; busy: boolean; } function origin(tab?: ActiveTabInfo): string { try { return tab ? new URL(tab.url).origin : ''; } catch { return ''; } } function requestLabel(item: BrowserAuthorizationRequest): string { try { const parsed = new URL(item.url); return `${item.method} ${parsed.pathname}${parsed.search}${item.statusCode ? ` · ${item.statusCode}` : ''}`; } catch { return `${item.method} ${item.url}`; } } function requestRoute(item: BrowserAuthorizationRequest): string { try { const parsed = new URL(item.url); const path = parsed.pathname.split('/').map((segment) => ( /^\d+$/.test(segment) || /^[0-9a-f-]{16,}$/i.test(segment) ? ':value' : segment )).join('/'); return `${item.method} ${parsed.origin}${path} ${[...parsed.searchParams.keys()].sort().join(',')}`; } catch { return `${item.method} ${item.url}`; } } function newestPair(left: BrowserAuthorizationRequest[], right: BrowserAuthorizationRequest[]) { for (const leftItem of left) { const rightItem = right.find((item) => requestRoute(item) === requestRoute(leftItem)); if (rightItem) return { left: leftItem, right: rightItem }; } return undefined; } function outcomeLabel(value: BrowserAuthorizationResult['cases'][number]['outcome']): string { if (value === 'success') return '成功'; if (value === 'denied') return '明确拒绝'; if (value === 'redirect') return '重定向'; if (value === 'client-error') return '客户端错误'; if (value === 'server-error') return '服务端错误'; return '响应不可判定'; } function verdictTone(value: BrowserAuthorizationResult['verdict']): 'warning' | 'success' | 'muted' { if (value === 'protected') return 'success'; if (value === 'suspected' || value === 'possible') return 'warning'; return 'muted'; } export function AuthorizationTestingWorkspace({ bridge, run, busy }: AuthorizationTestingWorkspaceProps) { const [instances, setInstances] = useState([]); const [leftDeviceId, setLeftDeviceId] = useState(''); const [rightDeviceId, setRightDeviceId] = useState(''); const [leftTabId, setLeftTabId] = useState(); const [rightTabId, setRightTabId] = useState(); const [leftLabel, setLeftLabel] = useState('账号 A'); const [rightLabel, setRightLabel] = useState('账号 B'); const [capture, setCapture] = useState>>({}); const [requests, setRequests] = useState>({ left: [], right: [] }); const [selected, setSelected] = useState>({ left: '', right: '' }); const [inspection, setInspection] = useState(); const [selectorId, setSelectorId] = useState(''); const [result, setResult] = useState(); const [localError, setLocalError] = useState(''); const leftInstance = instances.find((item) => item.deviceId === leftDeviceId); const rightInstance = instances.find((item) => item.deviceId === rightDeviceId); const leftTab = leftInstance?.tabs.find((item) => item.id === leftTabId); const rightTab = rightInstance?.tabs.find((item) => item.id === rightTabId); const sameOrigin = Boolean(leftTab && rightTab && origin(leftTab) === origin(rightTab)); const capabilityReady = bridge.state === 'connected' && Boolean(bridge.capabilities?.includes('yakit.browser_authorization.task')); const discoveryReady = bridge.state === 'connected' && Boolean(bridge.capabilities?.includes('yakit.browser_authorization.instances')); const pairReady = Boolean(leftInstance && rightInstance && leftTab && rightTab && leftDeviceId !== rightDeviceId && sameOrigin && capabilityReady); const capturing = Boolean(capture.left?.active && capture.right?.active); const pair = useCallback((): BrowserAuthorizationPair => { if (!pairReady || !leftTabId || !rightTabId) throw new Error('请选择两个在线 YTray 浏览器中的同站点页面'); return { left: { deviceId: leftDeviceId, tabId: leftTabId }, right: { deviceId: rightDeviceId, tabId: rightTabId }, }; }, [leftDeviceId, leftTabId, pairReady, rightDeviceId, rightTabId]); const refreshInstances = useCallback(async () => { if (!discoveryReady) { setInstances([]); return; } const response = await request('authorization.yakit.instances'); const next = Array.isArray(response.instances) ? response.instances : []; setInstances(next); const left = next.find((item) => item.current); const right = next.find((item) => !item.current); setLeftDeviceId((current) => next.some((item) => item.deviceId === current) ? current : left?.deviceId || ''); setRightDeviceId((current) => next.some((item) => item.deviceId === current && !item.current) ? current : right?.deviceId || ''); }, [discoveryReady]); useEffect(() => { void refreshInstances().catch((error) => setLocalError(errorMessage(error))); const visible = () => document.visibilityState === 'visible' && void refreshInstances().catch((error) => setLocalError(errorMessage(error))); document.addEventListener('visibilitychange', visible); return () => document.removeEventListener('visibilitychange', visible); }, [refreshInstances]); useEffect(() => { setLeftTabId((current) => leftInstance?.tabs.some((tab) => tab.id === current) ? current : leftInstance?.tabs.find((tab) => tab.active)?.id || leftInstance?.tabs[0]?.id); }, [leftInstance]); useEffect(() => { setRightTabId((current) => rightInstance?.tabs.some((tab) => tab.id === current) ? current : rightInstance?.tabs.find((tab) => tab.active)?.id || rightInstance?.tabs[0]?.id); }, [rightInstance]); const task = (schema: Parameters[0], extra: Record = {}, timeoutMs?: number) => ( runBrowserAuthorizationTask(schema, { ...pair(), ...extra }, timeoutMs) ); const startCapture = () => run(async () => { setLocalError(''); const left = await task('authorization.capture.start', { side: 'left' }); try { const right = await task('authorization.capture.start', { side: 'right' }); setCapture({ left, right }); setRequests({ left: [], right: [] }); setInspection(undefined); setResult(undefined); } catch (error) { await task('authorization.capture.stop', { side: 'left' }).catch(() => undefined); throw error; } }, 'A/B 请求捕获已开始'); const stopCapture = (side: BrowserAuthorizationSide) => run(async () => { const status = await task('authorization.capture.stop', { side }); setCapture((current) => ({ ...current, [side]: status })); }, `${side === 'left' ? leftLabel : rightLabel} 的捕获已停止`); const inspect = async (leftRequestId: string, rightRequestId: string) => { const next = await task('authorization.pair.inspect', { leftRequestId, rightRequestId, }); setInspection(next); setSelectorId((current) => next.selectors.some((item) => item.id === current) ? current : next.selectors[0]?.id || ''); setResult(undefined); }; const readRequests = () => run(async () => { setLocalError(''); const [left, right] = await Promise.all([ task('authorization.requests', { side: 'left', limit: 50 }), task('authorization.requests', { side: 'right', limit: 50 }), ]); setRequests({ left, right }); const found = newestPair(left, right); if (!found) throw new Error('没有找到 A/B 最近一次同类请求,请分别执行相同业务动作后重试'); setSelected({ left: found.left.id, right: found.right.id }); await inspect(found.left.id, found.right.id); }, '已找到 A/B 最近一次同类请求'); const inspectSelected = () => run(async () => { if (!selected.left || !selected.right) throw new Error('请为 A/B 各选择一条请求'); await inspect(selected.left, selected.right); }, '已重新分析可交换资源字段'); const execute = () => run(async () => { if (!selected.left || !selected.right || !selectorId || !inspection) throw new Error('请先读取并确认请求与资源字段'); const approved = window.confirm( `${inspection.method} ${inspection.route} 将发送最多 4 个真实请求` + `${inspection.sideEffect ? ',该方法可能改变业务状态' : ''}。仅对你有权测试的目标继续。`, ); if (!approved) return; const next = await task('authorization.execute', { leftRequestId: selected.left, rightRequestId: selected.right, selectorId, approveSideEffect: inspection.sideEffect, }, 120_000); setResult(next); }, 'A/B 交叉测试完成'); const reset = () => run(async () => { await Promise.allSettled((['left', 'right'] as const).map(async (side) => ( task('authorization.capture.stop', { side }) ))); setCapture({}); setRequests({ left: [], right: [] }); setSelected({ left: '', right: '' }); setInspection(undefined); setSelectorId(''); setResult(undefined); setLocalError(''); }); const assignRightInstance = (deviceId: string) => { const instance = instances.find((item) => item.deviceId === deviceId); setRightDeviceId(deviceId); setRightTabId(instance?.tabs.find((tab) => tab.active)?.id || instance?.tabs[0]?.id); }; return
本地确定性测试

越权测试

在两个 YTray 浏览器中执行同一业务动作,插件捕获请求,Yak 只交换资源字段并重放 A/B 对照。

{capabilityReady ? '引擎可用' : '引擎能力不可用'}
{localError &&
{localError}
}
{[ ['1', 'A/B 页面', capturing], ['2', '同类请求', Boolean(inspection)], ['3', '交叉结果', Boolean(result)], ].map(([index, label, complete], position) =>
{complete ? : index}{label} {position < 2 && }
)}
{leftTab ? : '1'}A 登录资源账号 {rightTab ? : '2'}B 登录对照账号 {pairReady ? : '3'}开始捕获
{leftInstance && rightInstance ? '独立 YTray 实例' : '等待 A/B 实例'} {sameOrigin ? '当前页面属于同一站点' : 'A/B 请选择同一站点页面'}
不调用 AI,不保存身份工作区登录跳转后直接读取当前页面;请求凭据只在本机重放。
{pairReady ? 'A/B 已就绪' : discoveryReady ? '请完成 A/B 页面选择' : '请更新并连接 Yak 引擎'}
{capturing &&
STEP 02

分别执行一次相同业务动作

完成后读取双方最近请求。自动匹配失败时,可在下方手工选择。

{(['left', 'right'] as const).map((side) =>
{side === 'left' ? leftInstance?.badge || 'A' : rightInstance?.badge || 'B'}
{side === 'left' ? leftLabel : rightLabel}{side === 'left' ? leftTab?.title : rightTab?.title}
{capture[side]?.count || 0} 条
)}
{inspection ? `${inspection.method} ${inspection.route}` : 'A/B 请求应当属于同一个接口与业务动作'}
} {inspection &&
STEP 03

选择要交换的资源字段

只交换一个非认证字段;Cookie、Token、CSRF、签名字段不会作为候选。

{inspection.selectors.map((selector) => )}
最多 4 个真实请求 A 正常 → B 正常 → A 访问 B → B 访问 A {inspection.sideEffect ? '当前方法可能改变业务状态,执行前会再次确认。' : '当前方法为只读请求。'}
{inspection.limitations.length > 0 &&
请求限制

{inspection.limitations.join(';')}

}
} {result &&
{result.verdict === 'protected' ? : }{result.summary}确定性结果 · {result.selector.label}
{result.cases.map((item, index) =>
{String(index + 1).padStart(2, '0')}
{item.label}{item.status} {item.statusText} · {item.durationMs.toFixed(0)} ms · {item.bodyBytes} B
{item.matchesTarget ? '匹配目标响应' : outcomeLabel(item.outcome)}
)}
}
; }