diff --git a/README.md b/README.md
index 6b62520..0aa9017 100644
--- a/README.md
+++ b/README.md
@@ -90,6 +90,7 @@ Rule of thumb: if you build and operate your own agent in production, use a trac
- **Session insights** — Aggregate analytics dashboard with tool stats, error clustering and daily trends
- **Evidence-backed failure events (React UI)** — Groups unresolved failures by the same tool, complete arguments and call's user turn, with repeated operations first, first/last evidence jumps and every original result retained. Successful results split groups; missing arguments stay separate. Execution success requires an explicit zero exit code or OMP-native completion evidence. These are review groups, not root-cause diagnoses or proof of task failure. Local rules, no LLM. [Try the synthetic walkthrough and read the boundaries](docs/diagnostics.md).
- **Local review queue** — Record follow-up, expected-failure or alternative-verification notes in your browser. Evidence changes invalidate the old review; manual labels never rewrite automatic outcomes. No account or review backend. [Review workflow and storage limits](docs/diagnostics.md#local-review-workflow).
+- **Review portability** — Preview and download current-session review notes, then import only exact evidence matches into empty local slots. Existing notes are never overwritten; stale/unmatched records are skipped. JSON files are unencrypted and contain your written notes, not automatically copied logs. [Transfer limits](docs/diagnostics.md#transfer-reviews-between-browsers).
- **Narrow-screen session workflow** — Below 768px, switch between the session list and full-width content without losing the current review draft; platform tabs scroll horizontally, and evidence jumps keep navigation visible. Desktop retains the two-column layout. [Scope and tested viewports](docs/diagnostics.md#narrow-screen-session-workflow).
- **Spawn tracking** — Detect and navigate parent/child agent relationships
- **OMP sub-agents** — Sub-agents spawned by an OMP session show up as chips in the summary; click one to read the child agent's full transcript
diff --git a/README.zh-CN.md b/README.zh-CN.md
index df07385..2597c67 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -56,6 +56,7 @@ LangSmith、Langfuse 这类观测平台面向的是*你自己写的* agent:接
- **有证据的失败事件(React UI)** — 将同一调用所在用户轮次、同工具、完整同参数的待复查失败分组,重复最多的操作优先展示;可跳转首末及每条原始证据。同参成功切断分组,缺少参数不合并。执行成功采用明确零退出码或 OMP 原生完成证据;事件不等于根因或任务失败。本地规则,无 LLM。[合成演示与判定边界](docs/diagnostics.md#中文使用指南)。
- **本机复核队列** — 用必填依据标记“需跟进”“预期失败”“其他验证已通过”,仅存当前浏览器;新证据使旧标记失效,人工判断不改写自动结果。无需账号或复核后端。[使用方式与存储边界](docs/diagnostics.md#本机复核闭环)。
+- **复核迁移** — 预览并导出当前会话的有效复核,导入只接受完整证据匹配且本地为空的记录;不覆盖已有笔记,跳过过期或不匹配记录。明文 JSON 含手写依据,不自动复制日志。[迁移边界](docs/diagnostics.md#迁移复核记录)。
- **窄屏会话复核** — 小于 768px 时切换“会话列表 / 返回内容”,正文获得完整宽度,切换列表不清空当前复核草稿;平台栏横向滚动,证据跳转保留顶部导航,桌面继续双栏。[验收范围](docs/diagnostics.md#窄屏操作)。
- **多平台支持** — 一个界面统一查看 OpenClaw、Codex、Claude Code、Hermes、OMP、DeepSeek Harness、Gemini CLI 的会话日志(dsh 的多帧 zstd 压缩日志透明解压;Gemini CLI 的 `/rewind` 回滚记录会先折叠,回滚掉的历史不会重复渲染)
diff --git a/claims.json b/claims.json
index 00cd42b..17ffc6c 100644
--- a/claims.json
+++ b/claims.json
@@ -102,16 +102,16 @@
},
{
"id": "test-count",
- "claim": "216 tests pass on Node's built-in test runner, the count docs/ROADMAP.md records for `npm test`.",
- "value": "216",
- "metric": "passing node:test cases (# tests 216 / # pass 216 / # fail 0)",
- "method": "npm test → node --test test/*.test.js, run in the claims job after npm ci, and the TAP summary is asserted. The roadmap sentence ('216 tests on Node's built-in runner (`npm test`, 2026-09-23)') is verified by the run, not read back from the prose.",
+ "claim": "233 tests pass on Node's built-in test runner, the count docs/ROADMAP.md records for `npm test`.",
+ "value": "233",
+ "metric": "passing node:test cases (# tests 233 / # pass 233 / # fail 0)",
+ "method": "npm test → node --test test/*.test.js, run in the claims job after npm ci, and the TAP summary is asserted. The roadmap sentence ('233 tests on Node's built-in runner (`npm test`, 2026-09-23)') is verified by the run, not read back from the prose.",
"repro": "npm test 2>&1 | grep -E '^# (tests|pass|fail)'",
"evidence": "docs/ROADMAP.md",
"as_of": "2026-09-13",
"check": {
"cmd": "npm test 2>&1 | grep -E '^# (tests|pass|fail)'",
- "expect": { "contains": ["# tests 216", "# pass 216", "# fail 0"] },
+ "expect": { "contains": ["# tests 233", "# pass 233", "# fail 0"] },
"timeout": 120
}
},
@@ -238,7 +238,7 @@
"check": {
"cmd": "node scripts/claims-receipts.mjs tests-node-only",
"expect": {
- "equals": "16 files in test/ · 14 distinct requires: 11 node builtins, 3 relative, 0 third-party"
+ "equals": "17 files in test/ · 14 distinct requires: 11 node builtins, 3 relative, 0 third-party"
},
"timeout": 60
}
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index e9ad4e7..4c9894e 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -8,7 +8,7 @@
- **Session browser** with tool-call inspection, trace/waterfall view, spawn tracking and message timeline
- **Prompt tooling** — extraction (noise filtered), template clustering with outcome attribution, Claude-powered rewrites, and a prompt library that installs entries as native slash commands
- **Global search** across all platforms, insights dashboard, incremental session backup
-- **React + Vite frontend** served by an Express backend; 216 tests on Node's built-in runner (`npm test`, 2026-09-23), CI on Node 22
+- **React + Vite frontend** served by an Express backend; 233 tests on Node's built-in runner (`npm test`, 2026-09-23), CI on Node 22
- **Evidence-backed failure events and local review** with full-result invalidation, evidence navigation and narrow-screen session layout
## Current priorities
@@ -18,7 +18,7 @@ The product direction is **review the coding-agent sessions you already have, wi
| Priority | Outcome | Acceptance, not a promise |
| --- | --- | --- |
| P0 | Make the new workflow immediately testable | A demo-only entry opens a clearly synthetic case: 7 pending records in 2 events, all evidence accessible, local review does not rewrite automatic results. Preserve the existing default demo and samples. |
-| P1 | Make review work portable without exposing logs | Design explicit user-controlled review export/import, validate schema and evidence fingerprints, show a preview of exactly what leaves the browser, and reject stale/mismatched records. No automatic upload or raw-log export by default. |
+| P1 | Current-session review portability implemented | Preview-only import and explicit plaintext download; exact identity/evidence matching, no overwrites, bounded schema and partial-failure reporting. Validate with synthetic migration and publish after CI; whole-history backup and path remapping remain out of scope. |
| P2 | Validate daily usefulness with the maintainer's own sessions | Record reviewed/follow-up/expected/alternative-verification counts and timed review tasks using a fixed rubric. Keep measurements local, separate unknowns and stale labels, and publish only consented aggregate evidence. Do not infer precision or time saved from event compression. |
| P3 | Make releases reproducible for contributors | Keep clean-install tests, generated fixtures, documentation claims and release/package verification aligned. Add browser regression automation when it can run deterministically without personal logs. |
diff --git a/docs/diagnostics-verification.md b/docs/diagnostics-verification.md
index 63ba24d..94ecc64 100644
--- a/docs/diagnostics-verification.md
+++ b/docs/diagnostics-verification.md
@@ -67,3 +67,19 @@ A separate hosted-demo increment adds the explicitly synthetic `0199demo-diagnos
The hosted sample has 8 historical failures: one has a matching successful retry, leaving 7 pending records grouped into 2 events (6 edit failures and 1 nested search failure). A background start stays running. Six individual edit results, evidence jumps and browser-local review were verified; no backend API requests were made during the static walkthrough. Fixture regeneration produced identical bytes twice.
The demo-only guide is height-limited on short screens. Expanded guide checks leave about 102px for messages at 360×640 and 224px at 360×800; at 740×360 landscape only 35px remains, so portrait use is recommended. This does not change the normal local application's layout or imply physical-device certification.
+
+## Review portability acceptance
+
+The next increment adds explicit current-session export/import, not whole-history backup. Run `node --test test/review-transfer.test.js` for its 17 cases; the combined focused diagnostic/review suite contains 103 tests and the full suite contains 233 tests. Earlier counts above record earlier releases, not the current suite size.
+
+Verified using synthetic notes and isolated browser contexts only:
+
+- Actual JSON download contains only allowed hash/status/note/time fields. Unrelated browser storage, raw tool output, command arguments and paths are not copied. Acknowledgment is required before downloading or importing.
+- Two notes migrate to an empty browser after a no-write preview and persist after refresh. Re-import is classified as identical and skipped; a different local note is a conflict and retained.
+- An HTML-shaped note displays literally and does not execute. Unknown fields, malformed JSON, duplicate records, invalid identifiers, files over 1 MiB and lists over 500 records are rejected.
+- Adding a failure while a preview is open disables confirmation. Re-preview imports the one still-matching event and skips the stale one. A different session skips both records.
+- An independent `localhost` origin accepts the matching review exported on `127.0.0.1`; another tab writing first invalidates the preview and preserves that local note.
+- A simulated quota failure on the second write reports **1 imported / 0 skipped / 1 failed**. Re-preview reports **1 imported / 1 skipped / 0 failed** on retry, preserving the first import. No atomic rollback is claimed.
+- Browser migration sent zero backend write requests; automatic failure/recovery counts remained unchanged. At 360px, the transfer panel had no horizontal overflow.
+
+This does not verify real-device file pickers, simultaneous cross-tab transaction safety or actual productivity gains. The file is unencrypted human text, not signed evidence; only current exact matches can be restored. Detailed local run artifacts remain ignored and are not published.
diff --git a/docs/diagnostics.md b/docs/diagnostics.md
index f5ad26f..52262fe 100644
--- a/docs/diagnostics.md
+++ b/docs/diagnostics.md
@@ -60,7 +60,29 @@ Storage limits:
- They are not encrypted, authenticated attestations or a backup. Another browser, a changed hostname/port, private mode or cleared site data may make them unavailable. Other tabs use the latest saved record; there is no collaborative edit merge.
- Storage denial, a corrupt record or quota exhaustion is shown explicitly. A failed save does not count as saved; unreadable reviews remain unreviewed. You can revoke an individual review without clearing unrelated settings.
- Use localhost or HTTPS for browser cryptography. If fingerprints cannot be calculated, saving reviews is disabled while automatic diagnostics remain available.
-- Notes are not automatically deleted when an event becomes automatically recovered or its identity changes; they may remain in site storage. There is no review archive, export/import or cross-device sync in this version.
+- Notes are not automatically deleted when an event becomes automatically recovered or its identity changes; they may remain in site storage. Current-event export/import is available below, but there is no whole-history review archive or automatic cross-device sync.
+
+## Transfer reviews between browsers
+
+Open **迁移当前会话复核 / Export & import** in the diagnostics panel. This is a local, explicit file transfer—not a server backup or authenticated proof that a task passed.
+
+1. On the source browser, open the session you reviewed and select **预览导出**. The full JSON is shown before downloading. Only reviews whose identity and full-evidence fingerprint still match the **currently loaded automatic events** are included; the selected review filter does not limit the export.
+2. Inspect the notes, acknowledge the plaintext warning, then select **下载复核 JSON**. The filename is always `agentxray-reviews.json`, without a session ID or path. Store it safely: notes you typed may include secrets even though the app does not copy logs into the file.
+3. On the destination browser/origin, open the same session with the same platform, configured log-directory setting and child scope. Select the JSON file. **No review is written during preview.** Every record shows its note, timestamp and one decision: import, unmatched, stale, already identical, or local conflict.
+4. After checking the preview, acknowledge the warning and select **确认导入匹配记录**. Only exact current-event/full-evidence matches with an empty local slot are written. Every existing local slot is kept—even an old or corrupt one. Clear it explicitly in the ordinary review UI first if you intend to replace it, then preview again.
+5. The result reports imported, skipped and failed counts. Evidence/review changes invalidate an open preview. Storage is rechecked immediately before each write; partial failures retain successful writes and report failures, not an all-or-nothing transaction. Re-preview to retry; already-imported records are skipped.
+
+### File format and boundaries
+
+- Format `agentxray-review-transfer`, version `1`; maximum **1 MiB UTF-8** and **500 unique records**. Invalid JSON, unknown fields/versions, duplicate keys in the record list, invalid storage identifiers and invalid review fields are rejected before import writes.
+- Top-level fields: `format`, `version`, `exportedAt`, `records`. Each record has `storageKey` and `record`; the nested record has `version`, `fingerprint`, `status`, `note`, `reviewedAt`.
+- Only hash identifiers, evidence fingerprints, human states, human-written notes and timestamps are exported. Raw commands, log content, paths, session titles and automatic diagnostics are not copied into the file. Notes are plain text, not redacted or encrypted; HTML in a note is displayed as text.
+- Matching preserves the existing fingerprint contract. Different browser or port can work; changed directory settings, session/child scope, first occurrence, original arguments or full evidence do not. There is no fuzzy cross-machine path remapping. If source logs are no longer available at the destination, the file cannot recreate them.
+- Files can be edited or fabricated. A matching hash is an association check, **not a signature, trust guarantee or independent task verification**. Imported human labels never modify automatic failure/recovery counts.
+- This is not a complete browser/history backup: recovered, missing, stale or unloaded event notes are excluded. Unreadable current records abort export with an error rather than silently producing an incomplete file. Exceeding limits requires a smaller current-session set; automatic splitting is not provided.
+- Browser localStorage has no multi-tab transaction. The app skips observed conflicts and rechecks each write, but cannot guarantee a lock against truly simultaneous independent writes. No remote requests or automatic cloud sync are added.
+
+
## What an event means
@@ -99,6 +121,7 @@ These checks cover navigation, session messages and the review workflow—not co
```sh
node --test test/diagnostic-events.test.js test/diagnostics.test.js test/omp-outcome.test.js
node --test test/diagnostic-reviews.test.js
+node --test test/review-transfer.test.js
npm test
npm run build:ui
npm run lint
@@ -141,7 +164,20 @@ The local frozen regression set contained 30 sessions and 9,076 tool results. Gr
**存储边界:**标记只在当前浏览器同源 localStorage 中保存,刷新可恢复、同源多标签页会同步,但不上传后端、不改原始日志。自动保存的内容为哈希标识、完整证据指纹、状态、时间和你手写的依据,不复制原始日志或参数;手写依据是明文,请勿填写密钥。不同日志目录、平台、会话及子会话隔离。换浏览器、端口、清除站点数据可能不可见或丢失,不能当备份;多标签页采用最后一次保存,不做协作合并。
-新增失败、完整输出变化或状态依据变化会要求重新复核;身份变化的事件不沿用旧标记。读取损坏、浏览器拒绝存储或配额不足会明确报错,不会假装保存成功。使用 localhost 或 HTTPS 以便计算指纹。自动恢复或身份变化后的旧笔记不会自动清理;当前没有复核归档、导入导出或跨设备同步。
+新增失败、完整输出变化或状态依据变化会要求重新复核;身份变化的事件不沿用旧标记。读取损坏、浏览器拒绝存储或配额不足会明确报错,不会假装保存成功。使用 localhost 或 HTTPS 以便计算指纹。自动恢复或身份变化后的旧笔记不会自动清理;可迁移当前有效复核,但不是历史归档或自动跨设备同步。
+
+## 迁移复核记录
+
+在诊断面板展开“迁移当前会话复核 / Export & import”:
+
+1. **源浏览器**打开已有复核的会话,点击“预览导出”,检查将下载的完整 JSON,勾选明文提醒后下载。仅导出当前自动事件中仍有效的复核,筛选队列不影响范围。
+2. **目标浏览器**打开相同平台、配置目录、会话及子会话,选择文件。预览逐条展示依据、时间与处理决定,尚不写入数据。
+3. 只有**身份和完整证据匹配、本地没有任何记录**的条目可导入。过期、不匹配、重复均跳过;不同、过期或损坏的本地记录也不覆盖。确需替换时,先在原复核界面主动撤销,再重新预览。
+4. 明确确认后导入,显示成功、跳过、失败数量。预览期间证据或复核变化会禁用旧确认;确认时再次核对。部分写入失败不回滚成功项,可重新预览重试,已成功项会跳过。
+
+限制 **500 条、1 MiB UTF-8**,严格检查版本、允许字段、哈希标识、重复记录和依据格式;导出文件只含哈希、状态、时间和手写笔记,不自动包含日志、命令、参数或路径。**手写内容仍可能含秘密,文件未加密、未脱敏、未签名**;不要盲目信任他人给的复核文件,哈希匹配不等于结论可信。导入不会改变自动失败/恢复计数。
+
+不同浏览器、端口可迁移,但目录配置或原始会话/证据变化会拒绝匹配,不自动改写路径。没有原始日志时不能用文件重建事件。已恢复、过期或未加载事件不在导出范围;这不是整库备份。多标签页只做写入前复查,不提供跨标签页事务锁。操作均在浏览器本地完成,没有新增上传或同步服务。
本轮的 373 个真实冻结事件只使用**内存中的合成测试标记**检验隔离与失效,没有替你判断真实事件,也没有把这些测试标记写成真实复核。完整证据见 [本机复核验收](diagnostics-verification.md)。
diff --git a/docs/releases/v1.19.0.md b/docs/releases/v1.19.0.md
new file mode 100644
index 0000000..9679590
--- /dev/null
+++ b/docs/releases/v1.19.0.md
@@ -0,0 +1,31 @@
+# v1.19.0 — Explicit, evidence-matched review transfers
+
+Move current-session human review notes between browsers without exporting the original session log or overwriting existing local judgments.
+
+## What's new
+
+- **Preview before export:** view the exact JSON before an explicit download. Only currently loaded, still-valid event reviews are included; the filename contains no session identifier.
+- **Preview before import:** inspect every note and its decision. Only exact event identity and full-evidence matches with empty local slots may be imported.
+- **Safe defaults:** stale, unmatched, identical and conflicting records are skipped. Existing notes—including stale or corrupt ones—are never overwritten by import.
+- **Bounded format:** strict versioned schema, at most 500 unique records and 1 MiB UTF-8. Unknown fields/versions, duplicate entries and malformed values are rejected before writes.
+- **Honest failure handling:** changed evidence/reviews disable an old preview; confirmation rechecks storage. Partial write failures report imported/skipped/failed counts and preserve prior successes rather than claiming an atomic transaction.
+
+## Privacy and limitations
+
+Files contain hash identifiers, evidence fingerprints, human statuses, timestamps and the notes you wrote. The app does not copy raw logs, arguments or paths into them. **Written notes can still contain secrets; files are plain text, not encrypted, redacted or signed.** A matching fingerprint does not authenticate a human conclusion.
+
+Open the same platform, configured directory, session and child scope at the destination. Different browser/origin can work; changed paths, missing logs or changed evidence do not get fuzzy-matched. Recovered, stale and unloaded event notes are not exported. This is not whole-history backup or cloud sync. LocalStorage has no cross-tab transaction lock.
+
+Automatic diagnostic/recovery results are unchanged. No new backend endpoints, runtime dependencies, model calls or uploads.
+
+## Validation
+
+233 Node tests passed, including 17 transfer tests; 103 focused diagnostic/review tests passed. UI build, lint and published-claim checks pass. Browser checks cover actual downloads, isolated browser/origin migration, preview with no writes, exact-match import, conflicts, changed-evidence and cross-tab invalidation, malformed/oversized files, literal rendering of HTML notes, partial write failure and 360px layout.
+
+See [usage and transfer limits](https://github.com/alloevil/AgentXRay/blob/master/docs/diagnostics.md#transfer-reviews-between-browsers) and [the public verification receipt](https://github.com/alloevil/AgentXRay/blob/master/docs/diagnostics-verification.md).
+
+```sh
+npx @alloevil/agent-xray@1.19.0
+```
+
+Node 22.13+ required; compressed DeepSeek Harness logs require 22.15+.
diff --git a/frontend/src/views/sessions/DiagnosticReview.tsx b/frontend/src/views/sessions/DiagnosticReview.tsx
index 69f3b67..7e62578 100644
--- a/frontend/src/views/sessions/DiagnosticReview.tsx
+++ b/frontend/src/views/sessions/DiagnosticReview.tsx
@@ -14,6 +14,7 @@ interface ReviewSnapshot {
export function useEventReviews(scope: string, events: FailureEvent[]) {
const [snapshot, setSnapshot] = useState(null);
+ const [revision, setRevision] = useState(0);
useEffect(() => {
let generation = 0;
let cancelled = false;
@@ -45,7 +46,7 @@ export function useEventReviews(scope: string, events: FailureEvent[]) {
void load();
window.addEventListener('storage', onStorage);
return () => { cancelled = true; window.removeEventListener('storage', onStorage); };
- }, [scope, events]);
+ }, [scope, events, revision]);
const loaded = snapshot?.scope === scope && snapshot.events === events;
const entries = loaded ? snapshot.entries : {};
@@ -67,7 +68,7 @@ export function useEventReviews(scope: string, events: FailureEvent[]) {
} : previous);
}
- return { entries, loaded, error, update };
+ return { entries, loaded, error, update, refresh: () => setRevision((value) => value + 1) };
}
export function DiagnosticReview({ entry, ready, onUpdate }: {
diff --git a/frontend/src/views/sessions/ReviewTransferPanel.tsx b/frontend/src/views/sessions/ReviewTransferPanel.tsx
new file mode 100644
index 0000000..f711a49
--- /dev/null
+++ b/frontend/src/views/sessions/ReviewTransferPanel.tsx
@@ -0,0 +1,164 @@
+import { useRef, useState } from 'react';
+import {
+ createReviewTransfer, importReviewTransfer, previewReviewTransfer, parseReviewTransfer,
+ REVIEW_LABELS, REVIEW_TRANSFER_MAX_BYTES,
+ type EventReview, type ReviewTransfer, type TransferDecision, type TransferPreview,
+} from './diagnostic-reviews';
+
+const DECISIONS: Record = {
+ import: '可导入', unmatched: '非当前事件,跳过', stale: '证据已变化,跳过',
+ duplicate: '已存在相同记录,跳过', conflict: '本地已有记录,保留本地',
+};
+const BUTTON = 'min-h-9 rounded border border-border px-2 py-1 hover:border-primary disabled:opacity-50';
+interface TransferSnapshot {
+ entries: Record;
+ bundle: ReviewTransfer;
+ rows: TransferPreview[];
+ mode: 'export' | 'import';
+}
+
+export function ReviewTransferPanel({ entries, ready, onImported }: {
+ entries: Record;
+ ready: boolean;
+ onImported: () => void;
+}) {
+ const [preview, setPreview] = useState(null);
+ const [confirmed, setConfirmed] = useState(false);
+ const [error, setError] = useState('');
+ const [result, setResult] = useState('');
+ const [reading, setReading] = useState(false);
+ const readSequence = useRef(0);
+ const identities = Object.values(entries).map((entry) => entry.identity);
+ const current = ready && preview?.entries === entries;
+ const json = preview ? `${JSON.stringify(preview.bundle, null, 2)}\n` : '';
+ const importCount = preview?.rows.filter((row) => row.decision === 'import').length ?? 0;
+
+ function clearPreview() {
+ readSequence.current++;
+ setReading(false);
+ setPreview(null);
+ setConfirmed(false);
+ setError('');
+ setResult('');
+ }
+
+ function prepareExport() {
+ clearPreview();
+ try {
+ if (!ready) throw new Error('请等待当前证据核对完成。');
+ const bundle = createReviewTransfer(window.localStorage, identities);
+ setPreview({ entries, bundle, rows: [], mode: 'export' });
+ } catch (failure) {
+ setError(`未导出:${failure instanceof Error ? failure.message : '本地复核无法读取。'}`);
+ }
+ }
+
+ async function readFile(file: File | undefined) {
+ clearPreview();
+ if (!file) return;
+ const sequence = readSequence.current;
+ setReading(true);
+ try {
+ if (!ready) throw new Error('请等待当前证据核对完成。');
+ if (file.size > REVIEW_TRANSFER_MAX_BYTES) throw new Error('迁移文件不能超过 1 MiB。');
+ const text = await file.text();
+ if (sequence !== readSequence.current) return;
+ const bundle = parseReviewTransfer(text);
+ const rows = previewReviewTransfer(window.localStorage, bundle, identities);
+ setPreview({ entries, bundle, rows, mode: 'import' });
+ } catch (failure) {
+ if (sequence === readSequence.current) setError(`未导入:${failure instanceof Error ? failure.message : '文件或本地存储无法读取。'}`);
+ } finally {
+ if (sequence === readSequence.current) setReading(false);
+ }
+ }
+
+ function download() {
+ if (!current || !confirmed || !preview || !preview.bundle.records.length) return;
+ setError('');
+ try {
+ const latest = createReviewTransfer(window.localStorage, identities, preview.bundle.exportedAt);
+ if (JSON.stringify(latest) !== JSON.stringify(preview.bundle)) throw new Error('本地复核已变化,请重新预览。');
+ const url = URL.createObjectURL(new Blob([json], { type: 'application/json' }));
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = 'agentxray-reviews.json';
+ link.click();
+ window.setTimeout(() => URL.revokeObjectURL(url), 1000);
+ setResult(`已发起下载 ${preview.bundle.records.length} 条复核。请妥善保管明文文件。`);
+ } catch (failure) {
+ setError(`未导出:${failure instanceof Error ? failure.message : '下载失败。'}`);
+ }
+ }
+
+ function applyImport() {
+ if (!current || !confirmed || !preview || !importCount) return;
+ setError('');
+ try {
+ const report = importReviewTransfer(window.localStorage, preview.bundle, identities);
+ setResult(`导入 ${report.imported} 条,跳过 ${report.skipped} 条,失败 ${report.failed} 条。${report.failed ? '已成功写入的记录保留;可重新预览重试,未进行整批回滚。' : '自动诊断结果未改变。'}`);
+ setPreview(null);
+ setConfirmed(false);
+ onImported();
+ } catch (failure) {
+ setError(`未导入:${failure instanceof Error ? failure.message : '本地存储无法读取。'}`);
+ }
+ }
+
+ return (
+
+ 迁移当前会话复核 / Export & import
+