import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AlertTriangle, ArrowDown, ArrowRight, Braces, CheckCircle2, ChevronDown, CirclePlus, Code2, FileInput, FileKey2, FlaskConical, Link2, Play, Plus, RefreshCw, Save, Sparkles, Trash2, Unplug, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; import { errorMessage, request } from '@/platform/messaging/runtime'; import type { ActiveTabInfo, BrowserPageCallable, BrowserRecordingEvent, BrowserTransformBuiltinOperation, BrowserTransformDirection, BrowserTransformDirectionName, BrowserTransformExecution, BrowserTransformNodeReference, BrowserTransformPipelineNode, BrowserTransformProfile, BrowserTransformProfileInput, BrowserProfileInferenceCandidate, } from '@/types/models'; import { compileGuidedTransform, defaultGuidedTransform, guidedOutputDescription, parseGuidedTransform, type GuidedTransformDraft, type GuidedTransformOutputKind, } from './guided'; import { createBrowserTransformProfileInput } from './profile-draft'; import { clearBrowserTransformReplayDraft, deleteBrowserTransformReplayDrafts, getBrowserTransformReplayDraft, saveBrowserTransformReplayDraft, type BrowserTransformReplayDraftFields, type BrowserTransformReplayDraftInput, } from './replay-draft'; import './browser-transform-workspace.css'; type RunTask = (task: () => Promise, success?: string) => Promise; interface BrowserTransformWorkspaceProps { tab?: ActiveTabInfo; selectedEvent?: BrowserRecordingEvent; busy: boolean; run: RunTask; onOpenCapture: () => void; suggestion?: BrowserTransformSuggestionSeed; } export interface BrowserTransformSuggestionSeed { revision: number; candidate: BrowserProfileInferenceCandidate; callable: BrowserPageCallable; profile: BrowserTransformProfile; sampleBody?: string; sampleLabel?: string; } const BUILTINS: Array<{ value: BrowserTransformBuiltinOperation; label: string }> = [ { value: 'value.literal', label: '固定值' }, { value: 'json.stringify', label: 'JSON 序列化' }, { value: 'json.parse', label: 'JSON 解析' }, { value: 'text.toString', label: '转为文本' }, { value: 'url.encode', label: 'URL 编码' }, { value: 'url.decode', label: 'URL 解码' }, { value: 'base64.encode', label: 'Base64 编码' }, { value: 'base64.decode', label: 'Base64 解码' }, { value: 'hex.encode', label: 'Hex 编码' }, { value: 'hex.decode', label: 'Hex 解码' }, { value: 'object.pick', label: '选择对象字段' }, { value: 'object.compose', label: '组合对象' }, { value: 'form.compose', label: '组合表单' }, ]; function originOf(url?: string): string { try { return url ? new URL(url).origin : ''; } catch { return ''; } } function absoluteUrl(value?: string, base?: string): string { try { return value ? new URL(value, base).toString() : base || ''; } catch { return value || base || ''; } } function uid(prefix: string): string { return `${prefix}-${crypto.randomUUID()}`; } type GuidedInputKind = 'body' | 'body-field' | 'text' | 'header-field' | 'query-field' | 'custom'; function splitInputPath(path: string): { kind: GuidedInputKind; field: string } { if (path === 'body') return { kind: 'body', field: '' }; if (path === 'text') return { kind: 'text', field: '' }; if (path.startsWith('body.')) return { kind: 'body-field', field: path.slice(5) }; if (path.startsWith('headers.')) return { kind: 'header-field', field: path.slice(8) }; if (path.startsWith('query.')) return { kind: 'query-field', field: path.slice(6) }; return { kind: 'custom', field: path }; } function joinInputPath(kind: GuidedInputKind, field: string): string { if (kind === 'body') return 'body'; if (kind === 'text') return 'text'; if (kind === 'body-field') return `body.${field.trim()}`; if (kind === 'header-field') return `headers.${field.trim().toLowerCase()}`; if (kind === 'query-field') return `query.${field.trim()}`; return field.trim(); } function outputFieldLabel(kind: GuidedTransformOutputKind): string { if (kind === 'json-field') return 'JSON 字段名'; if (kind === 'form-field') return '表单字段名'; if (kind === 'header') return 'Header 名称'; if (kind === 'query') return 'Query 参数名'; return ''; } const INPUT_ROLE_LABELS: Record = { data: '明文数据', key: '密钥', iv: 'IV', algorithm: '算法', options: '选项', signature: '签名', salt: 'Salt', nonce: 'Nonce', aad: '附加数据', unknown: '页面参数', }; function toInput(profile: BrowserTransformProfile): BrowserTransformProfileInput { return { id: profile.id, name: profile.name, enabled: profile.enabled, target: { ...profile.target }, origin: profile.origin, match: { methods: [...profile.match.methods], urlPattern: profile.match.urlPattern }, request: structuredClone(profile.request), response: structuredClone(profile.response), failMode: 'closed', maxConcurrency: profile.maxConcurrency, }; } function profileFingerprint(profile?: BrowserTransformProfileInput): string { return profile ? JSON.stringify(profile) : ''; } function encodeUtf8(value: string): string { const bytes = new TextEncoder().encode(value); let binary = ''; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary); } function formatSampleBody(value: string): string { try { const parsed = JSON.parse(value) as unknown; return typeof parsed === 'object' && parsed !== null ? JSON.stringify(parsed, null, 2) : value; } catch { return value; } } function sampleHeaders(value: string): string { try { JSON.parse(value); return '{"Content-Type":"application/json"}'; } catch { return '{"Content-Type":"text/plain; charset=utf-8"}'; } } const DEFAULT_REPLAY_BODY = '{\n "value": "plain"\n}'; type ReplayPersistenceState = 'memory' | 'loading' | 'ready' | 'saving' | 'saved' | 'too-large' | 'error'; interface PendingReplaySave { key: string; fingerprint: string; input: BrowserTransformReplayDraftInput; revision: number; timeout: ReturnType; } function defaultReplayFields(tab?: ActiveTabInfo, selectedEvent?: BrowserRecordingEvent): BrowserTransformReplayDraftFields { return { method: selectedEvent?.method || 'POST', url: absoluteUrl(selectedEvent?.url, tab?.url), headers: '{"Content-Type":"application/json"}', body: DEFAULT_REPLAY_BODY, }; } function replayFieldsFingerprint(fields: BrowserTransformReplayDraftFields): string { return JSON.stringify(fields); } function replayPersistenceLabel(state: ReplayPersistenceState): string { if (state === 'memory') return '保存网关后自动保存'; if (state === 'loading') return '正在恢复'; if (state === 'ready') return '本机自动保存'; if (state === 'saving') return '正在保存'; if (state === 'saved') return '本机已保存'; if (state === 'too-large') return '样本过大'; return '保存失败'; } function nodeLabel(kind: BrowserTransformPipelineNode['kind']): string { if (kind === 'context.read') return '上下文'; if (kind === 'builtin') return '内置转换'; if (kind === 'page.call') return '页面函数'; return '输出'; } function callableKindLabel(callable: BrowserPageCallable): string { if (callable.kind === 'recorded-call') return '录制调用'; if (callable.kind === 'business-closure') return '业务闭包'; if (callable.kind === 'request-transaction') return '请求事务'; return '全局函数'; } function referencesOf(node: BrowserTransformPipelineNode): BrowserTransformNodeReference[] { if (node.kind === 'builtin') return node.inputs; if (node.kind === 'page.call') return node.arguments; if (node.kind === 'output.write') return [node.source]; return []; } export function BrowserTransformWorkspace({ tab, selectedEvent, busy, run, onOpenCapture, suggestion }: BrowserTransformWorkspaceProps) { const [profiles, setProfiles] = useState([]); const [callables, setCallables] = useState([]); const [selectedProfileId, setSelectedProfileId] = useState(''); const [draft, setDraft] = useState(); const [directionName, setDirectionName] = useState('request'); const [loadError, setLoadError] = useState(''); const [testMethod, setTestMethod] = useState('POST'); const [testUrl, setTestUrl] = useState(''); const [testHeaders, setTestHeaders] = useState('{"Content-Type":"application/json"}'); const [testBody, setTestBody] = useState(DEFAULT_REPLAY_BODY); const [testSample, setTestSample] = useState<{ body: string; label: string }>(); const [testResult, setTestResult] = useState(); const [testError, setTestError] = useState(''); const [replayPersistence, setReplayPersistence] = useState('memory'); const [replayStorageError, setReplayStorageError] = useState(''); const [replayLoadedKey, setReplayLoadedKey] = useState(''); const [editorMode, setEditorMode] = useState<'guided' | 'advanced'>('guided'); const [confirmDeleteCallableId, setConfirmDeleteCallableId] = useState(''); const handledSuggestion = useRef(0); const replayLoadRevision = useRef(0); const replaySaveRevision = useRef(0); const replayBaselineFingerprint = useRef(''); const replayStablePersistence = useRef('memory'); const pendingReplaySeed = useRef<{ key: string; fields: BrowserTransformReplayDraftFields } | undefined>(undefined); const pendingReplaySave = useRef(undefined); const replayProfileId = draft?.id || ''; const replayOrigin = draft?.origin || ''; const replayKey = replayProfileId ? `${replayProfileId}:${directionName}` : ''; const replayActiveKey = useRef(replayKey); replayActiveKey.current = replayKey; const workspaceMounted = useRef(false); const replayFields = useMemo(() => ({ method: testMethod, url: testUrl, headers: testHeaders, body: testBody, sample: testSample, }), [testBody, testHeaders, testMethod, testSample, testUrl]); const replayFingerprint = useMemo(() => replayFieldsFingerprint(replayFields), [replayFields]); const applyReplayFields = useCallback((fields: BrowserTransformReplayDraftFields) => { setTestMethod(fields.method); setTestUrl(fields.url); setTestHeaders(fields.headers); setTestBody(fields.body); setTestSample(fields.sample); setTestResult(undefined); setTestError(''); }, []); const discardPendingReplaySave = useCallback(() => { replaySaveRevision.current += 1; if (pendingReplaySave.current) clearTimeout(pendingReplaySave.current.timeout); pendingReplaySave.current = undefined; }, []); const flushPendingReplaySave = useCallback(() => { const pending = pendingReplaySave.current; if (!pending) return; clearTimeout(pending.timeout); pendingReplaySave.current = undefined; replaySaveRevision.current += 1; void saveBrowserTransformReplayDraft(pending.input).catch(() => undefined); }, []); const load = useCallback(async () => { try { const target = tab ? { tabId: tab.id, frameId: 0 } : undefined; const [nextProfiles, nextCallables] = await Promise.all([ request('transform.profile.list', target || {}), target ? request('callable.list', target).catch(() => []) : Promise.resolve([]), ]); setProfiles(nextProfiles); setCallables(nextCallables); setLoadError(''); setSelectedProfileId((current) => nextProfiles.some((profile) => profile.id === current) ? current : nextProfiles[0]?.id || ''); } catch (error) { setLoadError(errorMessage(error)); } }, [tab]); useEffect(() => { workspaceMounted.current = true; return () => { workspaceMounted.current = false; }; }, []); useEffect(() => { void load(); }, [load]); useEffect(() => { const selected = profiles.find((profile) => profile.id === selectedProfileId); if (selected) { setDraft(toInput(selected)); const selectedDirection = selected.request.enabled ? selected.request : selected.response; setEditorMode(parseGuidedTransform(selectedDirection, callables) ? 'guided' : 'advanced'); } }, [callables, profiles, selectedProfileId]); useEffect(() => { const revision = ++replayLoadRevision.current; discardPendingReplaySave(); setReplayLoadedKey(''); setReplayStorageError(''); const fallback = defaultReplayFields(tab, selectedEvent); if (!replayProfileId) { replayBaselineFingerprint.current = replayFieldsFingerprint(fallback); replayStablePersistence.current = 'memory'; applyReplayFields(fallback); setReplayPersistence('memory'); return; } const seed = pendingReplaySeed.current?.key === replayKey ? pendingReplaySeed.current.fields : undefined; if (seed) { pendingReplaySeed.current = undefined; replayBaselineFingerprint.current = ''; replayStablePersistence.current = 'ready'; applyReplayFields(seed); setReplayPersistence('ready'); setReplayLoadedKey(replayKey); return; } replayBaselineFingerprint.current = replayFieldsFingerprint(fallback); replayStablePersistence.current = 'ready'; applyReplayFields(fallback); setReplayPersistence('loading'); void getBrowserTransformReplayDraft(replayProfileId, directionName, replayOrigin) .then((stored) => { if (!workspaceMounted.current || replayLoadRevision.current !== revision) return; const fields = stored ? { method: stored.method, url: stored.url, headers: stored.headers, body: stored.body, sample: stored.sample, } : fallback; replayBaselineFingerprint.current = replayFieldsFingerprint(fields); replayStablePersistence.current = stored ? 'saved' : 'ready'; applyReplayFields(fields); setReplayPersistence(stored ? 'saved' : 'ready'); setReplayLoadedKey(replayKey); }) .catch((error) => { if (!workspaceMounted.current || replayLoadRevision.current !== revision) return; replayBaselineFingerprint.current = replayFieldsFingerprint(fallback); replayStablePersistence.current = 'error'; setReplayStorageError(`无法恢复本机回放草稿:${errorMessage(error)}`); setReplayPersistence('error'); setReplayLoadedKey(replayKey); }); return () => { replayLoadRevision.current += 1; }; // Replay defaults are captured only when the profile/direction changes. // Navigation and new recording selections must not overwrite an edited draft. // eslint-disable-next-line react-hooks/exhaustive-deps }, [replayKey]); useEffect(() => () => flushPendingReplaySave(), [flushPendingReplaySave, replayKey]); useEffect(() => { if (!replayProfileId || replayLoadedKey !== replayKey) return; if (replayFingerprint === replayBaselineFingerprint.current) { pendingReplaySave.current = undefined; setReplayPersistence(replayStablePersistence.current); return; } const revision = ++replaySaveRevision.current; setReplayPersistence('saving'); setReplayStorageError(''); const input: BrowserTransformReplayDraftInput = { ...structuredClone(replayFields), profileId: replayProfileId, direction: directionName, origin: replayOrigin, }; const pending: PendingReplaySave = { key: replayKey, fingerprint: replayFingerprint, input, revision, timeout: setTimeout(() => { if (pendingReplaySave.current === pending) pendingReplaySave.current = undefined; void saveBrowserTransformReplayDraft(input) .then((result) => { if (!workspaceMounted.current || replayActiveKey.current !== pending.key || replaySaveRevision.current !== revision) return; replayBaselineFingerprint.current = pending.fingerprint; replayStablePersistence.current = result.status === 'saved' ? 'saved' : 'too-large'; setReplayPersistence(replayStablePersistence.current); }) .catch((error) => { if (!workspaceMounted.current || replayActiveKey.current !== pending.key || replaySaveRevision.current !== revision) return; setReplayStorageError(`无法保存本机回放草稿:${errorMessage(error)}`); setReplayPersistence('error'); }); }, 350), }; pendingReplaySave.current = pending; return () => clearTimeout(pending.timeout); }, [directionName, replayFields, replayFingerprint, replayKey, replayLoadedKey, replayOrigin, replayProfileId]); useEffect(() => { if (!suggestion || !tab || handledSuggestion.current >= suggestion.revision) return; handledSuggestion.current = suggestion.revision; replayLoadRevision.current += 1; discardPendingReplaySave(); const sampleBody = suggestion.sampleBody ? formatSampleBody(suggestion.sampleBody) : undefined; const fields: BrowserTransformReplayDraftFields = { ...defaultReplayFields(tab, selectedEvent), method: suggestion.candidate.request.method || 'POST', url: absoluteUrl(suggestion.candidate.request.url, tab.url), headers: suggestion.sampleBody ? sampleHeaders(suggestion.sampleBody) : '{"Content-Type":"application/json"}', body: sampleBody || DEFAULT_REPLAY_BODY, sample: sampleBody ? { body: sampleBody, label: suggestion.sampleLabel || '录制短时样本' } : undefined, }; const seedKey = `${suggestion.profile.id}:request`; pendingReplaySeed.current = { key: seedKey, fields }; replayBaselineFingerprint.current = ''; replayStablePersistence.current = 'ready'; applyReplayFields(fields); setReplayLoadedKey(seedKey); setReplayPersistence('ready'); setReplayStorageError(''); setCallables((current) => [...current.filter((item) => item.id !== suggestion.callable.id), suggestion.callable]); setProfiles((current) => [suggestion.profile, ...current.filter((item) => item.id !== suggestion.profile.id)]); setSelectedProfileId(suggestion.profile.id); setDraft(toInput(suggestion.profile)); setDirectionName('request'); setEditorMode('guided'); setTestResult(undefined); setTestError(''); }, [applyReplayFields, discardPendingReplaySave, selectedEvent, suggestion, tab]); const savedProfile = profiles.find((profile) => profile.id === selectedProfileId); const dirty = Boolean(draft && profileFingerprint(draft) !== profileFingerprint(savedProfile ? toInput(savedProfile) : undefined)); const callableIds = useMemo(() => new Set(callables.map((callable) => callable.id)), [callables]); const referencedCallableIds = useMemo(() => draft ? [draft.request, draft.response] .flatMap((direction) => direction.enabled ? direction.nodes : []) .filter((node): node is Extract => node.kind === 'page.call') .map((node) => node.callableId) : [], [draft]); const bindingReady = Boolean(draft && originOf(tab?.url) === draft.origin && referencedCallableIds.every((id) => callableIds.has(id))); const callableReferences = useMemo(() => { const references = new Map(); for (const profile of profiles) { for (const node of [profile.request, profile.response].flatMap((item) => item.enabled ? item.nodes : [])) { if (node.kind !== 'page.call') continue; references.set(node.callableId, (references.get(node.callableId) || 0) + 1); } } return references; }, [profiles]); const direction = draft?.[directionName]; const guide = useMemo(() => direction ? parseGuidedTransform(direction, callables) : undefined, [callables, direction]); const guidedCallable = callables.find((callable) => callable.id === guide?.callableId); const guidedValid = Boolean(guide && guide.callableId && guide.inputPaths.every((path) => path.trim()) && (guide.outputKind === 'body' || guide.outputField.trim())); const replayLoading = replayPersistence === 'loading'; const replayPersistenceTitle = replayStorageError || (replayPersistence === 'too-large' ? '当前草稿超过 256 KiB,仅保留在本次页面中;已移除旧的本机副本,避免下次恢复过期内容。' : replayPersistence === 'memory' ? '保存明文网关后,回放输入会仅保存在当前浏览器中。' : '仅保存在当前浏览器,不会进入明文网关导出、Bridge、Yak 引擎或 AI 上下文。'); const selectProfile = (profile: BrowserTransformProfile) => { setSelectedProfileId(profile.id); setDraft(toInput(profile)); setDirectionName(profile.request.enabled ? 'request' : 'response'); setEditorMode(parseGuidedTransform(profile.request.enabled ? profile.request : profile.response, callables) ? 'guided' : 'advanced'); setTestResult(undefined); }; const create = () => { if (!tab) return; setSelectedProfileId(''); setDraft(createBrowserTransformProfileInput(tab, selectedEvent, callables[0])); setDirectionName('request'); setEditorMode('guided'); setTestResult(undefined); }; const patchDirection = (patcher: (value: BrowserTransformDirection) => BrowserTransformDirection) => { setDraft((current) => current ? { ...current, [directionName]: patcher(current[directionName]) } : current); setTestResult(undefined); }; const patchNode = (id: string, patch: Partial) => patchDirection((current) => ({ ...current, nodes: current.nodes.map((node) => node.id === id ? { ...node, ...patch } as BrowserTransformPipelineNode : node), })); const patchGuide = (next: GuidedTransformDraft) => { const callable = callables.find((item) => item.id === next.callableId); patchDirection(() => compileGuidedTransform(next, callable)); }; const selectGuidedCallable = (callableId: string) => { if (!guide) return; const callable = callables.find((item) => item.id === callableId); const defaults = defaultGuidedTransform(callable, { outputKind: guide.outputKind, outputField: guide.outputField }); patchGuide({ ...guide, callableId, inputPaths: defaults.inputPaths.map((path, index) => guide.inputPaths[index] || path), }); }; const patchGuideInput = (index: number, path: string) => { if (!guide) return; patchGuide({ ...guide, inputPaths: guide.inputPaths.map((item, itemIndex) => itemIndex === index ? path : item) }); }; const addNode = (kind: BrowserTransformPipelineNode['kind']) => patchDirection((current) => { const previous = current.nodes.at(-1); const reference = previous ? { nodeId: previous.id } : { nodeId: '' }; let node: BrowserTransformPipelineNode; if (kind === 'context.read') node = { id: uid('context'), name: '读取上下文', kind, path: 'body' }; else if (kind === 'builtin') node = { id: uid('builtin'), name: '转换数据', kind, operation: 'json.stringify', inputs: previous ? [reference] : [] }; else if (kind === 'page.call') node = { id: uid('call'), name: callables[0]?.name || '调用页面函数', kind, callableId: callables[0]?.id || '', arguments: previous ? [reference] : [] }; else node = { id: uid('output'), name: '写入输出', kind, destination: 'body', source: reference, encoding: 'auto' }; return { ...current, nodes: [...current.nodes, node] }; }); const patchReferences = (node: BrowserTransformPipelineNode, references: BrowserTransformNodeReference[]) => { if (node.kind === 'builtin') patchNode(node.id, { inputs: references }); else if (node.kind === 'page.call') patchNode(node.id, { arguments: references }); }; const save = () => run(async () => { if (!draft) return; const replaySeed = structuredClone(replayFields); const wasNew = !draft.id; const profile = await request('transform.profile.save', draft); if (wasNew) { const seedKey = `${profile.id}:${directionName}`; pendingReplaySeed.current = { key: seedKey, fields: replaySeed }; replayBaselineFingerprint.current = ''; replayStablePersistence.current = 'ready'; setReplayLoadedKey(seedKey); setReplayPersistence('ready'); } setProfiles((current) => [profile, ...current.filter((item) => item.id !== profile.id)]); setSelectedProfileId(profile.id); setDraft(toInput(profile)); }, 'Pipeline v2 配置已保存'); const remove = () => run(async () => { if (!draft?.id) { setDraft(undefined); return; } const removedId = draft.id; discardPendingReplaySave(); setReplayLoadedKey(''); const remaining = await request('transform.profile.delete', { id: removedId }); // The background service also removes these keys. Repeating the local // cleanup here serializes behind any Options-page write already in flight. await deleteBrowserTransformReplayDrafts(removedId).catch(() => undefined); setProfiles(remaining); setSelectedProfileId(remaining[0]?.id || ''); setDraft(remaining[0] ? toInput(remaining[0]) : undefined); }, '明文网关配置已删除'); const clearReplay = () => run(async () => { if (!draft?.id) return; const profileId = draft.id; const key = replayKey; replayLoadRevision.current += 1; discardPendingReplaySave(); setReplayLoadedKey(''); await clearBrowserTransformReplayDraft(profileId, directionName); if (replayActiveKey.current !== key) return; const fallback = defaultReplayFields(tab, selectedEvent); replayBaselineFingerprint.current = replayFieldsFingerprint(fallback); replayStablePersistence.current = 'ready'; applyReplayFields(fallback); setReplayStorageError(''); setReplayPersistence('ready'); setReplayLoadedKey(key); }, '当前方向的本机回放草稿已清空'); const deleteCallable = (callable: BrowserPageCallable) => run(async () => { const remaining = await request('callable.delete', { ...callable.target, callableId: callable.id }); setCallables(remaining); setConfirmDeleteCallableId(''); setTestResult(undefined); }, callableReferences.get(callable.id) ? '页面函数已删除,引用它的明文网关需要重新绑定' : '页面函数已删除'); const execute = async () => { if (!draft?.id || dirty) { setTestError('请先保存当前 Pipeline'); return; } setTestError(''); setTestResult(undefined); try { const parsed = JSON.parse(testHeaders) as unknown; if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('Header 必须是 JSON 对象'); const headers = Object.entries(parsed as Record).map(([name, value]) => ({ name, value: String(value) })); setTestResult(await request('transform.execute', { profileId: draft.id, direction: directionName, packet: { method: testMethod.toUpperCase(), url: testUrl, statusCode: directionName === 'response' ? 200 : undefined, headers, bodyBase64: encodeUtf8(testBody) }, })); } catch (error) { setTestError(errorMessage(error)); } }; return
{!draft ?
建立明文与线上报文的转换链路{callables.length ? : }
: <>
setDraft({ ...draft, name: event.target.value })} />{draft.origin} · document 生命周期
{(['request', 'response'] as const).map((name) => )}
{direction &&
{directionName === 'request' ? '明文 → 线上请求' : '线上响应 → 明文'}{editorMode === 'guided' ? '确认三个业务选择,底层 Pipeline 自动生成' : '直接编辑有序 DAG 与节点引用'}
patchDirection((current) => ({ ...current, enabled }))} />
{editorMode === 'guided' && (!guide ?
{direction.nodes.length ? '这条 Pipeline 包含高级结构' : '选择页面函数后自动生成'}{direction.nodes.length ? '高级结构不会被静默改写;可继续使用高级编辑,或明确替换成三步引导流程。' : '无需添加节点、引用或内置转换。'}
: guide &&
逻辑明文{guidedCallable?.name || '选择页面函数'}{guidedOutputDescription(guide)}
1
明文从哪里来通常选择整个逻辑 Body;多参数函数会逐项显示。
{guide.inputPaths.map((path, index) => { const source = splitInputPath(path); const slot = guidedCallable?.inputSlots.filter((item) => !item.retained)[index]; const needsField = !['body', 'text'].includes(source.kind); return
{needsField && }
; })} {!guide.inputPaths.length &&
这个页面函数不需要外部输入,将直接使用页面内保留的环境。
}
2
交给哪个页面函数函数在当前页面文档中执行,Key、IV 与闭包值不会离开页面。
{guidedCallable &&
{callableKindLabel(guidedCallable)}{guidedCallable.algorithm || guidedCallable.operation}{guidedCallable.inputSlots.filter((slot) => !slot.retained).length} 个明文参数
}
函数返回的是对象,需要取其中一个字段
3
线上请求写到哪里选择报文形态即可,字段组合与节点引用由插件生成。
{guide.outputKind !== 'body' && }
{guide.outputKind === 'form-field' && }
{guidedValid ? `将自动生成 ${direction.nodes.length} 个底层节点` : '补全页面函数、输入来源和输出字段后即可保存'}
)} {editorMode === 'advanced' && <>
高级 Pipeline节点、引用和白名单操作会直接影响线上报文;常规加解密场景建议使用引导配置。
{direction.nodes.map((node, index) => { const available = direction.nodes.slice(0, index); const references = referencesOf(node); return
{index + 1}{index < direction.nodes.length - 1 && }
{nodeLabel(node.kind)} patchNode(node.id, { name: event.target.value })} />
{node.kind === 'context.read' && } {node.kind === 'builtin' && <>{node.operation === 'value.literal' && }{['form.compose', 'object.compose'].includes(node.operation) && }{node.operation === 'object.pick' && <>}} {node.kind === 'page.call' && } {(node.kind === 'page.call' || (node.kind === 'builtin' && node.operation !== 'value.literal')) &&
输入引用{references.map((reference, referenceIndex) =>
patchReferences(node, references.map((item, itemIndex) => itemIndex === referenceIndex ? { ...item, path: event.target.value || undefined } : item))} placeholder="可选子路径" />
)}
} {node.kind === 'output.write' &&
}
; })}
添加节点
}
}
{bindingReady ? '当前页面函数可用' : '页面函数缺失或文档已变化'}
}