diff --git a/src/app/background/index.ts b/src/app/background/index.ts
index 0b4fe5d..9f6dd27 100644
--- a/src/app/background/index.ts
+++ b/src/app/background/index.ts
@@ -167,7 +167,7 @@ async function handleRequest(request: ExtensionRequest, sender: Browser.runtime.
case 'authorization.yakit.open':
return ok(await engineBridge.requestEngine(
'yakit.browser_authorization.open',
- { workspaceId: request.payload.workspaceId },
+ request.payload,
));
case 'context.capture': {
const { tabId, frameId, documentId, ...options } = request.payload;
diff --git a/src/entrypoints/options/App.css b/src/entrypoints/options/App.css
index 44eca0d..bad30c2 100644
--- a/src/entrypoints/options/App.css
+++ b/src/entrypoints/options/App.css
@@ -75,6 +75,7 @@ input[type='checkbox'] { width: 15px; height: 15px; flex: 0 0 auto; padding: 0;
.sidebar-brand .product-brand { width: 100%; color: var(--foreground); }
.sidebar nav { min-height: 0; padding: 10px 10px 16px; overflow-y: auto; display: grid; gap: 10px; scrollbar-width: thin; }
.sidebar-group { display: grid; gap: 2px; }
+.sidebar-group.is-primary { padding-bottom: 8px; border-bottom: 1px solid var(--border); }
.sidebar-group__label { min-height: 24px; padding: 0 10px; display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: var(--text-xs); font-weight: 650; letter-spacing: .04em; }
.sidebar-group__label svg { color: var(--primary); }
.sidebar nav button { width: 100%; height: 40px; padding: 0 10px; display: grid; grid-template-columns: 20px 1fr 14px; align-items: center; gap: 8px; border: 0; border-radius: var(--radius-md); background: transparent; color: var(--muted-strong); font-size: var(--text-md); font-weight: 500; text-align: left; cursor: pointer; transition: background-color .14s ease, color .14s ease; }
diff --git a/src/entrypoints/options/App.tsx b/src/entrypoints/options/App.tsx
index a440525..24619fb 100644
--- a/src/entrypoints/options/App.tsx
+++ b/src/entrypoints/options/App.tsx
@@ -2,8 +2,8 @@ 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, Fingerprint, History, KeyRound, MousePointer2, Network, Play, Power, Radio,
- RefreshCw, Route, Save, Search, Send, Server, ShieldCheck, Square, Trash2, Upload, UserRoundCog, Wrench, X,
+ 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';
@@ -30,37 +30,43 @@ 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' | 'proxies' | 'rules' | 'sources' | 'cookies' | 'user-agent' | 'network' | 'context' | 'engine' | 'activity';
+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; icon?: ReactNode; items: Array<{ id: Section; label: string; icon: ReactNode }> }> = [
+const NAVIGATION: Array<{ label?: string; items: Array<{ id: Section; label: string; icon: ReactNode }> }> = [
{
- label: '工作区',
+ items: [{ id: 'overview', label: '概览', icon: }],
+ },
+ {
+ label: '安全测试',
+ items: [{ id: 'authorization', label: '越权测试', icon: }],
+ },
+ {
+ label: '请求与改写',
items: [
- { id: 'overview', label: '运行概览', icon: },
- { id: 'authorization', label: '授权测试', icon: },
+ { id: 'network', label: '请求捕获', icon: },
+ { id: 'gateway', label: '明文网关', icon: },
],
},
{
- label: '网络与流量',
+ label: '代理',
items: [
- { id: 'proxies', label: '代理出口', icon: },
- { id: 'rules', label: '自动切换', icon: },
+ { id: 'proxies', label: '代理设置', icon: },
+ { id: 'rules', label: '分流规则', icon: },
{ id: 'sources', label: '规则订阅', icon: },
- { id: 'network', label: '网络活动', icon: },
],
},
{
- label: '常用工具', icon: ,
+ label: '浏览器工具',
items: [
- { id: 'cookies', label: 'Cookie Editor', icon: },
- { id: 'user-agent', label: 'UA 快速切换', icon: },
+ { id: 'context', label: '页面上下文', icon: },
+ { id: 'cookies', label: 'Cookie 管理', icon: },
+ { id: 'user-agent', label: 'User-Agent', icon: },
],
},
{
- label: 'Agent 与系统',
+ label: '系统',
items: [
- { id: 'context', label: '登录态工作区', icon: },
{ id: 'engine', label: '引擎连接', icon: },
{ id: 'activity', label: '操作记录', icon: },
],
@@ -204,7 +210,7 @@ function App() {
;
+}
+
+function GatewayWorkspace({
+ tab,
+ bridge,
+ run,
+ busy,
+}: {
+ tab?: ActiveTabInfo;
+ bridge: BridgeStatus;
+ run: (task: () => Promise, success?: string) => Promise;
+ busy: boolean;
+}) {
+ const shareTransform = async () => {
+ if (!tab) throw new Error('请先选择需要使用的页面');
+ if (bridge.state !== 'connected') await request('bridge.connect');
+ };
+
+ return
;
}
diff --git a/src/features/authorization-testing/ui/AuthorizationTestingWorkspace.tsx b/src/features/authorization-testing/ui/AuthorizationTestingWorkspace.tsx
index a884422..755ef61 100644
--- a/src/features/authorization-testing/ui/AuthorizationTestingWorkspace.tsx
+++ b/src/features/authorization-testing/ui/AuthorizationTestingWorkspace.tsx
@@ -445,6 +445,14 @@ export function AuthorizationTestingWorkspace({
}
}, 'A/B 身份已验证,双方请求捕获已开始');
+ const openCrossBrowserWorkspace = () => run(async () => {
+ if (!activeTab) throw new Error('请先选择需要测试的页面');
+ if (!bridge.capabilities?.includes('yakit.browser_authorization.open')) {
+ throw new Error('当前 Yak 引擎不支持打开跨浏览器工作区');
+ }
+ await request('authorization.yakit.open', { tabId: activeTab.id, mode });
+ }, '已在 Yakit 打开跨浏览器越权测试,并带入当前页面');
+
const refreshWorkspaceDocuments = async (): Promise => {
if (!workspace || !leftTab || !rightTab) throw new Error('请先建立 A/B 工作区');
const nextState = await request('grant.refresh');
@@ -722,6 +730,21 @@ export function AuthorizationTestingWorkspace({
)}
+ {!workspace &&
+
+
+ 使用 YTray 的多个浏览器实例
+ 把当前页面作为资源身份带入 Yakit,再选择在线的浏览器 A、B 或 C 作为独立对照账号。
+
+
+ }
+
{!workspace ?
测试类型
diff --git a/src/features/authorization-testing/ui/authorization-testing-workspace.css b/src/features/authorization-testing/ui/authorization-testing-workspace.css
index a4c4c60..0eb08aa 100644
--- a/src/features/authorization-testing/ui/authorization-testing-workspace.css
+++ b/src/features/authorization-testing/ui/authorization-testing-workspace.css
@@ -116,6 +116,44 @@
color: var(--success);
}
+.authorization-cross-browser {
+ min-height: 64px;
+ padding: 10px 14px;
+ display: grid;
+ grid-template-columns: 34px minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 11px;
+ border: 1px solid color-mix(in srgb, var(--authorization-accent) 24%, var(--border));
+ border-radius: var(--radius-lg);
+ background: color-mix(in srgb, var(--authorization-accent) 5%, var(--surface));
+}
+
+.authorization-cross-browser > span {
+ width: 34px;
+ height: 34px;
+ display: grid;
+ place-items: center;
+ border-radius: var(--radius-md);
+ background: color-mix(in srgb, var(--authorization-accent) 12%, var(--surface));
+ color: var(--authorization-accent);
+}
+
+.authorization-cross-browser strong,
+.authorization-cross-browser small {
+ display: block;
+}
+
+.authorization-cross-browser strong {
+ font-size: var(--text-sm);
+}
+
+.authorization-cross-browser small {
+ margin-top: 3px;
+ color: var(--muted);
+ font-size: var(--text-xs);
+ line-height: 1.45;
+}
+
.authorization-identity-stage,
.authorization-baseline-stage,
.authorization-plan-stage {
@@ -1458,6 +1496,15 @@
}
@media (max-width: 820px) {
+ .authorization-cross-browser {
+ grid-template-columns: 34px minmax(0, 1fr);
+ }
+
+ .authorization-cross-browser > .ui-button {
+ grid-column: 2;
+ justify-self: start;
+ }
+
.authorization-heading,
.authorization-prepare-bar,
.authorization-section-heading {
diff --git a/src/features/browser-recording/RecordingWorkspace.tsx b/src/features/browser-recording/RecordingWorkspace.tsx
index 82c6223..742e4e7 100644
--- a/src/features/browser-recording/RecordingWorkspace.tsx
+++ b/src/features/browser-recording/RecordingWorkspace.tsx
@@ -30,6 +30,7 @@ interface RecordingWorkspaceProps {
run: RunTask;
gatewayShared: boolean;
onShareGateway: () => Promise
;
+ initialMode?: 'gateway' | 'recording' | 'deep';
}
const KIND_LABELS: Record = {
@@ -182,8 +183,9 @@ export function RecordingWorkspace({
run,
gatewayShared,
onShareGateway,
+ initialMode = 'recording',
}: RecordingWorkspaceProps) {
- const [workspaceMode, setWorkspaceMode] = useState<'gateway' | 'recording' | 'deep'>('recording');
+ const [workspaceMode, setWorkspaceMode] = useState<'gateway' | 'recording' | 'deep'>(initialMode);
const [autoArmRequest, setAutoArmRequest] = useState(0);
const [autoRecoveryRequest, setAutoRecoveryRequest] = useState(0);
const [recoveryProfileId, setRecoveryProfileId] = useState('');
diff --git a/src/features/browser-transform/TransformDataFlowView.tsx b/src/features/browser-transform/TransformDataFlowView.tsx
index 1ab9dd2..3ac1be4 100644
--- a/src/features/browser-transform/TransformDataFlowView.tsx
+++ b/src/features/browser-transform/TransformDataFlowView.tsx
@@ -1,8 +1,9 @@
-import type { ComponentType } from 'react';
+import { useEffect, useMemo, useState, type ComponentType } from 'react';
import {
ArrowDownToLine,
Braces,
CheckCircle2,
+ ChevronDown,
Code2,
FileInput,
KeyRound,
@@ -20,6 +21,12 @@ import type {
BrowserTransformValueSummary,
} from '@/types/models';
+interface FlowStageItem {
+ id: string;
+ stage: BrowserTransformExplanationStage;
+ members: BrowserTransformExplanationStage[];
+}
+
const OWNER_LABELS: Record = {
webfuzzer: 'Web Fuzzer',
extension: '浏览器扩展',
@@ -69,6 +76,44 @@ function operationLabel(operation: BrowserTransformExplanationStage['operations'
return details.join(' · ');
}
+function displayStages(
+ stages: BrowserTransformExplanationStage[],
+ direction: BrowserTransformDirectionName,
+): FlowStageItem[] {
+ const items: FlowStageItem[] = [];
+ for (const stage of stages) {
+ const assembly = stage.owner === 'extension' && (stage.kind === 'builtin' || stage.kind === 'output');
+ const previous = items[items.length - 1];
+ if (assembly && previous?.members.every((item) => (
+ item.owner === 'extension' && (item.kind === 'builtin' || item.kind === 'output')
+ ))) {
+ previous.members.push(stage);
+ continue;
+ }
+ items.push({ id: stage.id, stage, members: [stage] });
+ }
+ return items.map((item) => {
+ if (item.members.length === 1) return item;
+ const first = item.members[0];
+ const last = item.members[item.members.length - 1];
+ return {
+ ...item,
+ id: `${first.id}:assembly`,
+ stage: {
+ ...first,
+ id: `${first.id}:assembly`,
+ title: direction === 'request' ? '浏览器扩展组装线上请求' : '浏览器扩展还原逻辑响应',
+ summary: `${item.members.length} 个受限步骤,将中间结果写入最终报文`,
+ nodeIds: item.members.flatMap((member) => member.nodeIds),
+ inputPaths: first.inputPaths,
+ outputPaths: last.outputPaths,
+ operations: item.members.flatMap((member) => member.operations),
+ evidence: item.members.flatMap((member) => member.evidence),
+ },
+ };
+ });
+}
+
export function TransformDataFlowView({
profile,
direction,
@@ -85,6 +130,13 @@ export function TransformDataFlowView({
const explained = profile.explanation?.directions.find((item) => item.direction === direction);
const currentExecution = execution?.direction === direction ? execution : undefined;
const availableDirections = profile.explanation?.directions.map((item) => item.direction) || [];
+ const stages = useMemo(() => displayStages(explained?.stages || [], direction), [direction, explained?.stages]);
+ const defaultOpenStageId = stages.find((item) => item.members.some((member) => member.kind === 'page-call'))?.id || '';
+ const [openStageId, setOpenStageId] = useState('');
+
+ useEffect(() => {
+ setOpenStageId(defaultOpenStageId);
+ }, [defaultOpenStageId, direction, profile.id]);
if (!explained) return
@@ -97,7 +149,9 @@ export function TransformDataFlowView({
明文网关 · {direction === 'request' ? '请求方向' : '响应方向'}
{direction === 'request' ? '明文如何成为线上请求' : '线上响应如何还原为明文'}
-
{explained.summary}
+
{stages.length === explained.stages.length
+ ? explained.summary
+ : `${explained.stages.length} 个处理步骤已收拢为 ${stages.length} 个主要阶段`}
{availableDirections.length > 1 &&
{availableDirections.map((item) =>