feat(browser): add local handoff presentation

This commit is contained in:
go0p
2026-09-03 17:04:00 +08:00
parent 0a349a7928
commit 8e84735ecf
11 changed files with 536 additions and 26 deletions
+10
View File
@@ -193,6 +193,16 @@ export const capabilityParams = {
message: v.optional(v.pipe(v.string(), v.trim(), v.maxLength(500)), ''),
}),
'browser.handoff.status': v.optional(v.strictObject({})),
'browser.handoff.presentation.get': v.strictObject({
handoffId: id,
}),
'browser.handoff.focus': v.strictObject({
handoffId: id,
}),
'browser.handoff.resolve': v.strictObject({
handoffId: id,
outcome: v.picklist(['completed', 'cancelled']),
}),
'browser.network.start': v.optional(v.strictObject({
...targetFields,
captureHeaders: v.optional(v.boolean()),
+18
View File
@@ -3,6 +3,7 @@ import {
BRIDGE_CAPABILITIES,
canonicalCapabilityCatalogPayload,
capabilityBaseScope,
capabilityVisibleToAgent,
getBridgeCapabilityCatalog,
} from './capabilities';
@@ -34,6 +35,23 @@ describe('versioned Bridge capability catalog', () => {
expect(JSON.stringify(evalCapability?.paramsSchema)).toContain('"mode"');
expect(JSON.stringify(evalCapability?.paramsSchema)).toContain('"program"');
expect(capabilityBaseScope('browser.profile.validate')).toBe('browser.transform.execute');
expect(catalog.capabilities.find((capability) => capability.method === 'browser.thumbnail')).toMatchObject({
agentVisible: false,
});
expect(catalog.capabilities.find((capability) => capability.method === 'browser.handoff.presentation.get')).toMatchObject({
agentVisible: false,
});
expect(catalog.capabilities.find((capability) => capability.method === 'browser.handoff.focus')).toMatchObject({
agentVisible: false,
});
expect(catalog.capabilities.find((capability) => capability.method === 'browser.handoff.resolve')).toMatchObject({
agentVisible: false,
});
expect(capabilityVisibleToAgent('browser.handoff.presentation.get')).toBe(false);
expect(capabilityVisibleToAgent('browser.handoff.focus')).toBe(false);
expect(capabilityVisibleToAgent('browser.handoff.resolve')).toBe(false);
expect(capabilityVisibleToAgent('browser.thumbnail')).toBe(false);
expect(capabilityVisibleToAgent('browser.context')).toBe(true);
expect(catalog.capabilities.find((capability) => capability.method === 'browser.transform.recovery.capture')).toMatchObject({
access: 'dangerous',
scopes: ['browser.transform.manage', 'browser.debugger.control', 'browser.callable.execute'],
+24 -2
View File
@@ -20,6 +20,7 @@ export type BridgeCapabilityMethod = keyof typeof capabilityParams;
interface CapabilityMetadata {
domain: BridgeCapabilityDomain;
access: BridgeCapabilityAccess;
agentVisible?: boolean;
summary: string;
scopes: CapabilityScope[];
conditionalScopes?: BridgeCapabilityScopeCondition[];
@@ -42,7 +43,7 @@ const CAPABILITY_METADATA = {
},
'browser.thumbnail': {
domain: 'page', access: 'read', summary: '读取当前可见标签页的低清预览图,供 Yakit 实例列表展示',
scopes: ['browser.tabs.read'], targetMode: 'tab', defaultTimeoutMs: READ_TIMEOUT_MS,
scopes: ['browser.tabs.read'], targetMode: 'tab', defaultTimeoutMs: READ_TIMEOUT_MS, agentVisible: false,
},
'browser.isolation.inspect': {
domain: 'isolation', access: 'read', summary: '读取浏览器实例内标签页的 Cookie Store 与身份隔离上下文',
@@ -168,13 +169,29 @@ const CAPABILITY_METADATA = {
scopes: ['browser.instance.close'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
},
'browser.handoff.request': {
domain: 'handoff', access: 'write', summary: '请求用户完成扫码、MFA、验证码或设备确认',
domain: 'handoff', access: 'write',
summary: '页面需要用户扫码、MFA、验证码或设备确认时调用;Yakit 会在本地呈现交互内容,Agent 只等待结果',
scopes: ['browser.human.takeover'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
},
'browser.handoff.status': {
domain: 'handoff', access: 'read', summary: '读取当前人工接管状态',
scopes: ['browser.human.takeover'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
},
'browser.handoff.presentation.get': {
domain: 'handoff', access: 'sensitive-read', agentVisible: false,
summary: '仅在本机提取当前扫码接管的二维码展示数据',
scopes: ['browser.human.takeover'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
},
'browser.handoff.focus': {
domain: 'handoff', access: 'write', agentVisible: false,
summary: '二维码无法在本地呈现时,由 Yakit 将对应浏览器实例切换到前台',
scopes: ['browser.human.takeover'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
},
'browser.handoff.resolve': {
domain: 'handoff', access: 'write', agentVisible: false,
summary: '由 Yakit 本地界面完成或取消人工接管',
scopes: ['browser.human.takeover'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
},
'browser.network.start': {
domain: 'network', access: 'control', summary: '启动有界网络捕获,可选采集请求头和 Body',
scopes: ['browser.network.capture'],
@@ -458,6 +475,11 @@ export function capabilityBaseScope(method: string): CapabilityScope | undefined
return CAPABILITY_METADATA[method as BridgeCapabilityMethod]?.scopes[0];
}
export function capabilityVisibleToAgent(method: string): boolean {
const metadata = CAPABILITY_METADATA[method as BridgeCapabilityMethod] as CapabilityMetadata | undefined;
return metadata?.agentVisible !== false;
}
export function isControlScopeSet(scopes: readonly CapabilityScope[]): boolean {
return scopes.some((scope) => !READ_CAPABILITY_SCOPES.includes(scope));
}