mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-26 21:21:53 +08:00
fix: expose browser pairing diagnostics
This commit is contained in:
@@ -409,7 +409,10 @@ async function handleRequest(request: ExtensionRequest, sender: Browser.runtime.
|
|||||||
}
|
}
|
||||||
case 'agent.actions.clear': return ok(await clearAgentActions());
|
case 'agent.actions.clear': return ok(await clearAgentActions());
|
||||||
case 'policy.status': return ok(await getEnterprisePolicy());
|
case 'policy.status': return ok(await getEnterprisePolicy());
|
||||||
case 'diagnostics.export': return ok(await createDiagnosticsBundle(engineBridge.getStatus()));
|
case 'diagnostics.export': return ok(await createDiagnosticsBundle(
|
||||||
|
engineBridge.getStatus(),
|
||||||
|
engineBridge.getPairingStatus(),
|
||||||
|
));
|
||||||
case 'metrics.get': return ok(await getRuntimeMetrics());
|
case 'metrics.get': return ok(await getRuntimeMetrics());
|
||||||
case 'metrics.reset': return ok(await resetRuntimeMetrics());
|
case 'metrics.reset': return ok(await resetRuntimeMetrics());
|
||||||
case 'bridge.config.save': {
|
case 'bridge.config.save': {
|
||||||
@@ -451,9 +454,25 @@ async function handleRequest(request: ExtensionRequest, sender: Browser.runtime.
|
|||||||
return ok(engineBridge.getStatus());
|
return ok(engineBridge.getStatus());
|
||||||
}
|
}
|
||||||
case 'bridge.pair': {
|
case 'bridge.pair': {
|
||||||
const status = await engineBridge.startPairing();
|
const startedAt = Date.now();
|
||||||
void appendAuditEvent({ category: 'bridge', action: 'bridge.pair', outcome: 'success' });
|
try {
|
||||||
return ok(status);
|
const status = await engineBridge.startPairing();
|
||||||
|
void appendAuditEvent({
|
||||||
|
category: 'bridge', action: 'bridge.pair',
|
||||||
|
outcome: status.state === 'rejected'
|
||||||
|
? 'denied'
|
||||||
|
: status.state === 'pending' || status.state === 'approved' ? 'success' : 'error',
|
||||||
|
durationMs: Date.now() - startedAt, summary: `${status.state}: ${status.message}`,
|
||||||
|
});
|
||||||
|
return ok(status);
|
||||||
|
} catch (error) {
|
||||||
|
void appendAuditEvent({
|
||||||
|
category: 'bridge', action: 'bridge.pair', outcome: 'error',
|
||||||
|
durationMs: Date.now() - startedAt, errorCode: errorCode(error),
|
||||||
|
summary: (error instanceof Error ? error.message : String(error)).slice(0, 512),
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case 'bridge.pair.cancel': return ok(engineBridge.cancelPairing());
|
case 'bridge.pair.cancel': return ok(engineBridge.cancelPairing());
|
||||||
case 'bridge.pair.status': return ok(engineBridge.getPairingStatus());
|
case 'bridge.pair.status': return ok(engineBridge.getPairingStatus());
|
||||||
|
|||||||
@@ -716,6 +716,10 @@ input[type='checkbox'] { width: 15px; height: 15px; flex: 0 0 auto; padding: 0;
|
|||||||
.pairing-code span { color: var(--muted); font-size: var(--text-xs); font-weight: 650; letter-spacing: .05em; text-transform: uppercase; }
|
.pairing-code span { color: var(--muted); font-size: var(--text-xs); font-weight: 650; letter-spacing: .05em; text-transform: uppercase; }
|
||||||
.pairing-code strong { font-family: var(--font-mono); font-size: 30px; font-weight: 700; letter-spacing: .12em; }
|
.pairing-code strong { font-family: var(--font-mono); font-size: 30px; font-weight: 700; letter-spacing: .12em; }
|
||||||
.pairing-code small { color: var(--muted); font-size: var(--text-sm); }
|
.pairing-code small { color: var(--muted); font-size: var(--text-sm); }
|
||||||
|
.pairing-diagnostics { padding: 10px 12px; display: grid; grid-template-columns: minmax(90px, .35fr) minmax(0, 1fr) auto; gap: 12px; align-items: center; border-radius: var(--radius-md); background: var(--surface-subtle); text-align: left; }
|
||||||
|
.pairing-diagnostics > div { min-width: 0; }
|
||||||
|
.pairing-diagnostics span { display: block; color: var(--muted); font-size: var(--text-xs); }
|
||||||
|
.pairing-diagnostics code { display: block; margin-top: 2px; overflow: hidden; font-size: var(--text-sm); white-space: nowrap; text-overflow: ellipsis; }
|
||||||
.paired-engine-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
.paired-engine-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||||
.paired-engine-meta > div { min-width: 0; }
|
.paired-engine-meta > div { min-width: 0; }
|
||||||
.paired-engine-meta span { color: var(--muted); font-size: var(--text-xs); font-weight: 650; letter-spacing: .04em; text-transform: uppercase; }
|
.paired-engine-meta span { color: var(--muted); font-size: var(--text-xs); font-weight: 650; letter-spacing: .04em; text-transform: uppercase; }
|
||||||
@@ -795,6 +799,8 @@ input[type='checkbox'] { width: 15px; height: 15px; flex: 0 0 auto; padding: 0;
|
|||||||
.ua-assignment-list code { display: none; }
|
.ua-assignment-list code { display: none; }
|
||||||
.task-command-bar, .agent-runtime-summary { flex-direction: column; display: flex; align-items: stretch; }
|
.task-command-bar, .agent-runtime-summary { flex-direction: column; display: flex; align-items: stretch; }
|
||||||
.task-status-grid, .context-session-strip, .diff-summary, .context-inventory-grid, .grant-options, .panel-policy-grid, .form-grid, .paired-engine-meta { grid-template-columns: minmax(0, 1fr); }
|
.task-status-grid, .context-session-strip, .diff-summary, .context-inventory-grid, .grant-options, .panel-policy-grid, .form-grid, .paired-engine-meta { grid-template-columns: minmax(0, 1fr); }
|
||||||
|
.pairing-diagnostics { grid-template-columns: minmax(0, 1fr); }
|
||||||
|
.pairing-diagnostics .ui-button { justify-self: start; }
|
||||||
.agent-action-row { grid-template-columns: 12px 76px minmax(0, 1fr) 76px; }
|
.agent-action-row { grid-template-columns: 12px 76px minmax(0, 1fr) 76px; }
|
||||||
.agent-action-row span:nth-child(4), .agent-action-row span:last-child { display: none; }
|
.agent-action-row span:nth-child(4), .agent-action-row span:last-child { display: none; }
|
||||||
.activity-table__head, .activity-table__row { grid-template-columns: 120px minmax(0, 1fr) 80px; }
|
.activity-table__head, .activity-table__row { grid-template-columns: 120px minmax(0, 1fr) 80px; }
|
||||||
|
|||||||
@@ -838,6 +838,28 @@ function EngineSettings({ state, setState, bridge, setBridge, tabs, run, busy }:
|
|||||||
setState(next);
|
setState(next);
|
||||||
setPanelDraft(next.floatingPanel);
|
setPanelDraft(next.floatingPanel);
|
||||||
}, '悬浮面板策略已保存');
|
}, '悬浮面板策略已保存');
|
||||||
|
const pairingEndpoint = (() => {
|
||||||
|
try {
|
||||||
|
const value = new URL(state.bridge.endpoint);
|
||||||
|
value.pathname = '/pairing';
|
||||||
|
value.search = '';
|
||||||
|
value.hash = '';
|
||||||
|
return value.toString();
|
||||||
|
} catch {
|
||||||
|
return state.bridge.endpoint;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
const copyPairingDiagnostics = () => run(async () => {
|
||||||
|
const { code, ...safePairing } = pairing;
|
||||||
|
await navigator.clipboard.writeText(JSON.stringify({
|
||||||
|
extensionVersion: browser.runtime.getManifest().version,
|
||||||
|
pairing: { ...safePairing, hasVerificationCode: Boolean(code) },
|
||||||
|
bridge,
|
||||||
|
transport: state.bridge.transport,
|
||||||
|
endpoint: pairingEndpoint,
|
||||||
|
managedInstance: state.bridge.managedInstance,
|
||||||
|
}, null, 2));
|
||||||
|
}, '配对排查信息已复制');
|
||||||
return <div className="section-view engine-view">
|
return <div className="section-view engine-view">
|
||||||
<div className="page-heading"><div><h1>Yak 引擎连接</h1><p>扩展主动连接本机 Bridge,网页无法直接访问此通道。</p></div><span className={`large-status ${bridge.state}`}><Radio size={16} />{bridge.message}</span></div>
|
<div className="page-heading"><div><h1>Yak 引擎连接</h1><p>扩展主动连接本机 Bridge,网页无法直接访问此通道。</p></div><span className={`large-status ${bridge.state}`}><Radio size={16} />{bridge.message}</span></div>
|
||||||
{policy.managed && <div className="managed-policy-banner"><ShieldCheck size={16} /><span><strong>此浏览器由组织策略管理</strong><small>{policy.policy.disableWebSocket ? '必须使用 Native Messaging' : policy.policy.bridgeTransport ? `传输锁定为 ${policy.policy.bridgeTransport}` : '连接与授权限制已应用'}{policy.policy.maxGrantMinutes ? ` · 授权最长 ${policy.policy.maxGrantMinutes} 分钟` : ''}{policy.policy.allowProgramEval === false ? ' · 程序 Eval 已禁用' : ''}</small>{policy.warnings.map((warning) => <i key={warning}>{warning}</i>)}</span></div>}
|
{policy.managed && <div className="managed-policy-banner"><ShieldCheck size={16} /><span><strong>此浏览器由组织策略管理</strong><small>{policy.policy.disableWebSocket ? '必须使用 Native Messaging' : policy.policy.bridgeTransport ? `传输锁定为 ${policy.policy.bridgeTransport}` : '连接与授权限制已应用'}{policy.policy.maxGrantMinutes ? ` · 授权最长 ${policy.policy.maxGrantMinutes} 分钟` : ''}{policy.policy.allowProgramEval === false ? ' · 程序 Eval 已禁用' : ''}</small>{policy.warnings.map((warning) => <i key={warning}>{warning}</i>)}</span></div>}
|
||||||
@@ -847,6 +869,7 @@ function EngineSettings({ state, setState, bridge, setBridge, tabs, run, busy }:
|
|||||||
<div className="pairing-workspace__heading"><span className="pairing-icon"><KeyRound size={19} /></span><div><h2>{state.bridge.pairedEngine ? '浏览器已安全配对' : pairing.state === 'pending' ? '等待 Yakit 确认' : '连接本机 Yakit'}</h2><p>{state.bridge.pairedEngine ? '设备身份已锁定到首次批准的 Yak 引擎。' : pairing.message}</p></div></div>
|
<div className="pairing-workspace__heading"><span className="pairing-icon"><KeyRound size={19} /></span><div><h2>{state.bridge.pairedEngine ? '浏览器已安全配对' : pairing.state === 'pending' ? '等待 Yakit 确认' : '连接本机 Yakit'}</h2><p>{state.bridge.pairedEngine ? '设备身份已锁定到首次批准的 Yak 引擎。' : pairing.message}</p></div></div>
|
||||||
{pairing.state === 'pending' && <div className="pairing-code" aria-live="polite"><span>配对验证码</span><strong>{pairing.code?.slice(0, 3)} {pairing.code?.slice(3)}</strong><small>{pairing.expiresAt ? `${Math.max(0, Math.ceil((pairing.expiresAt - Date.now()) / 1000))} 秒内有效` : ''}</small></div>}
|
{pairing.state === 'pending' && <div className="pairing-code" aria-live="polite"><span>配对验证码</span><strong>{pairing.code?.slice(0, 3)} {pairing.code?.slice(3)}</strong><small>{pairing.expiresAt ? `${Math.max(0, Math.ceil((pairing.expiresAt - Date.now()) / 1000))} 秒内有效` : ''}</small></div>}
|
||||||
{state.bridge.pairedEngine && <div className="paired-engine-meta"><div><span>引擎身份</span><code title={state.bridge.pairedEngine.engineIdentityId}>{state.bridge.pairedEngine.engineIdentityId.slice(0, 24)}</code></div><div><span>设备 ID</span><code title={state.bridge.pairedEngine.deviceId}>{state.bridge.pairedEngine.deviceId.slice(0, 24)}</code></div></div>}
|
{state.bridge.pairedEngine && <div className="paired-engine-meta"><div><span>引擎身份</span><code title={state.bridge.pairedEngine.engineIdentityId}>{state.bridge.pairedEngine.engineIdentityId.slice(0, 24)}</code></div><div><span>设备 ID</span><code title={state.bridge.pairedEngine.deviceId}>{state.bridge.pairedEngine.deviceId.slice(0, 24)}</code></div></div>}
|
||||||
|
{!state.bridge.pairedEngine && <div className="pairing-diagnostics"><div><span>当前阶段</span><code>{pairing.state}</code></div><div><span>配对地址</span><code title={pairingEndpoint}>{pairingEndpoint}</code></div><Button size="sm" variant="ghost" disabled={busy} onClick={() => void copyPairingDiagnostics()}><Copy size={14} />复制排查信息</Button></div>}
|
||||||
<div className="editor-actions">
|
<div className="editor-actions">
|
||||||
{!state.bridge.pairedEngine && pairing.state !== 'pending' && <Button variant="primary" disabled={busy || pairing.state === 'requesting'} onClick={() => void run(async () => setPairing(await request('bridge.pair')))}><Power size={16} />{pairing.state === 'requesting' ? '正在查找' : '查找本机 Yakit'}</Button>}
|
{!state.bridge.pairedEngine && pairing.state !== 'pending' && <Button variant="primary" disabled={busy || pairing.state === 'requesting'} onClick={() => void run(async () => setPairing(await request('bridge.pair')))}><Power size={16} />{pairing.state === 'requesting' ? '正在查找' : '查找本机 Yakit'}</Button>}
|
||||||
{!state.bridge.pairedEngine && pairing.state === 'pending' && <Button disabled={busy} onClick={() => void run(async () => setPairing(await request('bridge.pair.cancel')))}><X size={16} />取消申请</Button>}
|
{!state.bridge.pairedEngine && pairing.state === 'pending' && <Button disabled={busy} onClick={() => void run(async () => setPairing(await request('bridge.pair.cancel')))}><X size={16} />取消申请</Button>}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ function App() {
|
|||||||
|
|
||||||
const toggleEngine = () => run(async () => {
|
const toggleEngine = () => run(async () => {
|
||||||
if (!state!.bridge.pairedEngine) {
|
if (!state!.bridge.pairedEngine) {
|
||||||
await request('bridge.pair');
|
void request('bridge.pair').catch(() => undefined);
|
||||||
await openTool('engine');
|
await openTool('engine');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { browser } from 'wxt/browser';
|
import { browser } from 'wxt/browser';
|
||||||
import { STATE_STORAGE_KEYS } from '@/protocol/storage';
|
import { STATE_STORAGE_KEYS } from '@/protocol/storage';
|
||||||
import type { BridgeStatus, DiagnosticsBundle } from '@/types/models';
|
import type { BridgePairingStatus, BridgeStatus, DiagnosticsBundle } from '@/types/models';
|
||||||
import { listAuditEvents } from '@/features/diagnostics/audit';
|
import { listAuditEvents } from '@/features/diagnostics/audit';
|
||||||
import { getState } from '@/platform/storage/state';
|
import { getState } from '@/platform/storage/state';
|
||||||
import { getEnterprisePolicy } from '@/platform/policy/managed';
|
import { getEnterprisePolicy } from '@/platform/policy/managed';
|
||||||
import { getRuntimeMetrics } from './metrics';
|
import { getRuntimeMetrics } from './metrics';
|
||||||
|
|
||||||
export async function createDiagnosticsBundle(bridge: BridgeStatus): Promise<DiagnosticsBundle> {
|
export async function createDiagnosticsBundle(
|
||||||
|
bridge: BridgeStatus,
|
||||||
|
pairing: BridgePairingStatus,
|
||||||
|
): Promise<DiagnosticsBundle> {
|
||||||
const manifest = browser.runtime.getManifest();
|
const manifest = browser.runtime.getManifest();
|
||||||
const sessionArea = (browser.storage as unknown as { session?: { get(keys: string[]): Promise<Record<string, unknown>> } }).session;
|
const sessionArea = (browser.storage as unknown as { session?: { get(keys: string[]): Promise<Record<string, unknown>> } }).session;
|
||||||
const [state, platform, policy, metrics, audit, local, session] = await Promise.all([
|
const [state, platform, policy, metrics, audit, local, session] = await Promise.all([
|
||||||
@@ -15,6 +18,7 @@ export async function createDiagnosticsBundle(bridge: BridgeStatus): Promise<Dia
|
|||||||
sessionArea?.get([...STATE_STORAGE_KEYS]) || Promise.resolve({}),
|
sessionArea?.get([...STATE_STORAGE_KEYS]) || Promise.resolve({}),
|
||||||
]);
|
]);
|
||||||
const { taskId: _taskId, grantId: _grantId, ...safeBridge } = bridge;
|
const { taskId: _taskId, grantId: _grantId, ...safeBridge } = bridge;
|
||||||
|
const { code, ...safePairing } = pairing;
|
||||||
return {
|
return {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
generatedAt: Date.now(),
|
generatedAt: Date.now(),
|
||||||
@@ -26,6 +30,15 @@ export async function createDiagnosticsBundle(bridge: BridgeStatus): Promise<Dia
|
|||||||
},
|
},
|
||||||
platform: { os: platform.os, arch: platform.arch },
|
platform: { os: platform.os, arch: platform.arch },
|
||||||
bridge: safeBridge,
|
bridge: safeBridge,
|
||||||
|
pairing: { ...safePairing, hasVerificationCode: Boolean(code) },
|
||||||
|
bridgeConfiguration: {
|
||||||
|
transport: state.bridge.transport,
|
||||||
|
endpoint: state.bridge.endpoint,
|
||||||
|
nativeHost: state.bridge.nativeHost,
|
||||||
|
autoConnect: state.bridge.autoConnect,
|
||||||
|
paired: Boolean(state.bridge.pairedEngine),
|
||||||
|
managedInstance: state.bridge.managedInstance,
|
||||||
|
},
|
||||||
policy,
|
policy,
|
||||||
state: {
|
state: {
|
||||||
proxyProfiles: state.proxyProfiles.length,
|
proxyProfiles: state.proxyProfiles.length,
|
||||||
|
|||||||
@@ -1482,6 +1482,15 @@ export interface DiagnosticsBundle {
|
|||||||
extension: { version: string; manifestVersion: number; buildChannel: string; permissions: string[] };
|
extension: { version: string; manifestVersion: number; buildChannel: string; permissions: string[] };
|
||||||
platform: { os: string; arch: string };
|
platform: { os: string; arch: string };
|
||||||
bridge: Omit<BridgeStatus, 'taskId' | 'grantId'>;
|
bridge: Omit<BridgeStatus, 'taskId' | 'grantId'>;
|
||||||
|
pairing: Omit<BridgePairingStatus, 'code'> & { hasVerificationCode: boolean };
|
||||||
|
bridgeConfiguration: {
|
||||||
|
transport: BridgeConfig['transport'];
|
||||||
|
endpoint: string;
|
||||||
|
nativeHost: string;
|
||||||
|
autoConnect: boolean;
|
||||||
|
paired: boolean;
|
||||||
|
managedInstance?: BridgeConfig['managedInstance'];
|
||||||
|
};
|
||||||
policy: EnterprisePolicyStatus;
|
policy: EnterprisePolicyStatus;
|
||||||
state: {
|
state: {
|
||||||
proxyProfiles: number;
|
proxyProfiles: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user