From 78dfbbf4d7739f5d4e82d414a88d787b3d52bdee Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Mon, 9 Nov 2026 17:57:23 +0800 Subject: [PATCH] Persist JSON array imports through recovery v14 and the canvas file picker --- headless/README.md | 11 +++++++-- src/components/canvas/headless-imports.tsx | 8 +++---- src/lib/canvas-runtime/engine.ts | 26 ++++++++++++--------- src/lib/canvas-runtime/imports.ts | 5 ++-- src/lib/canvas-runtime/recovery-encoding.ts | 4 ++-- src/lib/canvas-runtime/recovery.ts | 9 +++---- src/lib/canvas-runtime/storage-port.ts | 2 +- src/lib/canvas-runtime/storage.ts | 2 +- tests/headless/browser-storage-cases.mjs | 2 +- tests/headless/imports-engine.test.mjs | 3 +++ tests/headless/recovery-encoding.test.mjs | 2 +- tests/headless/renderer.integration.mjs | 8 ++++++- 12 files changed, 52 insertions(+), 30 deletions(-) diff --git a/headless/README.md b/headless/README.md index 881310e..771424f 100644 --- a/headless/README.md +++ b/headless/README.md @@ -180,7 +180,7 @@ Whole-source completion preserves pending/stored resources and history. Default image sizing and display handles reuse the existing decoder/cache lifecycle. Undo/redo, source failure, quota retry and late chunks do not re-upload bytes or restore deleted incarnations. Earlier loading/recovery versions retain their old -restrictions; future format 13 is rejected. Generation, cross-project durable depot +restrictions; current recovery compatibility is described below. Generation, cross-project durable depot sharing and broader lifecycle/acceptance work remain separate unfinished goals. The package now exposes `/bindings` for pure projection, renderer descriptors, @@ -333,7 +333,14 @@ claim of constant-time full-frame updates or complete region-based drawing. array, with byte cursors, bounded record payloads, structural depth limits and abort/yield checks. It uses parserVersion 3 and can feed a host-managed generic ImportRunner. It does not yet interpret the exported document envelope, and -ProjectEngine's persisted imports/UI still accept their existing formats only. +ProjectEngine now accepts native record arrays through `imports.startJsonArray(bytes, canvasId)`; the file picker routes `.json` to it. Exported document envelopes are still unsupported. A malformed structural tail ends with an explicit failure; previously emitted records remain available. Empty source bytes reject, while `[]` is a valid empty array. Array record errors are distinct from document schema/reference failures. + +JSON array jobs preserve the original Blob and integrity digest, parserVersion 3, +byte cursor, stable IDs, pending dependencies and failures using the same ImportJob +and commit boundary as NDJSON. Such roots use recovery envelope 14; readers accept +1–14 and reject 15. Downgrading an array-bearing image to 13 rejects. This changes +neither IndexedDB schema v4 nor the server wire protocol. Native array items have +the same `{kind,key,record}` schema as NDJSON; `$target` denotes the chosen canvas. diff --git a/src/components/canvas/headless-imports.tsx b/src/components/canvas/headless-imports.tsx index 7ff901a..3b3ede3 100644 --- a/src/components/canvas/headless-imports.tsx +++ b/src/components/canvas/headless-imports.tsx @@ -7,7 +7,7 @@ import { IconButton } from "@/components/ui/icon-button"; import { errorText } from "@/lib/api"; import styles from "./canvas.module.css"; -const reason = (code: string) => ({ invalid_json: "记录格式错误", invalid_json_value: "记录包含不支持的数据", invalid_utf8: "文件编码不是 UTF-8", record_too_large: "单条记录过大", unresolved_dependency: "引用的记录未加入", invalid_native_import_record: "不是画布记录", import_component_unavailable: "元素类型暂不支持", invalid_geometry: "元素尺寸不合法", invalid_endpoint: "连线端点已不存在", duplicate_import_source_key: "记录标识重复", import_source_unavailable: "导入源文件不可用", import_source_changed: "源文件内容与保存时不一致,任务已暂停", import_source_integrity_unavailable: "旧任务缺少源内容校验,无法安全继续;已加入内容保留", import_source_integrity_missing: "源校验记录缺失,任务已暂停", import_source_integrity_interpreter_unavailable: "源校验版本暂不支持", import_target_missing: "目标分区已不存在", import_target_changed: "目标分区已改变", import_dependency_changed: "引用对象已删除或恢复为新版本", dependency_capacity: "等待引用过多,请调整文件顺序", copy_external_changed: "原图引用已删除、移动或恢复为新版本", invalid_duplicate_source: "复制源数据不可用", invalid_copy_snapshot: "复制快照不可用", invalid_copy_preparation: "复制准备记录不可用", copy_interpreter_changed: "复制规则已变化,需要重新复制", invalid_import_source: "文件过大或格式不支持" } as Record)[code] ?? "这条记录暂时无法加入"; +const reason = (code: string) => ({ invalid_json_array: "JSON 记录数组格式错误", incomplete_json_array: "JSON 数组尾部不完整", json_nesting_limit: "JSON 嵌套层数过多", invalid_json: "记录格式错误", invalid_json_value: "记录包含不支持的数据", invalid_utf8: "文件编码不是 UTF-8", record_too_large: "单条记录过大", unresolved_dependency: "引用的记录未加入", invalid_native_import_record: "不是画布记录", import_component_unavailable: "元素类型暂不支持", invalid_geometry: "元素尺寸不合法", invalid_endpoint: "连线端点已不存在", duplicate_import_source_key: "记录标识重复", import_source_unavailable: "导入源文件不可用", import_source_changed: "源文件内容与保存时不一致,任务已暂停", import_source_integrity_unavailable: "旧任务缺少源内容校验,无法安全继续;已加入内容保留", import_source_integrity_missing: "源校验记录缺失,任务已暂停", import_source_integrity_interpreter_unavailable: "源校验版本暂不支持", import_target_missing: "目标分区已不存在", import_target_changed: "目标分区已改变", import_dependency_changed: "引用对象已删除或恢复为新版本", dependency_capacity: "等待引用过多,请调整文件顺序", copy_external_changed: "原图引用已删除、移动或恢复为新版本", invalid_duplicate_source: "复制源数据不可用", invalid_copy_snapshot: "复制快照不可用", invalid_copy_preparation: "复制准备记录不可用", copy_interpreter_changed: "复制规则已变化,需要重新复制", invalid_import_source: "文件过大或格式不支持" } as Record)[code] ?? "这条记录暂时无法加入"; export function HeadlessImports({ engine, canvasId }: { engine: ProjectEngine; canvasId: string }) { const subscribe = useCallback((notify: () => void) => engine.editor.subscribe(notify), [engine]); @@ -17,10 +17,10 @@ export function HeadlessImports({ engine, canvasId }: { engine: ProjectEngine; c const jobs = [...(snapshot.runtime.imports?.values() ?? [])].filter(job => job.source?.targetCanvasId === canvasId).slice(-3).reverse(); const control = (work: Promise) => { void work.then(() => { if (scope.current.engine === engine && scope.current.canvasId === canvasId) setError(""); }).catch(error => { if (scope.current.engine === engine && scope.current.canvasId === canvasId) setError(errorText(error)); }); }; return