fix(capture): automate safe bidirectional gateways for users and agents

This commit is contained in:
go0p
2026-09-17 17:24:54 +08:00
parent 45f53604e8
commit 601f8acbcf
48 changed files with 2111 additions and 516 deletions
+8
View File
@@ -484,6 +484,8 @@ input[type='checkbox'] { width: 15px; height: 15px; flex: 0 0 auto; padding: 0;
.recording-pipeline-step button > span:nth-child(2) { min-width: 0; }
.recording-pipeline-step small, .recording-pipeline-step strong, .recording-pipeline-step em, .recording-pipeline-step b { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.recording-pipeline-step small { color: var(--muted); font-size: 10px; font-weight: 650; }
.recording-pipeline-step small.is-request { color: var(--warning); }
.recording-pipeline-step small.is-response { color: var(--primary); }
.recording-pipeline-step strong { margin-top: 2px; font-size: var(--text-sm); font-weight: 650; }
.recording-pipeline-step em { margin-top: 2px; color: var(--muted); font-size: var(--text-xs); font-style: normal; }
.recording-pipeline-step b { margin-top: 4px; color: var(--warning); font-size: 10px; font-weight: 650; }
@@ -517,6 +519,12 @@ input[type='checkbox'] { width: 15px; height: 15px; flex: 0 0 auto; padding: 0;
.recording-evidence { border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.recording-evidence summary { padding: 9px 0; color: var(--muted-strong); font-size: var(--text-sm); font-weight: 600; cursor: pointer; }
.recording-evidence[open] { padding-bottom: 10px; }
.recording-related-transform { padding: 10px; display: flex; align-items: center; justify-content: space-between; gap: 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-subtle); }
.recording-related-transform > div { min-width: 0; display: flex; align-items: center; gap: 8px; color: var(--primary); }
.recording-related-transform > div span { min-width: 0; }
.recording-related-transform strong, .recording-related-transform small { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.recording-related-transform strong { color: var(--foreground); font-size: var(--text-sm); }
.recording-related-transform small { margin-top: 2px; color: var(--muted); font-size: var(--text-xs); }
.profile-inference { padding: 11px 0 0; display: grid; gap: 10px; border-top: 2px solid var(--primary); }
.profile-inference.is-medium { border-top-color: var(--warning); }
.profile-inference.is-low { border-top-color: var(--border-strong); }
-1
View File
@@ -682,7 +682,6 @@ function GatewayWorkspace({
run={run}
gatewayShared={bridge.state === 'connected'}
onShareGateway={shareTransform}
initialMode="gateway"
/>
</div>;
}
+43 -18
View File
@@ -7,7 +7,7 @@ import {
type PageRecorderBridgeResponse,
} from '@/features/browser-recording/bridge-protocol';
import { PAGE_CALLABLE_REGISTRY_KEY } from '@/features/page-callable/constants';
import { executeRequestTransaction, executeSideEffectFreeCallable } from '@/features/page-callable/request-transaction';
import { executeRequestTransaction, executeSideEffectFreeCallable, observeCallableInput } from '@/features/page-callable/request-transaction';
import { callableExecutionPolicy, settleCallableResult } from '@/features/page-callable/execution';
import {
createCryptoAdapterRuntime,
@@ -200,8 +200,13 @@ interface RecordedCallHandle {
original: Function;
thisArg: unknown;
args: unknown[];
inputIndex: number;
originalInput: unknown;
replayInputs: Array<{
path: string;
name: string;
role: CallArgumentRole;
originalInput: unknown;
apply(args: unknown[], value: unknown): void;
}>;
eventId?: string;
traceId?: string;
recordingId?: string;
@@ -209,7 +214,6 @@ interface RecordedCallHandle {
outputDataType?: string;
outputEncoding?: PageCallableMetadata['output']['encoding'];
resultMode: 'sync' | 'promise';
adaptInput(value: unknown): unknown;
}
interface RecorderController {
@@ -485,6 +489,7 @@ export default defineUnlistedScript(() => {
}
const started = performance.now();
const inputIndex = plan.inputIndex;
if (inputIndex >= 0) observeCallableInput(args[inputIndex]);
const callHandleId = plan.callableKind && inputIndex >= 0 ? registerHandle({
kind: plan.callableKind,
operation: `${plan.crypto.adapterId}.${plan.crypto.operation}`,
@@ -492,11 +497,17 @@ export default defineUnlistedScript(() => {
original,
thisArg,
args: [...args],
inputIndex,
originalInput: args[inputIndex],
replayInputs: plan.replayInputs || [{
path: '$input',
name: 'data',
role: 'data',
originalInput: args[inputIndex],
apply: (nextArgs, value) => {
nextArgs[inputIndex] = (plan.adaptInput || ((input) => defaultAdaptInput(input, args[inputIndex])))(value);
},
}],
outputEncoding: plan.outputEncoding || plan.crypto.outputEncoding,
resultMode: operation.resultMode,
adaptInput: plan.adaptInput || ((value) => defaultAdaptInput(value, args[inputIndex])),
}) : undefined;
const item = observe(() => ({
kind: 'crypto',
@@ -728,8 +739,13 @@ export default defineUnlistedScript(() => {
return value;
}
function createRecordedCallable(handle: RecordedCallHandle, name: string): PageCallableMetadata {
function createRecordedCallable(handle: RecordedCallHandle, name: string, inputPaths?: string[]): PageCallableMetadata {
const id = unique('callable');
const replayInputs = inputPaths?.length
? inputPaths.map((path) => handle.replayInputs.find((item) => item.path === path))
: [handle.replayInputs[0]];
if (replayInputs.some((item) => !item)) throw new Error('页面调用不支持请求的动态输入');
const resolvedInputs = replayInputs as RecordedCallHandle['replayInputs'];
const metadata: PageCallableMetadata = {
id,
name: name.trim().slice(0, 120) || handle.operation,
@@ -740,15 +756,15 @@ export default defineUnlistedScript(() => {
origin: location.origin,
lifecycle: 'document',
execution: callableExecutionPolicy(handle.resultMode),
inputSlots: [{
id: 'data',
name: 'data',
index: 0,
role: 'data',
dataType: dataType(handle.originalInput),
inputSlots: resolvedInputs.map((input, index) => ({
id: input.name,
name: input.name,
index,
role: input.role,
dataType: dataType(input.originalInput),
required: true,
retained: false,
}],
})),
output: {
dataType: handle.outputDataType || 'unknown',
encoding: handle.outputEncoding || 'auto',
@@ -767,9 +783,9 @@ export default defineUnlistedScript(() => {
pageCallableRegistry().set(id, {
metadata,
invoke(values) {
if (!values.length) throw new Error('页面函数缺少 data 参数');
if (values.length < resolvedInputs.length) throw new Error(`页面函数需要 ${resolvedInputs.length} 个动态参数`);
const args = [...handle.args];
args[handle.inputIndex] = handle.adaptInput(values[0]);
resolvedInputs.forEach((input, index) => input.apply(args, values[index]));
return Reflect.apply(handle.original, handle.thisArg, args);
},
});
@@ -786,6 +802,11 @@ export default defineUnlistedScript(() => {
logicalInput: values[0],
invoke: (context) => entry.invoke(values, context),
timeoutMs: entry.metadata.execution.timeoutMs,
observeInputs: () => {
const recording = active || Boolean(deepBreakMatcher);
cryptoAdapterRuntime.start();
return () => { if (!recording) cryptoAdapterRuntime.stop(); };
},
})
: entry.metadata.kind === 'business-closure' || entry.metadata.kind === 'global-function'
? await executeSideEffectFreeCallable(() => entry.invoke(values), entry.metadata.execution)
@@ -941,7 +962,11 @@ export default defineUnlistedScript(() => {
const callHandleId = String(input.callHandleId || '');
const handle = handles.get(callHandleId);
if (!handle) throw new Error('加解密调用句柄不存在或已经失效');
return createRecordedCallable(handle, String(input.name || handle.operation));
return createRecordedCallable(
handle,
String(input.name || handle.operation),
Array.isArray(input.dynamicInputPaths) ? input.dynamicInputPaths.map(String) : undefined,
);
}
if (command === 'callable.list') return callableMetadata();
if (command === 'callable.execute') {