diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3ded9d0..ef44f89 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -126,7 +126,7 @@ The independent `page-recorder-main-world.js` entrypoint temporarily wraps user- Each click or submit begins a five-second business Trace. Nested and subsequent events share that Trace. Inputs and outputs are reduced to bounded evidence paths, byte lengths, encodings, and a randomly seeded 64-bit correlation fingerprint. The seed remains inside one page document and is regenerated for every document observer, so fingerprints cannot be compared across document boundaries. Matching an earlier output fingerprint to a later input fingerprint creates an exact Pipeline link. This is evidence of value equality inside one document segment, not proof of semantic causality. -Raw previews are disabled by default. Enabling them requires `browser.recording.sensitive.read` and caps each preview at 8 KiB. A user-started recording is a tab/frame-scoped Session: the current document keeps live hooks and handles, while the background merges bounded document segments into extension-only `storage.session`. A full navigation is recorded as a first-class Trace event; the previous segment is sealed, the destination document receives a new observer with the same Session identity and a synchronized global sequence, and recording continues until explicit stop, expiry, clear, or tab close. The single per-target Session is removed by a new recording, explicit clear, tab close, or browser-session end. Previews are never written to persistent storage or included in audit or AI request-analysis payloads. Recording is bounded to 500 aggregate events, 48 evidence items per side, 1,000 links, and 64 live callable handles per document. +Raw previews are disabled by default. Enabling them requires `browser.recording.sensitive.read` and caps each preview at 8 KiB. A user-started local recording covers every accessible frame in the selected tab and automatically attaches newly committed frames; each frame still owns an independent document Session, hook set, evidence salt, and callable registry, while the UI merges their bounded timelines. Agent-owned recordings remain explicitly frame-scoped. A full navigation is recorded as a first-class Trace event; the previous segment is sealed, the destination document receives a new observer with the same Session identity and a synchronized global sequence, and recording continues until explicit stop, expiry, clear, or tab close. Sessions are removed by a new recording, explicit clear, tab close, or browser-session end. Previews are never written to persistent storage or included in audit or AI request-analysis payloads. Recording is bounded to 500 aggregate events, 48 evidence items per side, 1,000 links, and 64 live callable handles per document. Navigation is both a business event and a strict execution-context boundary. Full document navigation, reload, browser Back/Forward, same-document History changes and fragment changes are distinguished. If Back/Forward restores the original document from BFCache, its recorder, handles and callables are resumed without clearing earlier evidence; if the browser performs a hard reload, the historical evidence remains but the destroyed closure heap is truthfully unavailable. MAIN-world lifecycle and the tab-scoped Session are separate states, so a temporary document transition no longer appears as a completed recording. A grant-owned recording remains document-bound and stops at navigation instead of silently extending an Agent's authority into a new document. diff --git a/README.md b/README.md index 928943a..a4d8c19 100644 --- a/README.md +++ b/README.md @@ -338,7 +338,9 @@ Chrome Store 构建声明 Chrome 138+。用户需要在扩展详情页开启“ https://aliyun-oss.yaklang.com/chrome-extension/manifest.json ``` -manifest 的 `latest` 指向最新版本,`versions[0]` 为完整记录,最多保留 10 个历史版本。每个版本按 `variant`(`chrome-store` / `chrome-enterprise` / `firefox` / `firefox-amo`)匹配 artifact,字段包括 `url`、`filename`、`sha256`、`size` 与 `checksum_url`;manifest 自身的 SHA-256 在同目录的 `manifest.json.sha256.txt`。 +manifest 的 `latest` 指向最新版本,`versions[0]` 为完整记录,最多保留 10 个历史版本。每个版本的 `notes` 是面向用户的真实更新说明,按 `variant`(`chrome-store` / `chrome-enterprise` / `firefox` / `firefox-amo`)匹配 artifact,字段包括 `url`、`filename`、`sha256`、`size` 与 `checksum_url`;manifest 自身的 SHA-256 在同目录的 `manifest.json.sha256.txt`。 + +每次修改 `package.json` 的版本时,必须同步在 `release-notes.json` 中增加该版本的更新说明;缺失或内容为空会让打包和发布直接失败。YTray 等消费方会原样展示这些说明。 推荐的消费流程: diff --git a/package.json b/package.json index aaa029d..0ea04ac 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "yakit-chrome-client", "description": "Yakit Browser Extension", "private": true, - "version": "0.2.6", + "version": "0.2.7", "type": "module", "packageManager": "pnpm@10.28.2", "scripts": { diff --git a/release-notes.json b/release-notes.json new file mode 100644 index 0000000..fa55392 --- /dev/null +++ b/release-notes.json @@ -0,0 +1,11 @@ +{ + "0.2.7": [ + "浏览器录制现在覆盖标签页内所有可访问页面,并自动接续后续加载的登录页面。", + "由 YTray 启动的托管浏览器会自动向 Yakit 发起配对连接。", + "修复代理绕过列表无法正常换行编辑的问题,并改善深色界面的选项显示。", + "插件更新页现在会展示随版本发布的真实更新内容。" + ], + "0.2.6": [ + "自动为用户和 Agent 建立安全的双向浏览器网关。" + ] +} diff --git a/scripts/build-manifest.mjs b/scripts/build-manifest.mjs index 598df33..b1b7d17 100644 --- a/scripts/build-manifest.mjs +++ b/scripts/build-manifest.mjs @@ -34,8 +34,13 @@ function artifactFingerprint(artifacts) { } function toVersionEntry(entry) { + if (!Array.isArray(entry.notes) || entry.notes.length === 0 + || entry.notes.some((note) => typeof note !== 'string' || note.trim() === '')) { + throw new Error(`release ${entry.version} must contain non-empty notes`); + } return { version: entry.version, + notes: entry.notes, published_at: entry.built_at, commit: entry.commit ?? null, artifacts: entry.artifacts.map((a) => ({ @@ -65,6 +70,11 @@ function validate(manifest) { if (!Array.isArray(versionEntry.artifacts) || versionEntry.artifacts.length === 0) { throw new Error(`version ${versionEntry.version} has no artifacts`); } + if (versionEntry.notes !== undefined + && (!Array.isArray(versionEntry.notes) || versionEntry.notes.length === 0 + || versionEntry.notes.some((note) => typeof note !== 'string' || note.trim() === ''))) { + throw new Error(`version ${versionEntry.version} has invalid notes`); + } const variants = new Set(); for (const artifact of versionEntry.artifacts) { if (variants.has(artifact.variant)) throw new Error(`duplicate variant ${artifact.variant} in version ${versionEntry.version}`); @@ -103,8 +113,10 @@ if (args['existing-manifest']) { const newEntry = toVersionEntry(entry); const idx = versions.findIndex((v) => v.version === entry.version); if (idx >= 0 && artifactFingerprint(versions[idx].artifacts) === artifactFingerprint(entry.artifacts)) { - // Idempotent rerun: keep the original entry (published_at stays stable). - console.log(`version ${entry.version} already in manifest with identical artifacts; kept as-is`); + // An idempotent rerun may backfill release notes without changing immutable + // artifacts or their original publication metadata. + versions[idx] = { ...versions[idx], notes: newEntry.notes }; + console.log(`version ${entry.version} already in manifest with identical artifacts; release notes synchronized`); } else { if (idx >= 0) { versions.splice(idx, 1); diff --git a/scripts/package-release.mjs b/scripts/package-release.mjs index cb40e95..6bf91bb 100644 --- a/scripts/package-release.mjs +++ b/scripts/package-release.mjs @@ -65,6 +65,11 @@ const distDir = resolve(root, String(args.dist ?? 'dist')); const pkg = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8')); const { version } = pkg; +const notesByVersion = JSON.parse(await readFile(resolve(root, 'release-notes.json'), 'utf8')); +const notes = notesByVersion[version]?.map((note) => String(note).trim()).filter(Boolean); +if (!Array.isArray(notes) || notes.length === 0) { + throw new Error(`release-notes.json must contain at least one note for version ${version}`); +} let commit = null; try { @@ -138,6 +143,6 @@ for (const target of VARIANTS) { console.log(`packaged ${filename} (${size} bytes, sha256 ${sha256.slice(0, 12)}…)`); } -const entry = { version, commit, built_at: new Date().toISOString(), artifacts }; +const entry = { version, notes, commit, built_at: new Date().toISOString(), artifacts }; await writeFile(resolve(distDir, 'release-entry.json'), `${JSON.stringify(entry, null, 2)}\n`); console.log(`release entry written: ${resolve(distDir, 'release-entry.json').slice(root.length + 1)} (version ${version})`); diff --git a/scripts/verify-public.mjs b/scripts/verify-public.mjs index 9267d7d..2198078 100644 --- a/scripts/verify-public.mjs +++ b/scripts/verify-public.mjs @@ -89,6 +89,8 @@ const manifest = JSON.parse(manifestBytes.toString('utf8')); assert(manifest.latest === entry.version, `manifest.latest ${manifest.latest} != ${entry.version}`); const versionEntry = manifest.versions.find((v) => v.version === entry.version); assert(versionEntry, `manifest has no entry for version ${entry.version}`); +assert(JSON.stringify(versionEntry.notes) === JSON.stringify(entry.notes), + `manifest release notes do not match release entry for ${entry.version}`); assert(versionEntry.artifacts.length === entry.artifacts.length, `manifest artifacts count ${versionEntry.artifacts.length} != ${entry.artifacts.length}`); for (const artifact of entry.artifacts) { diff --git a/src/app/background/handlers/recording.ts b/src/app/background/handlers/recording.ts index deb7a47..a50de56 100644 --- a/src/app/background/handlers/recording.ts +++ b/src/app/background/handlers/recording.ts @@ -3,11 +3,16 @@ import { ok } from '../response'; import { requiredDebuggerTarget, requiredRequestTarget } from '../request-context'; import { browserRecordingStatus, + clearTabBrowserRecording, clearBrowserRecording, createRecordedPageCallable, getBrowserRecording, + getTabBrowserRecording, + startTabBrowserRecording, startBrowserRecording, + stopTabBrowserRecording, stopBrowserRecording, + tabBrowserRecordingStatus, } from '@/features/browser-recording/service'; import { createCapturedPageCallable, @@ -35,7 +40,9 @@ export const handleRecordingRequest: BackgroundRequestHandler = async (request, case 'recording.start': { const input = request.payload; const target = await requiredRequestTarget(input, sender); - const snapshot = await startBrowserRecording(target, input); + const snapshot = input.scope === 'tab' + ? await startTabBrowserRecording(target.tabId, input) + : await startBrowserRecording(target, input); void appendAuditEvent({ category: 'capability', action: 'recording.start', @@ -45,18 +52,25 @@ export const handleRecordingRequest: BackgroundRequestHandler = async (request, }); return ok(snapshot); } - case 'recording.status': return ok(await browserRecordingStatus( - await requiredRequestTarget(request.payload, sender), - )); + case 'recording.status': { + const target = await requiredRequestTarget(request.payload, sender); + return ok(request.payload.scope === 'tab' + ? await tabBrowserRecordingStatus(target.tabId) + : await browserRecordingStatus(target)); + } case 'recording.get': { const target = await requiredRequestTarget(request.payload, sender); - const snapshot = await getBrowserRecording(target, request.payload.limit, true); + const snapshot = request.payload.scope === 'tab' + ? await getTabBrowserRecording(target.tabId, request.payload.limit, true) + : await getBrowserRecording(target, request.payload.limit, true); await stageBrowserProfileEvidence(snapshot); return ok(snapshot); } case 'recording.clear': { const target = await requiredRequestTarget(request.payload, sender); - const snapshot = await clearBrowserRecording(target, true); + const snapshot = request.payload.scope === 'tab' + ? await clearTabBrowserRecording(target.tabId, true) + : await clearBrowserRecording(target, true); void appendAuditEvent({ category: 'capability', action: 'recording.clear', @@ -67,7 +81,9 @@ export const handleRecordingRequest: BackgroundRequestHandler = async (request, } case 'recording.stop': { const target = await requiredRequestTarget(request.payload, sender); - const snapshot = await stopBrowserRecording(target, true); + const snapshot = request.payload.scope === 'tab' + ? await stopTabBrowserRecording(target.tabId, true) + : await stopBrowserRecording(target, true); await stageBrowserProfileEvidence(snapshot); void appendAuditEvent({ category: 'capability', diff --git a/src/app/background/index.ts b/src/app/background/index.ts index 7246862..995eaa5 100644 --- a/src/app/background/index.ts +++ b/src/app/background/index.ts @@ -441,8 +441,12 @@ async function handleRequest(request: ExtensionRequest, sender: Browser.runtime. await syncManagedInstanceBadge(state.bridge.managedInstance); if (state.bridge.autoConnect && state.bridge.pairedEngine) { engineBridge.disconnect(); - await stopPairedBrowserTasks(); - await engineBridge.connect(state.bridge); + void stopPairedBrowserTasks() + .then(() => engineBridge.connect(state.bridge)) + .catch((error) => console.error('Managed browser reconnect failed', error)); + } else if (!state.bridge.pairedEngine) { + void engineBridge.startPairing() + .catch((error) => console.error('Managed browser pairing failed', error)); } return ok(engineBridge.getStatus()); } diff --git a/src/entrypoints/agent.content/index.ts b/src/entrypoints/agent.content/index.ts index 7dc53d9..30d414b 100644 --- a/src/entrypoints/agent.content/index.ts +++ b/src/entrypoints/agent.content/index.ts @@ -63,6 +63,7 @@ async function send(action: string, payload?: unknown): Promise { export default defineContentScript({ matches: ['http://*/*', 'https://*/*'], + allFrames: true, runAt: 'document_start', async main(ctx) { @@ -77,6 +78,7 @@ export default defineContentScript({ console.warn('[Yakit Browser Agent] MAIN-world bridge is unavailable; continuing without page Eval/Invoke.', error); }); } + if (window.top !== window) return; const host = document.createElement('yakit-browser-agent'); const shadow = host.attachShadow({ mode: 'open' }); diff --git a/src/features/browser-recording/RecordingWorkspace.tsx b/src/features/browser-recording/RecordingWorkspace.tsx index 9a1e71b..f4ccfc8 100644 --- a/src/features/browser-recording/RecordingWorkspace.tsx +++ b/src/features/browser-recording/RecordingWorkspace.tsx @@ -9,7 +9,7 @@ import { Switch } from '@/components/ui/switch'; import { errorMessage, request } from '@/platform/messaging/runtime'; import type { ActiveTabInfo, BrowserPageCallable, BrowserPageCallableExecution, BrowserRecordingEvent, - BrowserProfileInferenceCandidate, BrowserRecordingArgumentRole, BrowserRecordingSnapshot, + BrowserProfileInferenceCandidate, BrowserRecordingArgumentRole, BrowserRecordingSnapshot, BrowserTarget, } from '@/types/models'; import type { CapturedCallableSample } from '@/features/deep-capture/callable-sample'; import { DeepCaptureWorkspace } from '@/features/deep-capture/DeepCaptureWorkspace'; @@ -167,13 +167,6 @@ function navigationPhaseLabel(event: BrowserRecordingEvent): string { return '浏览器文档边界'; } -function emptySnapshot(tabId: number): BrowserRecordingSnapshot { - return { - status: { active: false, target: { tabId, frameId: 0 }, documentAvailable: true, count: 0, droppedCount: 0 }, - events: [], traces: [], links: [], callables: [], profileCandidates: [], - }; -} - function shortSample(event?: BrowserRecordingEvent): string | undefined { const value = event?.inputPreview || event?.inputs.find((item) => item.preview)?.preview; return value?.trim() || undefined; @@ -181,14 +174,21 @@ function shortSample(event?: BrowserRecordingEvent): string | undefined { function eventAvailableInDocument( event: BrowserRecordingEvent | undefined, - currentDocumentId: string | undefined, + target: BrowserTarget | undefined, documentAvailable: boolean, ): boolean { return documentAvailable && Boolean(event) && ( - !event?.documentId || !currentDocumentId || event.documentId === currentDocumentId + (event?.frameId === undefined || !target || event.frameId === target.frameId) + && (!event?.documentId || !target?.documentId || event.documentId === target.documentId) ); } +function recordingEventTarget(tabId: number | undefined, event?: BrowserRecordingEvent): BrowserTarget | undefined { + return tabId !== undefined && event + ? { tabId, frameId: event.frameId ?? 0, documentId: event.documentId } + : undefined; +} + export function RecordingWorkspace({ tab, busy, @@ -224,11 +224,8 @@ export function RecordingWorkspace({ return; } try { - const target = { tabId, frameId: 0 }; - const status = await request('recording.status', target); - const next = status.startedAt - ? await request('recording.get', { ...target, limit: 500 }) - : emptySnapshot(tabId); + const target = { tabId, frameId: 0, scope: 'tab' as const }; + const next = await request('recording.get', { ...target, limit: 500 }); setSnapshot(next); if (next.status.options) setCaptureValues(next.status.options.captureValues); setLoadError(''); @@ -272,8 +269,8 @@ export function RecordingWorkspace({ const selectedEvent = snapshot?.events.find((event) => event.id === selectedEventId); const selectedCallable = snapshot?.callables.find((callable) => callable.id === selectedCallableId); const recordingTarget = tab ? { tabId: tab.id, frameId: 0 } : undefined; - const documentAvailable = snapshot?.status.documentAvailable !== false; - const callableTarget = snapshot?.status.startedAt && documentAvailable ? snapshot.status.target : undefined; + const tabRecordingTarget = recordingTarget ? { ...recordingTarget, scope: 'tab' as const } : undefined; + const selectedEventTarget = recordingEventTarget(tab?.id, selectedEvent); useEffect(() => { if (!selectedEvent) return; @@ -292,7 +289,7 @@ export function RecordingWorkspace({ const start = () => run(async () => { if (!tab) throw new Error('请选择目标标签页'); const next = await request('recording.start', { - tabId: tab.id, captureValues, maxEntries: 500, maxValueBytes: 8_192, + tabId: tab.id, scope: 'tab', captureValues, maxEntries: 500, maxValueBytes: 8_192, }); setSnapshot(next); setSelectedTraceId(''); @@ -300,16 +297,16 @@ export function RecordingWorkspace({ setCallableResult(undefined); setPendingGatewayBinding(undefined); setCaptureCandidate(undefined); - }, captureValues ? '录制已开始;短时样本仅保留在本次浏览器会话,页面跳转后会自动接续' : '录制已开始,将跨页面记录业务执行链'); + }, captureValues ? '录制已开始;已覆盖标签页内所有页面,短时样本仅保留在本次浏览器会话' : '录制已开始;已覆盖标签页内所有页面与后续登录 frame'); const stop = () => run(async () => { - if (!recordingTarget) return; - setSnapshot(await request('recording.stop', recordingTarget)); + if (!tabRecordingTarget) return; + setSnapshot(await request('recording.stop', tabRecordingTarget)); }, '录制已停止,可以继续验证页面函数'); const clear = () => run(async () => { - if (!recordingTarget) return; - setSnapshot(await request('recording.clear', recordingTarget)); + if (!tabRecordingTarget) return; + setSnapshot(await request('recording.clear', tabRecordingTarget)); setCallableResult(undefined); setPendingGatewayBinding(undefined); setCaptureCandidate(undefined); @@ -317,11 +314,11 @@ export function RecordingWorkspace({ const createCallable = () => run(async () => { if (snapshot?.status.active) throw new Error('请先停止录制,再保存页面函数'); - if (!selectedEventAvailable || !callableTarget || !selectedEvent?.callHandleId) { + if (!selectedEventAvailable || !selectedEventTarget || !selectedEvent?.callHandleId) { throw new Error(selectedEvent ? '该调用属于另一个页面文档;返回对应页面现场后才能保存' : '当前事件没有可执行调用句柄'); } const callable = await request('callable.create', { - ...callableTarget, source: 'recording', callHandleId: selectedEvent.callHandleId, name: callableName, + ...selectedEventTarget, source: 'recording', callHandleId: selectedEvent.callHandleId, name: callableName, }); setSnapshot((current) => current ? { ...current, callables: [...current.callables.filter((item) => item.id !== callable.id), callable] } : current); setSelectedCallableId(callable.id); @@ -330,17 +327,23 @@ export function RecordingWorkspace({ }, '页面函数已创建'); const executeCallable = () => run(async () => { - if (!callableTarget || !selectedCallable) throw new Error(documentAvailable ? '请选择页面函数' : '页面已经导航,旧文档的页面函数不可再执行'); + if (!selectedCallable) throw new Error('请选择页面函数'); let args: unknown; try { args = JSON.parse(callableArguments); } catch { throw new Error('调用参数必须是有效的 JSON 数组'); } if (!Array.isArray(args)) throw new Error('调用参数必须是 JSON 数组'); - setCallableResult(await request('callable.execute', { ...callableTarget, callableId: selectedCallable.id, args })); + setCallableResult(await request('callable.execute', { ...selectedCallable.target, callableId: selectedCallable.id, args })); }, '页面函数验证完成'); const deleteCallable = () => run(async () => { - if (!callableTarget || !selectedCallable) return; - const callables = await request('callable.delete', { ...callableTarget, callableId: selectedCallable.id }); - setSnapshot((current) => current ? { ...current, callables } : current); + if (!selectedCallable) return; + const callables = await request('callable.delete', { ...selectedCallable.target, callableId: selectedCallable.id }); + setSnapshot((current) => current ? { + ...current, + callables: [ + ...current.callables.filter((item) => item.target.frameId !== selectedCallable.target.frameId), + ...callables, + ], + } : current); setCallableResult(undefined); }, '页面函数已删除'); @@ -370,8 +373,7 @@ export function RecordingWorkspace({ ? `页面函数句柄 ${(snapshot.status.retainedCallBytes / 1024).toFixed(1)} KiB` : undefined, ].filter(Boolean).join(' · '); - const currentDocumentId = snapshot?.status.target.documentId; - const selectedEventAvailable = eventAvailableInDocument(selectedEvent, currentDocumentId, documentAvailable); + const documentAvailable = snapshot?.status.documentAvailable !== false; const outgoingLinks = selectedEvent ? snapshot?.links.filter((link) => link.fromEventId === selectedEvent.id) || [] : []; const incomingLinks = selectedEvent ? snapshot?.links.filter((link) => link.toEventId === selectedEvent.id) || [] : []; const traceCandidates = snapshot?.profileCandidates.filter((candidate) => candidate.traceId === selectedTraceId) || []; @@ -382,6 +384,11 @@ export function RecordingWorkspace({ : undefined; const boundaryCandidates = selectedCandidate ? [] : traceCandidates.filter((candidate) => candidate.request.eventId === selectedEventId); const relatedCandidate = boundaryCandidates.length === 1 ? boundaryCandidates[0] : undefined; + const selectedEventAvailable = eventAvailableInDocument( + selectedEvent, + selectedCandidate?.target || relatedCandidate?.target || selectedEventTarget, + selectedEvent?.frameId === 0 ? documentAvailable : true, + ); const relatedSourceEvent = relatedCandidate ? snapshot?.events.find((event) => event.id === relatedCandidate.source.eventId) : undefined; @@ -394,7 +401,7 @@ export function RecordingWorkspace({ const candidateSourceEvent = selectedCandidate ? snapshot?.events.find((event) => event.id === selectedCandidate.source.eventId) : undefined; - const candidateAvailable = eventAvailableInDocument(candidateSourceEvent, currentDocumentId, documentAvailable); + const candidateAvailable = eventAvailableInDocument(candidateSourceEvent, selectedCandidate?.target, true); const canDeepCapture = DEEP_CAPTURE_AVAILABLE && selectedEventAvailable && Boolean(selectedEvent && ['crypto', 'fetch', 'xhr', 'form', 'beacon', 'worker', 'message'].includes(selectedEvent.kind) && (selectedEvent.url || selectedEvent.wrapperHandleId)); @@ -406,8 +413,8 @@ export function RecordingWorkspace({ return; } void run(async () => { - if (!recordingTarget) throw new Error('目标标签页不可用'); - setSnapshot(await request('recording.stop', recordingTarget)); + if (!tabRecordingTarget) throw new Error('目标标签页不可用'); + setSnapshot(await request('recording.stop', tabRecordingTarget)); setCallableEditorOpen(true); }, '录制已停止,请确认页面函数名称'); }; @@ -454,7 +461,7 @@ export function RecordingWorkspace({ throw new Error(`已检测到${pair.direction === 'request' ? '请求' : '响应'}方向,但${candidateStatusLabel(pair)},不能静默保存为单向网关`); } const pairEvent = snapshot?.events.find((item) => item.id === pair.source.eventId); - if (!eventAvailableInDocument(pairEvent, currentDocumentId, documentAvailable) || !snapshot?.status.target) { + if (!snapshot || !eventAvailableInDocument(pairEvent, pair.target, true)) { throw new Error('配对方向属于另一个页面文档,请返回对应页面现场后再生成'); } const pairInputCount = pair.source.dynamicInputPaths?.length || 1; @@ -463,7 +470,7 @@ export function RecordingWorkspace({ if (!pairCallable) { if (!pair.source.callHandleId) throw new Error('配对方向没有可复用的页面调用句柄'); pairCallable = await request('callable.create', { - ...snapshot.status.target, + ...pair.target, source: 'recording', callHandleId: pair.source.callHandleId, name: `${pair.source.crypto?.algorithm || pair.source.crypto?.operation || pair.source.operation} 页面函数`, @@ -507,16 +514,16 @@ export function RecordingWorkspace({ if (candidate.sources.length !== 1) { throw new Error('多调用请求需要先捕获上层业务函数,不能把相互依赖的低层调用拆开回放'); } - if (!candidateAvailable || !recordingTarget || !candidate.source.callHandleId) { + if (!candidateAvailable || !tabRecordingTarget || !candidate.source.callHandleId) { throw new Error(candidateAvailable ? '推断候选没有可复用的页面调用句柄' : '该函数属于另一个页面文档,请返回对应页面现场后再生成'); } let currentSnapshot = snapshot; if (currentSnapshot?.status.active) { - currentSnapshot = await request('recording.stop', recordingTarget); + currentSnapshot = await request('recording.stop', tabRecordingTarget); setSnapshot(currentSnapshot); } if (!currentSnapshot) throw new Error('没有可用的录制现场'); - const target = currentSnapshot.status.target; + const target = candidate.target; if (!target) throw new Error('录制文档已经失效'); const inputCount = candidate.source.dynamicInputPaths?.length || 1; let callable = currentSnapshot.callables.find((item) => item.provenance.eventId === candidate.source.eventId @@ -550,7 +557,7 @@ export function RecordingWorkspace({