feat: advance browser agent integration workflows

This commit is contained in:
go0p
2026-08-06 15:11:35 +08:00
committed by go0p
parent af5a4db694
commit 0c8e1c7b69
215 changed files with 35137 additions and 5442 deletions
@@ -0,0 +1,153 @@
import type { BrowserAuthorizationResourceSelector } from '@/types/models';
import type { CapabilityDomainHandler } from '../capability-context';
import { allowedTarget, requireScope } from '../capability-context';
import {
captureAuthContextHandle,
getAuthContextHandle,
} from '@/features/authorization-testing/auth-context';
import {
captureAuthContextAttestation,
getAuthContextAttestation,
} from '@/features/authorization-testing/auth-attestation';
import {
bindAuthorizationBaselineLogicalRequest,
captureAuthorizationBaseline,
compileAuthorizationBaseline,
compileAuthorizationBaselinePacket,
compileAuthorizationBaselineWithTransform,
getAuthorizationBaseline,
inspectAuthorizationBaselineTransform,
listAuthorizationBaselineCandidates,
readAuthorizationBaselineResource,
} from '@/features/authorization-testing/baseline';
import { AUTHORIZATION_CAPABILITY_DOMAIN } from '../capability-domains';
function requireAuthorizationContextScopes(
grant: Parameters<typeof requireScope>[0],
): void {
requireScope(grant, 'browser.cookies.read');
requireScope(grant, 'browser.storage.read');
}
function requireAuthorizationBaselineScopes(
grant: Parameters<typeof requireScope>[0],
): void {
requireScope(grant, 'browser.isolation.read');
requireAuthorizationContextScopes(grant);
}
export const authorizationCapabilityHandler: CapabilityDomainHandler = {
...AUTHORIZATION_CAPABILITY_DOMAIN,
async handle({ method, input, grant }) {
if (method === 'browser.authorization.context.capture') {
requireAuthorizationContextScopes(grant);
return captureAuthContextHandle({
slotId: input.slotId === 'right' ? 'right' : 'left',
accountLabel: typeof input.accountLabel === 'string' ? input.accountLabel : undefined,
isolationProofId: String(input.isolationProofId || ''),
target: await allowedTarget(grant, input),
grantId: grant.id,
grantExpiresAt: grant.expiresAt,
});
}
if (method === 'browser.authorization.context.get') {
requireAuthorizationContextScopes(grant);
return getAuthContextHandle(String(input.id || ''), grant.id);
}
if (method === 'browser.authorization.context.attest') {
requireAuthorizationContextScopes(grant);
return captureAuthContextAttestation({
target: await allowedTarget(grant, input),
grantId: grant.id,
grantExpiresAt: grant.expiresAt,
});
}
if (method === 'browser.authorization.context.attestation.get') {
requireAuthorizationContextScopes(grant);
return getAuthContextAttestation(String(input.id || ''), grant.id);
}
requireAuthorizationBaselineScopes(grant);
if (method === 'browser.authorization.baseline.capture') {
return captureAuthorizationBaseline({
target: await allowedTarget(grant, input),
grantId: grant.id,
authContextKind: input.authContextKind === 'attestation' ? 'attestation' : 'handle',
authContextId: String(input.authContextId || ''),
networkRequestId: String(input.networkRequestId || ''),
comparisonKey: String(input.comparisonKey || ''),
});
}
if (method === 'browser.authorization.baseline.candidates') {
return listAuthorizationBaselineCandidates({
target: await allowedTarget(grant, input),
grantId: grant.id,
authContextKind: input.authContextKind === 'attestation' ? 'attestation' : 'handle',
authContextId: String(input.authContextId || ''),
limit: typeof input.limit === 'number' ? input.limit : 100,
});
}
if (method === 'browser.authorization.baseline.get') {
return getAuthorizationBaseline(String(input.id || ''), grant.id);
}
if (method === 'browser.authorization.baseline.logical.bind') {
requireScope(grant, 'browser.network.sensitive.read');
requireScope(grant, 'browser.transform.execute');
return bindAuthorizationBaselineLogicalRequest({
id: String(input.id || ''),
grantId: grant.id,
profileId: String(input.profileId || ''),
comparisonKey: String(input.comparisonKey || ''),
});
}
if (method === 'browser.authorization.baseline.resource.get') {
return readAuthorizationBaselineResource({
id: String(input.id || ''),
grantId: grant.id,
selector: input.selector as BrowserAuthorizationResourceSelector,
});
}
if (method === 'browser.authorization.baseline.compile') {
requireScope(grant, 'browser.network.sensitive.read');
return compileAuthorizationBaseline({
id: String(input.id || ''),
grantId: grant.id,
selector: input.selector as BrowserAuthorizationResourceSelector,
replacement: input.replacement as Parameters<typeof compileAuthorizationBaseline>[0]['replacement'],
comparisonKey: String(input.comparisonKey || ''),
});
}
if (method === 'browser.authorization.baseline.packet.compile') {
requireScope(grant, 'browser.network.replay');
requireScope(grant, 'browser.network.sensitive.read');
return compileAuthorizationBaselinePacket({
id: String(input.id || ''),
grantId: grant.id,
});
}
if (method === 'browser.authorization.baseline.transform.inspect') {
requireScope(grant, 'browser.network.sensitive.read');
requireScope(grant, 'browser.transform.read');
return inspectAuthorizationBaselineTransform({
id: String(input.id || ''),
grantId: grant.id,
profileId: String(input.profileId || ''),
});
}
if (method === 'browser.authorization.baseline.transform.compile') {
requireScope(grant, 'browser.network.replay');
requireScope(grant, 'browser.network.sensitive.read');
requireScope(grant, 'browser.transform.execute');
return compileAuthorizationBaselineWithTransform({
id: String(input.id || ''),
grantId: grant.id,
selector: input.selector as BrowserAuthorizationResourceSelector,
replacement: input.replacement as Parameters<typeof compileAuthorizationBaselineWithTransform>[0]['replacement'],
comparisonKey: String(input.comparisonKey || ''),
profileId: String(input.profileId || ''),
bindingFingerprint: String(input.bindingFingerprint || ''),
});
}
throw new Error(`授权能力没有实现: ${method}`);
},
};
@@ -0,0 +1,50 @@
import { browser } from 'wxt/browser';
import type { HandoffReason } from '@/types/models';
import type { CapabilityDomainHandler } from '../capability-context';
import { allowedTarget } from '../capability-context';
import { activateTab } from '@/platform/browser/targets';
import { getState, updateState } from '@/platform/storage/state';
import { setAgentRuntimeState } from '@/features/agent-runtime/service';
import { ExtensionError } from '@/shared/errors';
import { HANDOFF_CAPABILITY_DOMAIN } from '../capability-domains';
export const handoffCapabilityHandler: CapabilityDomainHandler = {
...HANDOFF_CAPABILITY_DOMAIN,
async handle({ method, input, grant }) {
if (method === 'browser.handoff.status') {
const handoff = (await getState()).handoff;
return handoff?.taskId === grant.taskId ? handoff : { state: 'idle' };
}
const resolvedTarget = await allowedTarget(grant, input);
const grantTarget = grant.targets.find((target) => (
target.tabId === resolvedTarget.tabId && target.frameId === resolvedTarget.frameId
));
if (!grantTarget) throw new Error('目标标签页不在本次共享会话中');
const now = Date.now();
const state = await updateState((current) => {
if (current.activeGrant?.id !== grant.id || current.activeGrant.expiresAt <= Date.now()) {
throw new ExtensionError('grant_expired', '浏览器共享会话已经变化,请重新发起请求');
}
if (current.handoff?.state === 'waiting_for_user') {
throw new ExtensionError('handoff_in_progress', '已有人工接管请求正在等待处理');
}
return {
...current,
handoff: {
id: crypto.randomUUID(),
taskId: grant.taskId,
target: grantTarget,
reason: input.reason as HandoffReason,
message: typeof input.message === 'string' ? input.message : '',
state: 'waiting_for_user' as const,
requestedAt: now,
},
};
});
await activateTab(resolvedTarget.tabId);
await browser.action.setBadgeBackgroundColor({ color: '#ee7815' });
await browser.action.setBadgeText({ text: '待确认', tabId: resolvedTarget.tabId });
await setAgentRuntimeState('waiting_for_human', grant);
return state.handoff;
},
};
@@ -0,0 +1,88 @@
import type { CapabilityDomainHandler } from '../capability-context';
import { allowedTarget, requireScope } from '../capability-context';
import { getFrameInventory } from '@/features/page-context/frames';
import { getTab } from '@/platform/browser/targets';
import {
createBrowserIsolationProof,
deleteFirefoxContainerIdentity,
inspectBrowserIsolation,
listFirefoxContainerIdentities,
openFirefoxContainerIdentity,
openIncognitoIdentity,
} from '@/features/authorization-testing/isolation';
import { ExtensionError } from '@/shared/errors';
import { NAVIGATION_CAPABILITY_DOMAIN } from '../capability-domains';
export const navigationCapabilityHandler: CapabilityDomainHandler = {
...NAVIGATION_CAPABILITY_DOMAIN,
async handle({ method, input, grant }) {
if (method === 'browser.tabs') {
const tabIds = [...new Set(grant.targets.map((target) => target.tabId))];
const tabs = await Promise.all(tabIds.map(async (tabId) => {
const targets = grant.targets.filter((target) => target.tabId === tabId);
for (const target of targets) {
try {
await allowedTarget(grant, {
tabId,
frameId: target.frameId,
documentId: target.documentId,
});
return getTab(tabId);
} catch {
// A tab remains visible while at least one explicitly granted frame is current.
}
}
return undefined;
}));
return tabs.filter(Boolean);
}
if (method === 'browser.frames') {
const tabId = typeof input.tabId === 'number' ? input.tabId : grant.targets[0]?.tabId;
if (!tabId || !grant.targets.some((target) => target.tabId === tabId)) {
throw new ExtensionError('target_denied', '目标标签页不在本次共享会话中');
}
return getFrameInventory(tabId);
}
if (method === 'browser.isolation.inspect') {
const grantedTabIds = [...new Set(grant.targets.map((target) => target.tabId))];
const requestedTabIds = Array.isArray(input.tabIds)
? input.tabIds.map(Number)
: grantedTabIds;
if (requestedTabIds.some((tabId) => !grantedTabIds.includes(tabId))) {
throw new ExtensionError(
'target_denied',
'身份隔离检查只能读取本次共享会话中的标签页',
);
}
return inspectBrowserIsolation(requestedTabIds);
}
if (method === 'browser.isolation.proof') {
requireScope(grant, 'browser.cookies.read');
requireScope(grant, 'browser.storage.read');
const leftTabId = Number(input.leftTabId);
const rightTabId = Number(input.rightTabId);
if (![leftTabId, rightTabId].every((tabId) => (
grant.targets.some((target) => target.tabId === tabId)
))) {
throw new ExtensionError(
'target_denied',
'隔离证明的两个身份都必须在本次共享会话中',
);
}
return createBrowserIsolationProof(leftTabId, rightTabId);
}
if (method === 'browser.isolation.incognito.open') {
return openIncognitoIdentity(String(input.url || ''));
}
if (method === 'browser.isolation.container.open') {
return openFirefoxContainerIdentity({
url: String(input.url || ''),
name: typeof input.name === 'string' ? input.name : undefined,
});
}
if (method === 'browser.isolation.container.list') {
return listFirefoxContainerIdentities();
}
return deleteFirefoxContainerIdentity(String(input.cookieStoreId || ''));
},
};
@@ -0,0 +1,68 @@
import type {
BrowserRequestAnalysisBundle,
YakPocGenerateResult,
} from '@/types/models';
import type { CapabilityDomainHandler } from '../capability-context';
import { allowedTarget, requireScope } from '../capability-context';
import {
clearNetworkRequests,
exportNetworkRequest,
listNetworkRequests,
networkCaptureStatus,
redactNetworkRequests,
startNetworkCapture,
stopNetworkCapture,
} from '@/features/network-capture/service';
import { capturedRequestEnginePayload } from '@/features/network-capture/workflows';
import { ExtensionError } from '@/shared/errors';
import { NETWORK_CAPABILITY_DOMAIN } from '../capability-domains';
export const networkCapabilityHandler: CapabilityDomainHandler = {
...NETWORK_CAPABILITY_DOMAIN,
async handle({ method, input, grant, requestEngine }) {
const target = await allowedTarget(grant, input);
if (method === 'browser.network.start') {
if (input.captureHeaders === true || input.captureBody === true) {
requireScope(grant, 'browser.network.sensitive.read');
}
return startNetworkCapture(target, {
captureHeaders: input.captureHeaders === true,
captureBody: input.captureBody === true,
maxEntries: typeof input.maxEntries === 'number' ? input.maxEntries : undefined,
maxBodyBytes: typeof input.maxBodyBytes === 'number' ? input.maxBodyBytes : undefined,
}, { kind: 'grant', grantId: grant.id, expiresAt: grant.expiresAt });
}
if (method === 'browser.network.status') return networkCaptureStatus(target);
if (method === 'browser.network.list') {
const records = await listNetworkRequests(
target,
typeof input.limit === 'number' ? input.limit : 100,
);
return grant.scopes.includes('browser.network.sensitive.read')
? records
: redactNetworkRequests(records);
}
if (method === 'browser.network.clear') return clearNetworkRequests(target);
if (method === 'browser.network.stop') return stopNetworkCapture(target);
if (method === 'browser.network.export') {
return exportNetworkRequest(target, String(input.id));
}
if (!requestEngine) {
throw new ExtensionError('bridge_disconnected', 'Yak 引擎请求通道不可用');
}
if (method === 'browser.network.poc') {
return requestEngine<YakPocGenerateResult>(
'yakit.poc.generate',
await capturedRequestEnginePayload(target, String(input.id), false),
);
}
return requestEngine<BrowserRequestAnalysisBundle>(
'yakit.browser_request.prepare_analysis',
await capturedRequestEnginePayload(
target,
String(input.id),
grant.scopes.includes('browser.recording.read'),
),
);
},
};
@@ -0,0 +1,97 @@
import { browser } from 'wxt/browser';
import type { PageContextOptions } from '@/types/models';
import type { CapabilityDomainHandler } from '../capability-context';
import { allowedTarget, requireScope } from '../capability-context';
import { activateTab } from '@/platform/browser/targets';
import {
actOnPageNode,
capturePageContext,
evalInPage,
inspectPageNode,
invokePageFunction,
} from '@/features/page-context/service';
import { listCookies } from '@/features/cookies/service';
import { resolveTabCookieStoreId } from '@/platform/browser/isolation';
import { ExtensionError } from '@/shared/errors';
import { PAGE_CAPABILITY_DOMAIN } from '../capability-domains';
export const pageCapabilityHandler: CapabilityDomainHandler = {
...PAGE_CAPABILITY_DOMAIN,
async handle({ method, input, grant }) {
if (method === 'browser.context') {
const options: PageContextOptions = {
includeDom: input.includeDom !== false,
includeStorage: input.includeStorage === true,
includeCookies: input.includeCookies === true,
};
if (options.includeStorage) requireScope(grant, 'browser.storage.read');
if (options.includeCookies) requireScope(grant, 'browser.cookies.read');
return capturePageContext(options, await allowedTarget(grant, input));
}
if (method === 'browser.node.inspect') {
const target = await allowedTarget(grant, input);
return inspectPageNode(String(input.captureId), String(input.nodeId), target);
}
if (method === 'browser.node.action') {
const target = await allowedTarget(grant, input);
return actOnPageNode(
String(input.captureId),
String(input.nodeId),
input.action as 'click' | 'focus' | 'scroll' | 'setValue',
target,
typeof input.value === 'string' ? input.value : undefined,
);
}
if (method === 'browser.cookies') {
const target = await allowedTarget(grant, input);
const frame = await browser.webNavigation.getFrame({
tabId: target.tabId,
frameId: target.frameId,
});
const grantTarget = grant.targets.find((item) => (
item.tabId === target.tabId && item.frameId === target.frameId
));
const url = frame?.url && /^https?:/i.test(frame.url)
? frame.url
: `${grantTarget?.origin || ''}/`;
if (!/^https?:/i.test(url)) {
throw new ExtensionError(
'target_unavailable',
'目标 frame 没有可读取 Cookie 的 HTTP 来源',
);
}
return listCookies(url, await resolveTabCookieStoreId(target.tabId));
}
if (method === 'browser.takeover') {
const target = await allowedTarget(grant, input);
await activateTab(target.tabId);
await browser.action.setBadgeBackgroundColor({ color: '#f28c28' });
await browser.action.setBadgeText({ text: '接管', tabId: target.tabId });
globalThis.setTimeout(
() => void browser.action.setBadgeText({ text: '', tabId: target.tabId }),
10_000,
);
return { activated: true, target };
}
if (method === 'browser.invoke') {
if (typeof input.path !== 'string') throw new Error('缺少页面函数路径');
const timeoutMs = typeof input.timeoutMs === 'number' ? input.timeoutMs : 10_000;
return invokePageFunction(
input.path,
Array.isArray(input.args) ? input.args : [],
await allowedTarget(grant, input),
timeoutMs,
);
}
if (typeof input.code !== 'string' || !input.code.trim()) {
throw new Error('缺少页面执行代码');
}
const timeoutMs = typeof input.timeoutMs === 'number' ? input.timeoutMs : 10_000;
return evalInPage(
input.code,
input.mode as 'expression' | 'program',
await allowedTarget(grant, input),
timeoutMs,
);
},
};
@@ -0,0 +1,14 @@
import type { CapabilityDomainHandler } from '../capability-context';
import { getState } from '@/platform/storage/state';
import { switchProxy } from '@/features/proxy/service';
import { PROXY_CAPABILITY_DOMAIN } from '../capability-domains';
export const proxyCapabilityHandler: CapabilityDomainHandler = {
...PROXY_CAPABILITY_DOMAIN,
async handle({ method, input }) {
if (method === 'proxy.list') return (await getState()).proxyProfiles;
if (typeof input.id !== 'string') throw new Error('缺少代理配置 ID');
await switchProxy(input.id);
return { activeProxyId: input.id };
},
};
@@ -0,0 +1,174 @@
import type { BrowserDeepCaptureMatcher } from '@/types/models';
import type { CapabilityDomainHandler } from '../capability-context';
import { allowedTarget, requireScope } from '../capability-context';
import {
browserRecordingStatus,
clearBrowserRecording,
createRecordedPageCallable,
getBrowserRecording,
startBrowserRecording,
stopBrowserRecording,
} from '@/features/browser-recording/service';
import {
createCapturedPageCallable,
deepCaptureStatus,
detachDeepCapture,
keepDeepCaptureAlive,
resumeDeepCapture,
startDeepCapture,
} from '@/features/deep-capture/service';
import {
deletePageCallable,
executePageCallable,
listPageCallables,
} from '@/features/page-callable/service';
import { invalidateBrowserTransformProfilesForCallable } from '@/features/browser-transform/service';
import {
callableInspect,
callableReplay,
recordingEvidenceInspect,
recordingTraceList,
resolveBrowserProfileCallableAnalysis,
resolveBrowserProfileCaptureContext,
stageBrowserProfileEvidence,
} from '@/features/browser-analysis/service';
import { RECORDING_CAPABILITY_DOMAIN } from '../capability-domains';
export const recordingCapabilityHandler: CapabilityDomainHandler = {
...RECORDING_CAPABILITY_DOMAIN,
async handle({ method, input, grant }) {
if (method.startsWith('browser.recording.')) {
const target = await allowedTarget(grant, input);
if (method === 'browser.recording.trace.list') {
return recordingTraceList(target, typeof input.limit === 'number' ? input.limit : 40);
}
if (method === 'browser.recording.evidence.inspect') {
const includeValues = input.includeValues === true;
if (includeValues) requireScope(grant, 'browser.recording.sensitive.read');
return recordingEvidenceInspect(
target,
String(input.traceId || ''),
typeof input.eventId === 'string' ? input.eventId : undefined,
includeValues,
);
}
if (method === 'browser.recording.start') {
if (input.captureValues === true) {
requireScope(grant, 'browser.recording.sensitive.read');
}
return startBrowserRecording(target, {
captureValues: input.captureValues === true,
maxEntries: typeof input.maxEntries === 'number' ? input.maxEntries : undefined,
maxValueBytes: typeof input.maxValueBytes === 'number' ? input.maxValueBytes : undefined,
expiresAt: grant.expiresAt,
}, { kind: 'grant', grantId: grant.id, expiresAt: grant.expiresAt });
}
if (method === 'browser.recording.status') return browserRecordingStatus(target);
if (method === 'browser.recording.get') {
const snapshot = await getBrowserRecording(
target,
typeof input.limit === 'number' ? input.limit : 500,
grant.scopes.includes('browser.recording.sensitive.read'),
);
await stageBrowserProfileEvidence(snapshot);
return snapshot;
}
if (method === 'browser.recording.clear') {
return clearBrowserRecording(
target,
grant.scopes.includes('browser.recording.sensitive.read'),
);
}
const snapshot = await stopBrowserRecording(
target,
grant.scopes.includes('browser.recording.sensitive.read'),
);
await stageBrowserProfileEvidence(snapshot);
return snapshot;
}
if (method.startsWith('browser.callable.')) {
const source = String(input.source || '');
const target = await allowedTarget(grant, input, source !== 'deep-capture');
if (method === 'browser.callable.inspect') {
return callableInspect(
target,
typeof input.callableId === 'string' ? input.callableId : undefined,
);
}
if (method === 'browser.callable.replay') {
return callableReplay(
target,
String(input.callableId || ''),
Array.isArray(input.args) ? input.args : [],
);
}
if (method === 'browser.callable.list') return listPageCallables(target);
if (method === 'browser.callable.create') {
if (source === 'deep-capture') {
requireScope(grant, 'browser.debugger.control');
if (input.strategy === 'request-transaction') {
const capture = await resolveBrowserProfileCaptureContext(
target,
String(input.candidateId || ''),
);
return createCapturedPageCallable(target, String(input.callFrameId || ''), {
strategy: 'request-transaction',
name: typeof input.name === 'string' ? input.name : undefined,
transaction: capture.transaction,
analysis: capture.analysis,
}, { kind: 'grant', grantId: grant.id, expiresAt: grant.expiresAt });
}
const selectedFrameAnalysis = input.strategy === 'selected-frame' && typeof input.candidateId === 'string'
? await resolveBrowserProfileCallableAnalysis(target, input.candidateId)
: undefined;
return createCapturedPageCallable(
target,
String(input.callFrameId || ''),
input.strategy === 'expression' ? {
strategy: 'expression',
name: String(input.name || ''),
functionExpression: String(input.functionExpression || ''),
} : {
strategy: 'selected-frame',
name: typeof input.name === 'string' ? input.name : undefined,
analysis: selectedFrameAnalysis,
},
{ kind: 'grant', grantId: grant.id, expiresAt: grant.expiresAt },
);
}
return createRecordedPageCallable(target, {
callHandleId: String(input.callHandleId || ''),
name: String(input.name || ''),
});
}
if (method === 'browser.callable.execute') {
return executePageCallable(
target,
String(input.callableId || ''),
Array.isArray(input.args) ? input.args : [],
);
}
const callableId = String(input.callableId || '');
const callables = await deletePageCallable(target, callableId);
await invalidateBrowserTransformProfilesForCallable(target, callableId);
return callables;
}
const target = await allowedTarget(
grant,
input,
method === 'browser.deep_capture.start',
);
const owner = { kind: 'grant' as const, grantId: grant.id, expiresAt: grant.expiresAt };
if (method === 'browser.deep_capture.start') {
return startDeepCapture(target, input.matcher as BrowserDeepCaptureMatcher, owner);
}
if (method === 'browser.deep_capture.status') return deepCaptureStatus(target, owner);
if (method === 'browser.deep_capture.keepalive') return keepDeepCaptureAlive(target, owner);
if (method === 'browser.deep_capture.resume') {
return resumeDeepCapture(target, 'engine-request', owner);
}
return detachDeepCapture(target, owner);
},
};
@@ -0,0 +1,145 @@
import type {
BrowserTransformExecuteInput,
BrowserTransformPacket,
BrowserTransformProfileInput,
} from '@/types/models';
import type { CapabilityDomainHandler } from '../capability-context';
import { allowedTarget, requireScope } from '../capability-context';
import {
captureBrowserTransformRecovery,
confirmBrowserTransformRecovery,
deleteBrowserTransformProfile,
executeBrowserTransform,
getBrowserTransformProfile,
getBrowserTransformRecovery,
listBrowserTransformProfiles,
resetBrowserTransformRecovery,
saveBrowserTransformProfile,
startBrowserTransformRecovery,
validateBrowserTransformRecovery,
} from '@/features/browser-transform/service';
import {
compareBrowserPackets,
latestBrowserTransformValidation,
proposeBrowserTransformProfile,
validateInferredBrowserTransformProfile,
} from '@/features/browser-analysis/service';
import { ExtensionError } from '@/shared/errors';
import { TRANSFORM_CAPABILITY_DOMAIN } from '../capability-domains';
export const transformCapabilityHandler: CapabilityDomainHandler = {
...TRANSFORM_CAPABILITY_DOMAIN,
async handle({ method, input, grant }) {
if (method === 'browser.packet.compare') {
await allowedTarget(grant, input);
return compareBrowserPackets(
input.actual as BrowserTransformPacket,
input.expected as BrowserTransformPacket,
input.mode === 'exact' ? 'exact' : 'structure',
);
}
if (method === 'browser.profile.propose') {
requireScope(grant, 'browser.recording.read');
const target = await allowedTarget(grant, input);
return proposeBrowserTransformProfile(
target,
String(input.candidateId || ''),
String(input.callableId || ''),
Array.isArray(input.inputPaths) ? input.inputPaths.map(String) : undefined,
typeof input.name === 'string' ? input.name : undefined,
);
}
if (method === 'browser.profile.validation.latest') {
return latestBrowserTransformValidation(await allowedTarget(grant, input));
}
if (method === 'browser.profile.validate') {
requireScope(grant, 'browser.recording.read');
const target = await allowedTarget(grant, input);
return validateInferredBrowserTransformProfile(
target,
String(input.candidateId || ''),
String(input.callableId || ''),
input.packet as BrowserTransformPacket,
Array.isArray(input.inputPaths) ? input.inputPaths.map(String) : undefined,
typeof input.name === 'string' ? input.name : undefined,
input.observed as BrowserTransformPacket | undefined,
input.comparisonMode === 'exact' ? 'exact' : 'structure',
);
}
if (method.startsWith('browser.transform.recovery.')) {
const profile = await getBrowserTransformProfile(String(input.id || ''));
const resolveInPage = method !== 'browser.transform.recovery.capture'
&& method !== 'browser.transform.recovery.reset';
const target = await allowedTarget(grant, {
tabId: profile.target.tabId,
frameId: profile.target.frameId,
...(typeof input.documentId === 'string' ? { documentId: input.documentId } : {}),
}, resolveInPage);
const owner = { kind: 'grant' as const, grantId: grant.id, expiresAt: grant.expiresAt };
if (method === 'browser.transform.recovery.get') {
return getBrowserTransformRecovery(profile.id);
}
if (method === 'browser.transform.recovery.start') {
requireScope(grant, 'browser.debugger.control');
return startBrowserTransformRecovery(profile.id, owner);
}
if (method === 'browser.transform.recovery.capture') {
requireScope(grant, 'browser.debugger.control');
requireScope(grant, 'browser.callable.execute');
return captureBrowserTransformRecovery(
profile.id,
target,
String(input.callFrameId || ''),
input.strategy === 'request-transaction'
? 'request-transaction'
: 'selected-frame',
owner,
);
}
if (method === 'browser.transform.recovery.validate') {
return validateBrowserTransformRecovery(
profile.id,
input.packet as BrowserTransformPacket,
);
}
if (method === 'browser.transform.recovery.confirm') {
return confirmBrowserTransformRecovery(profile.id, String(input.validationId || ''));
}
return resetBrowserTransformRecovery(profile.id, owner);
}
if (method === 'browser.transform.profile.list') {
const profiles = await listBrowserTransformProfiles();
const visible = await Promise.all(profiles.map(async (profile) => {
try {
await allowedTarget(grant, profile.target);
return profile;
} catch {
return undefined;
}
}));
return visible.filter(Boolean);
}
if (method === 'browser.transform.profile.save') {
const profileInput = input as unknown as BrowserTransformProfileInput;
const target = await allowedTarget(grant, profileInput.target);
const grantedTarget = grant.targets.find((item) => (
item.tabId === target.tabId && item.frameId === target.frameId
));
if (!grantedTarget || profileInput.origin !== grantedTarget.origin) {
throw new ExtensionError('target_denied', '转换配置来源不在本次共享会话中');
}
return saveBrowserTransformProfile({ ...profileInput, target });
}
if (method === 'browser.transform.profile.delete') {
const profile = await getBrowserTransformProfile(String(input.id || ''));
await allowedTarget(grant, profile.target);
return deleteBrowserTransformProfile(profile.id);
}
const executeInput = input as unknown as BrowserTransformExecuteInput;
const profile = await getBrowserTransformProfile(executeInput.profileId);
await allowedTarget(grant, profile.target);
return executeBrowserTransform(executeInput);
},
};