From e00746d834314ac4d13d583014bf722f67ce0e22 Mon Sep 17 00:00:00 2001 From: go0p Date: Thu, 3 Sep 2026 13:31:59 +0800 Subject: [PATCH] feat(browser): add managed instance agent capabilities --- scripts/audit-build.mjs | 8 ++ src/app/background/index.ts | 68 ++++++++++-- src/entrypoints/options/App.tsx | 53 ++------- src/entrypoints/ytray-bootstrap/index.html | 11 ++ src/entrypoints/ytray-bootstrap/main.ts | 46 ++++++++ .../browser-recording/RecordingWorkspace.tsx | 6 -- .../BrowserTransformWorkspace.tsx | 8 +- .../TransformReplayPanel.tsx | 24 ++--- src/features/engine-bridge/identity.test.ts | 23 ++++ src/features/engine-bridge/identity.ts | 21 +++- src/features/engine-bridge/service.ts | 24 ++--- src/features/floating-panel/FloatingPanel.tsx | 11 +- src/features/grants/capability-context.ts | 67 ++++++------ src/features/grants/capability-domains.ts | 3 + .../grants/capability-handlers/handoff.ts | 23 ++-- .../grants/capability-handlers/navigation.ts | 101 +++++++++++------- .../grants/capability-handlers/page.ts | 7 +- .../grants/capability-handlers/transform.ts | 9 +- src/features/grants/lifecycle.test.ts | 48 ++++----- src/features/grants/lifecycle.ts | 49 ++------- src/features/grants/service.test.ts | 37 +++++++ src/features/grants/service.ts | 13 ++- src/platform/browser/isolation.ts | 3 + src/platform/browser/targets.test.ts | 53 +++++++++ src/platform/browser/targets.ts | 17 ++- src/platform/policy/managed.test.ts | 13 ++- src/platform/policy/managed.ts | 16 ++- src/platform/storage/state.test.ts | 18 ++++ src/platform/storage/state.ts | 23 +++- src/protocol/bridge.test.ts | 6 ++ src/protocol/bridge.ts | 15 +++ src/protocol/capabilities.ts | 26 ++++- src/protocol/extension.test.ts | 11 ++ src/protocol/extension.ts | 10 ++ src/types/messages.ts | 5 + src/types/models.ts | 10 +- wxt.config.ts | 3 +- 37 files changed, 608 insertions(+), 281 deletions(-) create mode 100644 src/entrypoints/ytray-bootstrap/index.html create mode 100644 src/entrypoints/ytray-bootstrap/main.ts create mode 100644 src/features/grants/service.test.ts create mode 100644 src/platform/browser/targets.test.ts diff --git a/scripts/audit-build.mjs b/scripts/audit-build.mjs index 214ad40..8231e1c 100644 --- a/scripts/audit-build.mjs +++ b/scripts/audit-build.mjs @@ -1,4 +1,5 @@ import { access, readFile, stat } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; import { join, resolve } from 'node:path'; import { gzipSync } from 'node:zlib'; @@ -11,6 +12,7 @@ const TOTAL_PACKAGE_BUDGET = Math.floor(1.25 * MIB); const BRIDGE_BACKGROUND_BUDGET = 204 * 1024; const BRIDGE_BACKGROUND_GZIP_BUDGET = 60 * 1024; const ENTERPRISE_BACKGROUND_GZIP_BUDGET = 61 * 1024; +const CHROMIUM_EXTENSION_ID = 'mcnaombmlombekhbonfndagbcfhmoail'; // Recorder, callable registry and Pipeline runtime are installed only for an // explicitly selected document. Keep their budget separate from the always-on // Service Worker so moving work out of startup code remains measurable. @@ -103,6 +105,12 @@ for (const target of targets) { const resources = (manifest.web_accessible_resources || []).flatMap((entry) => typeof entry === 'string' ? [entry] : entry.resources || []); const dynamicResourceGroup = (manifest.web_accessible_resources || []).find((entry) => typeof entry !== 'string' && entry.resources?.includes('floating.html')); + if (!isFirefox) { + assert(typeof manifest.key === 'string', `${target.name} 缺少固定扩展公钥`); + const extensionId = createHash('sha256').update(Buffer.from(manifest.key, 'base64')).digest('hex').slice(0, 32).replace(/[0-9a-f]/g, (digit) => String.fromCharCode(97 + Number.parseInt(digit, 16))); + assert(extensionId === CHROMIUM_EXTENSION_ID, `${target.name} 扩展 ID 漂移:${extensionId}`); + } + if (contentBytes > target.contentBudget) sizeAdvisories.push(`content script ${contentBytes}B > ${target.contentBudget}B reference`); if (backgroundBytes > target.backgroundBudget) sizeAdvisories.push(`background ${backgroundBytes}B > ${target.backgroundBudget}B reference`); if (backgroundGzipBytes > target.backgroundGzipBudget) sizeAdvisories.push(`background gzip ${backgroundGzipBytes}B > ${target.backgroundGzipBudget}B reference`); diff --git a/src/app/background/index.ts b/src/app/background/index.ts index c3fbbdd..9ec8876 100644 --- a/src/app/background/index.ts +++ b/src/app/background/index.ts @@ -1,11 +1,11 @@ import { browser, type Browser } from 'wxt/browser'; import { clearNetworkRequests, exportNetworkRequest, listNetworkRequests, networkCaptureStatus, - rebindNetworkCapturesForGrant, startNetworkCapture, stopNetworkCapture, + rebindNetworkCapturesForGrant, startNetworkCapture, stopNetworkCapture, stopNetworkCapturesForGrant, } from '@/features/network-capture/service'; import { capturedRequestEnginePayload } from '@/features/network-capture/workflows'; -import { initializeBrowserRecordingService } from '@/features/browser-recording/service'; -import { initializeDeepCaptureService } from '@/features/deep-capture/service'; +import { initializeBrowserRecordingService, stopBrowserRecordingsForGrant } from '@/features/browser-recording/service'; +import { initializeDeepCaptureService, stopDeepCapturesForGrant } from '@/features/deep-capture/service'; import { initializeBrowserTransformService } from '@/features/browser-transform/service'; import { initializeFloatingPanelLifecycle } from '@/features/floating-panel/lifecycle'; import type { ExtensionRequest, ExtensionResponse } from '@/types/messages'; @@ -36,6 +36,9 @@ import { import { applyPolicyToBridge, applyPolicyToState, assertGrantPolicy, getEnterprisePolicy, } from '@/platform/policy/managed'; +import { + browserInstanceAccess, PAIRED_BROWSER_INSTANCE_ACCESS_ID, +} from '@/features/grants/capability-context'; import { createDiagnosticsBundle } from '@/features/diagnostics/export'; import { getRuntimeMetrics, recordServiceWorkerStart, resetRuntimeMetrics } from '@/features/diagnostics/metrics'; import { @@ -68,6 +71,16 @@ function originOf(url: string): string { return parsed.origin; } +async function syncManagedInstanceBadge(managedInstance?: { badge: string }): Promise { + const badge = managedInstance?.badge || ''; + await browser.action.setBadgeText({ text: badge }); + if (badge) { + const color = badge === 'A' ? '#F26215' : badge === 'B' ? '#2563EB' : badge === 'C' ? '#16A34A' : '#7C3AED'; + await browser.action.setBadgeBackgroundColor({ color }); + } + await browser.action.setTitle({ title: badge ? `Yakit Browser Agent · 实例 ${badge}` : 'Yakit Browser Agent' }); +} + async function createGrantTargets(inputs: Array<{ tabId: number; frameId: number }>): Promise { const unique = [...new Map(inputs.map((target) => [`${target.tabId}:${target.frameId}`, target])).values()]; const tabIds = [...new Set(unique.map((target) => target.tabId))]; @@ -106,6 +119,12 @@ const domainHandlers: readonly BackgroundRequestHandler[] = [ handleTransformRequest, ]; +const stopPairedBrowserTasks = () => Promise.all([ + stopNetworkCapturesForGrant(PAIRED_BROWSER_INSTANCE_ACCESS_ID), + stopBrowserRecordingsForGrant(PAIRED_BROWSER_INSTANCE_ACCESS_ID), + stopDeepCapturesForGrant(PAIRED_BROWSER_INSTANCE_ACCESS_ID), +]); + async function handleRequest(request: ExtensionRequest, sender: Browser.runtime.MessageSender): Promise { const domainResponse = await dispatchBackgroundHandlers(request, sender, domainHandlers); if (domainResponse !== undefined) return domainResponse; @@ -290,7 +309,10 @@ async function handleRequest(request: ExtensionRequest, sender: Browser.runtime. }; }); const handoff = state.handoff!; - await setAgentRuntimeState(input.outcome === 'completed' ? 'running' : 'paused', state.activeGrant); + await setAgentRuntimeState( + input.outcome === 'completed' ? 'running' : 'paused', + await browserInstanceAccess('browser.tabs.read'), + ); await browser.action.setBadgeText({ text: '', tabId: handoff.target.tabId }); engineBridge.emitEvent('browser.handoff.changed', handoff); void appendAuditEvent({ @@ -388,14 +410,15 @@ async function handleRequest(request: ExtensionRequest, sender: Browser.runtime. } case 'agent.runtime.get': return ok(await getAgentRuntime()); case 'agent.pause': { - const grant = await requireActiveGrant(); + const grant = await browserInstanceAccess('browser.tabs.read'); engineBridge.cancelActiveRequests(); + await stopPairedBrowserTasks(); const runtime = await setAgentRuntimeState('paused', grant); void appendAuditEvent({ category: 'grant', action: 'agent.pause', outcome: 'success', taskId: grant.taskId }); return ok(runtime); } case 'agent.resume': { - const grant = await requireActiveGrant(); + const grant = await browserInstanceAccess('browser.tabs.read'); const runtime = await setAgentRuntimeState('running', grant); void appendAuditEvent({ category: 'grant', action: 'agent.resume', outcome: 'success', taskId: grant.taskId }); return ok(runtime); @@ -408,10 +431,29 @@ async function handleRequest(request: ExtensionRequest, sender: Browser.runtime. case 'bridge.config.save': { const config = applyPolicyToBridge(request.payload, (await getEnterprisePolicy()).policy); const state = await updateState((current) => ({ ...current, bridge: config })); + await syncManagedInstanceBadge(state.bridge.managedInstance); if (config.autoConnect && config.pairedEngine) await engineBridge.connect(config); else engineBridge.disconnect(); return ok(state); } + case 'bridge.managed-instance.bind': { + const senderURL = sender.url ? new URL(sender.url) : undefined; + const bootstrapURL = new URL(browser.runtime.getURL('/ytray-bootstrap.html')); + if (senderURL?.origin !== bootstrapURL.origin || senderURL.pathname !== bootstrapURL.pathname) { + throw new ExtensionError('forbidden', '浏览器实例身份只能由受管启动页设置'); + } + const state = await updateState((current) => ({ + ...current, + bridge: { ...current.bridge, managedInstance: request.payload }, + })); + await syncManagedInstanceBadge(state.bridge.managedInstance); + if (state.bridge.autoConnect && state.bridge.pairedEngine) { + engineBridge.disconnect(); + await stopPairedBrowserTasks(); + await engineBridge.connect(state.bridge); + } + return ok(engineBridge.getStatus()); + } case 'bridge.pair': { const status = await engineBridge.startPairing(); void appendAuditEvent({ category: 'bridge', action: 'bridge.pair', outcome: 'success' }); @@ -421,6 +463,7 @@ async function handleRequest(request: ExtensionRequest, sender: Browser.runtime. case 'bridge.pair.status': return ok(engineBridge.getPairingStatus()); case 'bridge.unpair': { await engineBridge.unpair(); + await stopPairedBrowserTasks(); void appendAuditEvent({ category: 'bridge', action: 'bridge.unpair', outcome: 'success' }); return ok(await getState()); } @@ -431,6 +474,7 @@ async function handleRequest(request: ExtensionRequest, sender: Browser.runtime. } case 'bridge.disconnect': { engineBridge.disconnect(); + await stopPairedBrowserTasks(); void appendAuditEvent({ category: 'bridge', action: 'bridge.disconnect', outcome: 'success' }); return ok(engineBridge.getStatus()); } @@ -443,10 +487,11 @@ let backgroundStarted = false; async function restoreBackgroundState(): Promise { const storedState = await restoreGrantLifecycle(); - const state = applyPolicyToState(storedState, (await getEnterprisePolicy()).policy); + const policy = (await getEnterprisePolicy()).policy; + const state = applyPolicyToState(storedState, policy); if (JSON.stringify(state.bridge) !== JSON.stringify(storedState.bridge) || JSON.stringify(state.floatingPanel) !== JSON.stringify(storedState.floatingPanel)) { - await updateState(() => state); + await updateState((current) => applyPolicyToState(current, policy)); } try { await reconcileUserAgentRuntime(); @@ -460,8 +505,10 @@ async function restoreBackgroundState(): Promise { summary: (error instanceof Error ? error.message : String(error)).slice(0, 512), }); } - if (state.bridge.autoConnect && state.bridge.pairedEngine) { - await engineBridge.connect(state.bridge).catch(console.error); + const currentState = await getState(); + await syncManagedInstanceBadge(currentState.bridge.managedInstance); + if (currentState.bridge.autoConnect && currentState.bridge.pairedEngine) { + await engineBridge.connect(currentState.bridge).catch(console.error); } } @@ -470,7 +517,6 @@ export function runBackground(): void { backgroundStarted = true; configureGrantLifecycleHooks({ - cancelActiveRequests: () => engineBridge.cancelActiveRequests(), emitHandoffChanged: (handoff) => engineBridge.emitEvent('browser.handoff.changed', handoff), }); registerGrantLifecycleListeners(); diff --git a/src/entrypoints/options/App.tsx b/src/entrypoints/options/App.tsx index 1bd6035..a440525 100644 --- a/src/entrypoints/options/App.tsx +++ b/src/entrypoints/options/App.tsx @@ -19,8 +19,6 @@ import { ProxyProfilesView } from '@/features/proxy/ui/ProxyProfilesView'; import { RuleSourcesView } from '@/features/proxy/ui/RuleSourcesView'; import { RecordingWorkspace } from '@/features/browser-recording/RecordingWorkspace'; import { AuthorizationTestingWorkspace } from '@/features/authorization-testing/ui/AuthorizationTestingWorkspace'; -import { gatewayShareActive, gatewayShareGrantInput } from '@/features/grants/gateway-share'; -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, @@ -227,7 +225,7 @@ function App() { {tab?.favIconUrl ? : } } -
{state.activeGrant ? `${isControlScopeSet(state.activeGrant.scopes) ? '控制' : '只读'}会话` : '未共享'}
+
{bridge.state === 'connected' ? '实例已连接' : '实例离线'}
{handoff && } @@ -240,7 +238,7 @@ function App() { {section === 'sources' && } {section === 'cookies' && } {section === 'user-agent' && } - {section === 'network' && } + {section === 'network' && } {section === 'context' && } {section === 'engine' && } {section === 'activity' && } @@ -313,7 +311,7 @@ function ActivityLog({ run, busy }: { run: (task: () => Promise, success?: 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.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、接管与能力结果。

@@ -362,12 +360,12 @@ function Overview({ state, bridge, tab, navigate, run, busy }: { state: Extensio

运行概览

{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 会话{state.activeGrant ? `${isControlScopeSet(state.activeGrant.scopes) ? '控制' : '只读'} · ${runtime.state}` : '未共享'}{state.activeGrant ? `${state.activeGrant.targets.length} 个 frame · ${new Date(state.activeGrant.expiresAt).toLocaleTimeString()} 到期` : '创建 task-bound grant 后才允许远程读取'}
+
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 会在这里出现'}
@@ -518,15 +516,11 @@ function networkLabel(record: NetworkRequestRecord): { host: string; path: strin } function NetworkActivity({ - state, - setState, tab, bridge, run, busy, }: { - state: ExtensionState; - setState: (state: ExtensionState) => void; tab?: ActiveTabInfo; bridge: BridgeStatus; run: (task: () => Promise, success?: string) => Promise; @@ -543,11 +537,11 @@ function NetworkActivity({ const [captureHeaders, setCaptureHeaders] = useState(false); const [captureBody, setCaptureBody] = useState(false); const [query, setQuery] = useState(''); - const transformShared = gatewayShareActive(state.activeGrant, tab); + const transformShared = bridge.state === 'connected'; const shareTransform = async () => { - if (!tab) throw new Error('请先选择需要共享的页面'); - setState(await request('grant.create', gatewayShareGrantInput(state, tab))); + if (!tab) throw new Error('请先选择需要使用的页面'); + if (bridge.state !== 'connected') await request('bridge.connect'); }; const load = useCallback(async () => { @@ -666,8 +660,6 @@ function NetworkActivity({ busy={busy} run={run} gatewayShared={transformShared} - gatewayShareExpiresAt={transformShared ? state.activeGrant?.expiresAt : undefined} - gatewayBridgeConnected={bridge.state === 'connected'} onShareGateway={shareTransform} />
; @@ -799,15 +791,7 @@ function EngineSettings({ state, setState, bridge, setBridge, tabs, run, busy }: const [draft, setDraft] = useState(state.bridge); const [pairing, setPairing] = useState({ state: 'idle', message: state.bridge.pairedEngine ? '当前浏览器已配对' : '尚未配对' }); const [panelDraft, setPanelDraft] = useState(state.floatingPanel); - const [framesByTab, setFramesByTab] = useState>({}); - const [selectedTargets, setSelectedTargets] = useState(state.activeGrant?.targets.map((target) => `${target.tabId}:${target.frameId}`) || []); - const [grantLevel, setGrantLevel] = useState<'read' | 'control'>(state.activeGrant && isControlScopeSet(state.activeGrant.scopes) ? 'control' : 'read'); - const [allowProgramEval, setAllowProgramEval] = useState(Boolean(state.activeGrant?.scopes.includes('browser.page.eval.program'))); const [policy, setPolicy] = useState({ managed: false, policy: {}, warnings: [] }); - const [durationMinutes, setDurationMinutes] = useState(30); - const selectedGrantScopes = grantLevel === 'control' - ? [...CONTROL_CAPABILITY_SCOPES, ...(allowProgramEval ? ['browser.page.eval.program' as const] : [])] - : READ_CAPABILITY_SCOPES; useEffect(() => { void request('policy.status').then(setPolicy).catch(() => undefined); void request('bridge.pair.status').then(setPairing).catch(() => undefined); @@ -818,23 +802,7 @@ function EngineSettings({ state, setState, bridge, setBridge, tabs, run, busy }: browser.runtime.onMessage.addListener(listener); return () => browser.runtime.onMessage.removeListener(listener); }, []); - useEffect(() => { - let active = true; - void Promise.all(tabs.map(async (item) => [item.id, await request('frame.list', { tabId: item.id }).catch(() => [])] as const)) - .then((inventories) => { - if (active) setFramesByTab(Object.fromEntries(inventories)); - }); - return () => { active = false; }; - }, [tabs]); useEffect(() => setDraft(state.bridge), [state.bridge]); - const toggleTarget = (key: string, checked: boolean) => setSelectedTargets((current) => checked - ? [...new Set([...current, key])] - : current.filter((item) => item !== key)); - const toggleTab = (tabId: number, checked: boolean) => { - const mainKey = `${tabId}:0`; - if (checked) toggleTarget(mainKey, true); - else setSelectedTargets((current) => current.filter((key) => !key.startsWith(`${tabId}:`))); - }; const save = () => run(async () => { if (draft.transport === 'native') { // Permission requests must be the first browser call made from the click gesture. @@ -879,8 +847,9 @@ function EngineSettings({ state, setState, bridge, setBridge, tabs, run, busy }:
-

浏览器共享会话

只把明确勾选的 frame 和能力授权给当前 Agent;子 frame、刷新和跨来源导航不会静默继承授权。

{tabs.map((tabItem) => { const frames = framesByTab[tabItem.id] || []; const mainSelected = selectedTargets.includes(`${tabItem.id}:0`); return
{mainSelected && frames.filter((frame) => !frame.isTop).map((frame) => )}
; })}
{grantLevel === 'control' && }
{selectedGrantScopes.filter((scope) => policy.policy.allowProgramEval !== false || scope !== 'browser.page.eval.program').map((scope) => {CAPABILITY_LABELS[scope]})}
{state.activeGrant && }
{state.activeGrant &&
{isControlScopeSet(state.activeGrant.scopes) ? '控制会话' : '只读会话'}{state.activeGrant.targets.length} 个 frame · {state.activeGrant.scopes.length} 项能力 · {new Date(state.activeGrant.expiresAt).toLocaleString()} 到期
}
-

Bridge 方法

browser.tabs / frames列出授权标签页与完整 frame inventory
browser.context生成结构化快照、存储 inventory、认证信号与上下文 diff
browser.node.*检查或操作快照内的文档绑定节点引用
browser.cookies读取指定标签页的浏览器 Cookie
browser.network.*控制有界网络捕获、读取请求时间线并导出重放包
browser.takeover将页面切到前台,交给用户扫码或二次验证
browser.invoke以控制权限调用页面已有全局函数
browser.eval以控制权限在页面主世界执行代码,支持 Promise 和超时
proxy.list / switch读取并切换扩展代理配置
+

浏览器实例访问

配对成功后,Yakit 可直接引用此浏览器中的全部 HTTP(S) 页面;刷新、跳转和新标签页会自动跟随,不再逐页授权。

{bridge.state === 'connected' ? '实例已连接' : state.bridge.pairedEngine ? '实例已配对,当前离线' : '实例尚未配对'}{tabs.length} 个可访问页面 · 浏览器内部页始终排除 · 无痕窗口沿用浏览器自己的独立访问开关
人工:逐次确认 · 协同 AI:按风险判断 · YOLO:自动执行{policy.policy.allowProgramEval === false ? '程序 Eval 已被企业策略禁用' : '程序 Eval 在 YOLO 下无需手动批准,仍受浏览器与企业策略限制'}{policy.policy.grantAllowedOrigins?.length ? 企业来源白名单:{policy.policy.grantAllowedOrigins.length} 项 : null}
+ +

Bridge 方法

browser.tabs / tab.open / frames列出当前实例的 HTTP(S) 标签页、打开网页并读取完整 frame inventory
browser.context生成结构化快照、存储 inventory、认证信号与上下文 diff
browser.node.*检查或操作快照内的文档绑定节点引用
browser.cookies读取指定标签页的浏览器 Cookie
browser.network.*控制有界网络捕获、读取请求时间线并导出重放包
browser.takeover将页面切到前台,交给用户扫码或二次验证
browser.invoke调用页面已有全局函数
browser.eval在页面主世界执行代码,支持 Promise 和超时
proxy.list / switch读取并切换扩展代理配置
; } diff --git a/src/entrypoints/ytray-bootstrap/index.html b/src/entrypoints/ytray-bootstrap/index.html new file mode 100644 index 0000000..829ed7f --- /dev/null +++ b/src/entrypoints/ytray-bootstrap/index.html @@ -0,0 +1,11 @@ + + + + + 正在准备浏览器实例 + + +

正在同步浏览器实例身份…

+ + + diff --git a/src/entrypoints/ytray-bootstrap/main.ts b/src/entrypoints/ytray-bootstrap/main.ts new file mode 100644 index 0000000..2026d7b --- /dev/null +++ b/src/entrypoints/ytray-bootstrap/main.ts @@ -0,0 +1,46 @@ +import { browser } from 'wxt/browser'; +import { request } from '@/platform/messaging/runtime'; + +const status = document.getElementById('status'); +const fail = (message: string) => { + if (status) status.textContent = message; +}; + +async function bootstrap(): Promise { + const query = new URLSearchParams(location.search); + const manager = query.get('manager'); + const instanceId = query.get('instanceId') || ''; + const badge = query.get('badge') || ''; + const target = query.get('target') || 'chrome://newtab/'; + if (!['ytray', 'yakit'].includes(manager || '') + || !/^[A-Za-z0-9-]{1,160}$/.test(instanceId) + || !/^[A-Z]{1,2}$/.test(badge)) { + throw new Error('浏览器实例身份参数无效'); + } + const protocol = new URL(target).protocol; + if (!['http:', 'https:', 'chrome:'].includes(protocol) + && target !== 'data:text/html,YTray') { + throw new Error('浏览器实例目标地址无效'); + } + + await request('bridge.managed-instance.bind', { + manager: manager as 'ytray' | 'yakit', instanceId, badge, + }); + + const current = await browser.tabs.getCurrent(); + if (!current?.id) { + location.replace(target); + return; + } + if (query.get('restore') === '1') { + await new Promise((resolve) => globalThis.setTimeout(resolve, 400)); + const tabs = await browser.tabs.query({ currentWindow: true }); + if (tabs.some((tab) => tab.id !== current.id)) { + await browser.tabs.remove(current.id); + return; + } + } + await browser.tabs.update(current.id, { url: target }); +} + +void bootstrap().catch((error) => fail(error instanceof Error ? error.message : String(error))); diff --git a/src/features/browser-recording/RecordingWorkspace.tsx b/src/features/browser-recording/RecordingWorkspace.tsx index 85a3ee8..82c6223 100644 --- a/src/features/browser-recording/RecordingWorkspace.tsx +++ b/src/features/browser-recording/RecordingWorkspace.tsx @@ -29,8 +29,6 @@ interface RecordingWorkspaceProps { busy: boolean; run: RunTask; gatewayShared: boolean; - gatewayShareExpiresAt?: number; - gatewayBridgeConnected: boolean; onShareGateway: () => Promise; } @@ -183,8 +181,6 @@ export function RecordingWorkspace({ busy, run, gatewayShared, - gatewayShareExpiresAt, - gatewayBridgeConnected, onShareGateway, }: RecordingWorkspaceProps) { const [workspaceMode, setWorkspaceMode] = useState<'gateway' | 'recording' | 'deep'>('recording'); @@ -634,8 +630,6 @@ export function RecordingWorkspace({ busy={busy} run={run} gatewayShared={gatewayShared} - gatewayShareExpiresAt={gatewayShareExpiresAt} - gatewayBridgeConnected={gatewayBridgeConnected} onShareGateway={onShareGateway} onOpenCapture={() => { setRecoveryProfileId(''); setWorkspaceMode(DEEP_CAPTURE_AVAILABLE ? 'deep' : 'recording'); }} onOpenRecovery={DEEP_CAPTURE_AVAILABLE ? openRecovery : () => setWorkspaceMode('recording')} diff --git a/src/features/browser-transform/BrowserTransformWorkspace.tsx b/src/features/browser-transform/BrowserTransformWorkspace.tsx index d772632..d4be4cb 100644 --- a/src/features/browser-transform/BrowserTransformWorkspace.tsx +++ b/src/features/browser-transform/BrowserTransformWorkspace.tsx @@ -45,8 +45,6 @@ interface BrowserTransformWorkspaceProps { busy: boolean; run: RunTask; gatewayShared: boolean; - gatewayShareExpiresAt?: number; - gatewayBridgeConnected: boolean; onShareGateway: () => Promise; onOpenCapture: () => void; onOpenRecovery: (profileId: string) => void; @@ -247,8 +245,6 @@ export function BrowserTransformWorkspace({ busy, run, gatewayShared, - gatewayShareExpiresAt, - gatewayBridgeConnected, onShareGateway, onOpenCapture, onOpenRecovery, @@ -926,11 +922,9 @@ export function BrowserTransformWorkspace({ replayPersistenceLabel={replayPersistenceLabel(replayPersistence)} replayPersistenceTitle={replayPersistenceTitle} gatewayShared={gatewayShared} - gatewayShareExpiresAt={gatewayShareExpiresAt} - gatewayBridgeConnected={gatewayBridgeConnected} onShareGateway={() => run( onShareGateway, - gatewayShared ? '共享会话已刷新' : '当前页面已共享给 Yakit', + gatewayShared ? '浏览器实例已连接' : '正在连接 Yakit', )} onClear={clearReplay} canExecute={Boolean(draft?.id && !dirty && !busy && !replayLoading && bindingReady)} diff --git a/src/features/browser-transform/TransformReplayPanel.tsx b/src/features/browser-transform/TransformReplayPanel.tsx index eb71520..1ad5b21 100644 --- a/src/features/browser-transform/TransformReplayPanel.tsx +++ b/src/features/browser-transform/TransformReplayPanel.tsx @@ -1,4 +1,4 @@ -import { AlertTriangle, CheckCircle2, FlaskConical, Play, Share2, ShieldCheck, Trash2 } from 'lucide-react'; +import { AlertTriangle, CheckCircle2, FlaskConical, Play, ShieldCheck, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import type { ActiveTabInfo, @@ -28,8 +28,6 @@ export function TransformReplayPanel({ replayPersistenceLabel, replayPersistenceTitle, gatewayShared, - gatewayShareExpiresAt, - gatewayBridgeConnected, onShareGateway, onClear, canExecute, @@ -56,8 +54,6 @@ export function TransformReplayPanel({ replayPersistenceLabel: string; replayPersistenceTitle: string; gatewayShared: boolean; - gatewayShareExpiresAt?: number; - gatewayBridgeConnected: boolean; onShareGateway: () => Promise; onClear: () => Promise; canExecute: boolean; @@ -87,21 +83,19 @@ export function TransformReplayPanel({ {draft?.id &&
- {gatewayShared ? : } +
- {gatewayShared ? '当前页面已共享给 Yakit' : '在 Yakit 中使用这个网关'} - {gatewayShared && gatewayShareExpiresAt - ? `控制会话 · ${new Date(gatewayShareExpiresAt).toLocaleTimeString()} 到期` - : gatewayBridgeConnected - ? '创建 30 分钟控制会话,并保留已共享页面' - : '可先创建会话;引擎重连后即可使用'} + {gatewayShared ? '当前浏览器实例已接入 Yakit' : '连接 Yakit 后使用这个网关'} + {gatewayShared + ? '页面刷新、跳转后仍可使用,无需续接授权' + : '连接后由 Agent 操作审核策略统一控制'}
- + >连接}
}