mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-27 05:31:53 +08:00
feat: advance browser agent integration workflows
This commit is contained in:
@@ -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);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user