mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-27 05:31:53 +08:00
feat(browser): add managed instance agent capabilities
This commit is contained in:
@@ -3,6 +3,7 @@ import type { HandoffReason } from '@/types/models';
|
||||
import type { CapabilityDomainHandler } from '../capability-context';
|
||||
import { allowedTarget } from '../capability-context';
|
||||
import { activateTab } from '@/platform/browser/targets';
|
||||
import { getTab } from '@/platform/browser/targets';
|
||||
import { getState, updateState } from '@/platform/storage/state';
|
||||
import { setAgentRuntimeState } from '@/features/agent-runtime/service';
|
||||
import { ExtensionError } from '@/shared/errors';
|
||||
@@ -16,15 +17,23 @@ export const handoffCapabilityHandler: CapabilityDomainHandler = {
|
||||
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 [tab, frame] = await Promise.all([
|
||||
getTab(resolvedTarget.tabId),
|
||||
browser.webNavigation.getFrame(resolvedTarget),
|
||||
]);
|
||||
if (!frame?.url || !/^https?:/i.test(frame.url)) {
|
||||
throw new ExtensionError('target_unavailable', '目标 frame 不是可接管的 HTTP(S) 页面');
|
||||
}
|
||||
const grantTarget = {
|
||||
...resolvedTarget,
|
||||
isolationContextId: tab.isolationContextId || `browser-profile:tab-${tab.id}`,
|
||||
cookieStoreId: tab.cookieStoreId,
|
||||
origin: new URL(frame.url).origin,
|
||||
grantedUrl: frame.url,
|
||||
title: tab.title,
|
||||
};
|
||||
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', '已有人工接管请求正在等待处理');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { browser } from 'wxt/browser';
|
||||
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 { activateTab, getTab, scheduleBrowserInstanceClose } from '@/platform/browser/targets';
|
||||
import {
|
||||
createBrowserIsolationProof,
|
||||
deleteFirefoxContainerIdentity,
|
||||
@@ -11,70 +12,88 @@ import {
|
||||
openIncognitoIdentity,
|
||||
} from '@/features/authorization-testing/isolation';
|
||||
import { ExtensionError } from '@/shared/errors';
|
||||
import { assertBrowserAccessPolicy, getEnterprisePolicy } from '@/platform/policy/managed';
|
||||
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);
|
||||
const { tabs } = await inspectBrowserIsolation();
|
||||
const allowedOrigins = (await getEnterprisePolicy()).policy.grantAllowedOrigins;
|
||||
return tabs.filter((tab) => !allowedOrigins?.length || allowedOrigins.includes(new URL(tab.url).origin))
|
||||
.sort((left, right) => Number(Boolean(right.active)) - Number(Boolean(left.active))
|
||||
|| (right.lastAccessed || 0) - (left.lastAccessed || 0));
|
||||
}
|
||||
if (method === 'browser.tab.open') {
|
||||
const url = String(input.url || '');
|
||||
assertBrowserAccessPolicy((await getEnterprisePolicy()).policy, { origin: new URL(url).origin });
|
||||
const tab = await browser.tabs.create({ url, active: true });
|
||||
if (!tab.id) throw new ExtensionError('target_unavailable', '浏览器没有返回新标签页 ID');
|
||||
await activateTab(tab.id);
|
||||
return { opened: true, id: tab.id, windowId: tab.windowId, active: true, url };
|
||||
}
|
||||
if (method === 'browser.thumbnail') {
|
||||
const tab = await getTab(typeof input.tabId === 'number' ? input.tabId : undefined);
|
||||
await allowedTarget(grant, { tabId: tab.id }, false);
|
||||
if (!tab.active) {
|
||||
throw new ExtensionError('target_not_active', '只能预览浏览器窗口当前可见的标签页');
|
||||
}
|
||||
return {
|
||||
tabId: tab.id,
|
||||
title: tab.title,
|
||||
url: tab.url,
|
||||
capturedAt: Date.now(),
|
||||
dataUrl: await browser.tabs.captureVisibleTab(tab.windowId, { format: 'jpeg', quality: 55 }),
|
||||
};
|
||||
}
|
||||
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);
|
||||
const tabId = (await getTab(typeof input.tabId === 'number' ? input.tabId : undefined)).id;
|
||||
await allowedTarget(grant, { tabId }, false);
|
||||
const frames = await getFrameInventory(tabId);
|
||||
const allowedOrigins = (await getEnterprisePolicy()).policy.grantAllowedOrigins;
|
||||
return frames.filter((frame) => !allowedOrigins?.length
|
||||
|| Boolean(frame.origin && allowedOrigins.includes(frame.origin)));
|
||||
}
|
||||
if (method === 'browser.instance.close') return scheduleBrowserInstanceClose();
|
||||
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);
|
||||
: undefined;
|
||||
const inspection = await inspectBrowserIsolation(requestedTabIds);
|
||||
const allowedOrigins = (await getEnterprisePolicy()).policy.grantAllowedOrigins;
|
||||
if (!allowedOrigins?.length) return inspection;
|
||||
const tabs = inspection.tabs.filter((tab) => allowedOrigins.includes(new URL(tab.url).origin));
|
||||
const tabIds = new Set(tabs.map((tab) => tab.id));
|
||||
return {
|
||||
...inspection,
|
||||
tabs,
|
||||
contexts: inspection.contexts
|
||||
.map((context) => ({ ...context, tabIds: context.tabIds.filter((tabId) => tabIds.has(tabId)) }))
|
||||
.filter((context) => context.tabIds.length > 0),
|
||||
};
|
||||
}
|
||||
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',
|
||||
'隔离证明的两个身份都必须在本次共享会话中',
|
||||
);
|
||||
}
|
||||
await Promise.all([
|
||||
allowedTarget(grant, { tabId: leftTabId }, false),
|
||||
allowedTarget(grant, { tabId: rightTabId }, false),
|
||||
]);
|
||||
return createBrowserIsolationProof(leftTabId, rightTabId);
|
||||
}
|
||||
if (method === 'browser.isolation.incognito.open') {
|
||||
assertBrowserAccessPolicy((await getEnterprisePolicy()).policy, {
|
||||
origin: new URL(String(input.url || '')).origin,
|
||||
});
|
||||
return openIncognitoIdentity(String(input.url || ''));
|
||||
}
|
||||
if (method === 'browser.isolation.container.open') {
|
||||
assertBrowserAccessPolicy((await getEnterprisePolicy()).policy, {
|
||||
origin: new URL(String(input.url || '')).origin,
|
||||
});
|
||||
return openFirefoxContainerIdentity({
|
||||
url: String(input.url || ''),
|
||||
name: typeof input.name === 'string' ? input.name : undefined,
|
||||
|
||||
@@ -48,12 +48,7 @@ export const pageCapabilityHandler: CapabilityDomainHandler = {
|
||||
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 || ''}/`;
|
||||
const url = frame?.url || '';
|
||||
if (!/^https?:/i.test(url)) {
|
||||
throw new ExtensionError(
|
||||
'target_unavailable',
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
BrowserTransformPacket,
|
||||
BrowserTransformProfileInput,
|
||||
} from '@/types/models';
|
||||
import { browser } from 'wxt/browser';
|
||||
import type { CapabilityDomainHandler } from '../capability-context';
|
||||
import { allowedTarget, requireScope } from '../capability-context';
|
||||
import {
|
||||
@@ -124,11 +125,9 @@ export const transformCapabilityHandler: CapabilityDomainHandler = {
|
||||
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', '转换配置来源不在本次共享会话中');
|
||||
const frame = await browser.webNavigation.getFrame(target);
|
||||
if (!frame?.url || profileInput.origin !== new URL(frame.url).origin) {
|
||||
throw new ExtensionError('target_denied', '转换配置来源与当前页面不一致');
|
||||
}
|
||||
return saveBrowserTransformProfile({ ...profileInput, target });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user