feat(browser): expose proxy state and browser identity (#9)

* feat(proxy): reflect and release browser proxy control

* feat(browser): report browser product identity
This commit is contained in:
go0p
2026-09-16 15:51:33 +08:00
committed by GitHub
parent 2000d59054
commit 4b0df8c706
22 changed files with 441 additions and 32 deletions
+2 -1
View File
@@ -16,6 +16,7 @@ import {
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';
@@ -337,7 +338,7 @@ function ActivityLog({ run, busy }: { run: (task: () => Promise<void>, success?:
}
function Overview({ state, bridge, tab, navigate, run, busy }: { state: ExtensionState; bridge: BridgeStatus; tab?: ActiveTabInfo; navigate: (value: Section) => void; run: (task: () => Promise<void>, success?: string) => Promise<void>; busy: boolean }) {
const activeProxy = state.proxyProfiles.find((profile) => profile.id === state.activeProxyId)?.name || (state.activeProxyId === 'auto' ? '自动切换' : '未知');
const activeProxy = useProxyStatus(state).label;
const [runtime, setRuntime] = useState<AgentRuntime>({ state: 'idle', updatedAt: Date.now(), actions: [] });
const [network, setNetwork] = useState<NetworkCaptureStatus>();
const [loginContext, setLoginContext] = useState<PageContext>();
+3 -1
View File
@@ -206,7 +206,9 @@
.popup-footer { margin-top: auto; padding: 10px 14px 12px; border-top: 1px solid var(--border); background: var(--surface); }
.popup-capture { width: 100%; height: 36px; border-radius: 7px; font-size: var(--text-md); box-shadow: 0 1px 0 color-mix(in srgb, var(--primary-strong) 55%, transparent); }
.popup-notice { display: block; margin-top: 6px; overflow: hidden; color: var(--muted); font-size: var(--text-sm); line-height: 16px; text-align: center; white-space: nowrap; text-overflow: ellipsis; }
.popup-global-notice { position: absolute; z-index: 20; left: 50%; bottom: 54px; max-width: calc(100% - 28px); padding: 7px 11px; overflow: hidden; border: 1px solid var(--border); border-radius: 999px; background: var(--foreground); color: var(--surface); box-shadow: var(--shadow-md); font-size: var(--text-xs); line-height: 16px; text-overflow: ellipsis; white-space: nowrap; pointer-events: none; transform: translateX(-50%); animation: popup-content-in .16s ease-out; }
/* Keep horizontal centering independent of the entrance animation's transform. */
.popup-global-notice { position: absolute; z-index: 20; left: 50%; bottom: 54px; max-width: calc(100% - 28px); padding: 7px 11px; overflow: hidden; border: 1px solid var(--border); border-radius: 999px; background: var(--foreground); color: var(--surface); box-shadow: var(--shadow-md); font-size: var(--text-xs); line-height: 16px; text-overflow: ellipsis; white-space: nowrap; pointer-events: none; translate: -50% 0; animation: popup-content-in .16s ease-out; }
@media (prefers-reduced-motion: reduce) { .popup-global-notice { animation: none; } }
.popup-loading { width: 390px; height: 230px; display: flex; align-items: center; justify-content: center; gap: 9px; color: var(--muted); background: var(--background); font-size: var(--text-md); }
.spin { animation: spin .8s linear infinite; }
@@ -2,6 +2,7 @@ import { Braces, ChevronRight, Cookie, Network, Radio, ShieldCheck, UserRoundCog
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { request } from '@/platform/messaging/runtime';
import { useProxyStatus } from '@/features/proxy/ui/ProxyStatusBar';
import { READ_CAPABILITY_SCOPES } from '@/protocol/capabilities';
import type { ActiveTabInfo, ExtensionState, UserAgentResolution } from '@/types/models';
@@ -25,9 +26,7 @@ interface OverviewQuickViewProps {
export function OverviewQuickView({
state, tab, grantActive, busy, run, setState, cookieCount, uaResolution, onNavigate, onOpenContext, onCapture,
}: OverviewQuickViewProps) {
const activeProxy = state.activeProxyId === 'auto'
? '自动切换'
: state.proxyProfiles.find((profile) => profile.id === state.activeProxyId)?.name || '未选择';
const activeProxy = useProxyStatus(state).label;
const targetAvailable = Boolean(tab?.url?.startsWith('http'));
return <section className="popup-overview-view">
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { AlertCircle, Check, ExternalLink, Globe2, LoaderCircle, Network, Route } from 'lucide-react';
import { request } from '@/platform/messaging/runtime';
import type { ActiveTabInfo, ExtensionState, ProxyProfile, ProxyRulePreview } from '@/types/models';
import { ProxyStatusBar, StartupProxyOption, useProxyStatus } from '@/features/proxy/ui/ProxyStatusBar';
type RunTask = (task: () => Promise<void>, success?: string) => Promise<void>;
@@ -43,10 +44,11 @@ function routeKindLabel(preview?: ProxyRulePreview): string {
}
export function ProxyQuickView({ state, setState, busy, run, tab, onOpenFull }: ProxyQuickViewProps) {
const status = useProxyStatus(state);
const [preview, setPreview] = useState<ProxyRulePreview>();
const currentHostname = hostname(tab?.url);
const autoActive = state.activeProxyId === 'auto';
const activeProfile = state.proxyProfiles.find((profile) => profile.id === state.activeProxyId);
const autoActive = status.activeProfileId === 'auto';
const activeProfile = state.proxyProfiles.find((profile) => profile.id === status.activeProfileId);
const routableProfiles = useMemo(
() => state.proxyProfiles.filter((profile) => profile.kind === 'direct' || profile.kind === 'fixed_servers'),
[state.proxyProfiles],
@@ -130,7 +132,7 @@ export function ProxyQuickView({ state, setState, busy, run, tab, onOpenFull }:
};
const effectiveProfile = state.proxyProfiles.find((profile) => profile.id === preview?.effectiveProfileId);
const activeModeName = autoActive ? '自动切换' : activeProfile?.name || '未选择';
const activeModeName = autoActive ? '自动切换' : status.label;
const siteHint = !autoActive
? `当前使用“${activeModeName}”;选择网站出口后将启用自动切换。`
: siteTarget === AUTOMATIC_TARGET
@@ -142,13 +144,14 @@ export function ProxyQuickView({ state, setState, busy, run, tab, onOpenFull }:
const routeKindText = autoActive ? routeKindLabel(preview) : '全局模式';
return <section className="popup-view popup-tool-view popup-proxy-view">
<ProxyStatusBar status={status} />
{currentHostname ? <section className="popup-site-router" aria-label="当前站点路由">
<div className="popup-site-router__heading">
<div><Globe2 size={16} /><span><small>当前站点</small><strong title={currentHostname}>{currentHostname}</strong></span></div>
<i className={routeKind}>{routeKindText}</i>
</div>
<div className="popup-site-decision" title={autoActive ? preview?.matchedCondition : activeModeName}>
<span>{routeLabel}</span><i>→</i><strong>{routeProfile?.name || '—'}</strong>
<span>{routeLabel}</span><i>→</i><strong>{routeProfile?.name || status.label}</strong>
</div>
<div className="popup-site-picker">
<label htmlFor="popup-site-proxy">网站出口 <span>选择后立即生效</span></label>
@@ -168,13 +171,14 @@ export function ProxyQuickView({ state, setState, busy, run, tab, onOpenFull }:
<div className="popup-mode-heading"><span><strong>浏览器模式</strong><small>全局切换,不会创建站点规则</small></span><i>{activeModeName}</i></div>
<div className="popup-proxy-list popup-proxy-list--view" role="radiogroup" aria-label="浏览器代理模式">
<StartupProxyOption state={state} status={status} setState={setState} run={run} busy={busy} />
<button role="radio" aria-checked={autoActive} className={autoActive ? 'is-active' : ''} disabled={busy} onClick={() => void switchAuto()}>
<span className="popup-mode-icon"><Route size={15} /></span>
<span><strong>自动切换</strong><small>{state.proxyRules.filter((rule) => rule.enabled).length} 条手动 · {sourceRuleCount.toLocaleString()} 条订阅</small></span>
{state.proxyRuntime.dirty ? <em>待应用</em> : autoActive ? <Check size={14} /> : null}
</button>
{state.proxyProfiles.map((profile) => {
const active = state.activeProxyId === profile.id;
const active = status.activeProfileId === profile.id;
return <button key={profile.id} role="radio" aria-checked={active} className={active ? 'is-active' : ''} disabled={busy} onClick={() => void run(async () => setState(await request('proxy.switch', { id: profile.id })), `${profile.name} 已作为全局模式启用`)}>
<span className="popup-mode-icon"><Network size={15} /></span>
<span><strong>{profile.name}</strong><small>{proxyDetail(profile)}</small></span>
+3
View File
@@ -25,6 +25,9 @@ async function bootstrap(): Promise<void> {
await request('bridge.managed-instance.bind', {
manager: manager as 'ytray' | 'yakit', instanceId, badge,
browserName: query.get('browserName') || undefined,
browserVersion: query.get('browserVersion') || undefined,
startupProxy: query.get('startupProxy') || undefined,
});
const current = await browser.tabs.getCurrent();