Lolly
打开应用

数据迁移 - lolly-backup 包

Lolly 用户积累的一切都保存在自己的设备上 - 没有账号,没有云端。数据迁移包就是这些数据移动的方式:在一个安装实例上导出它,通过任何方式携带该文件(USB、AirDrop、发送给自己的邮件、网络共享),再在另一个实例上导入。文件本身就是传输媒介。目标设备是否联网都无所谓,因为整个过程从不与任何服务器通信。

移动整个安装内容的两个按钮:导出我的数据写出一个 zip,导入数据将其读回 signed by Lollyvector SVG9 paths~4.9k nodes11 groups56 KB自己验证获取已签名文件移动整个安装内容的两个按钮:导出我的数据写出一个 zip,导入数据将其读回 signed by Lollyvector SVG9 paths~4.9k nodes11 groups56 KB自己验证获取已签名文件

本页是格式规范。终端用户的操作演示见找回你的作品 → 把你的作品移动到另一台设备。实现代码在 shells/web/src/data-transfer.ts,tests/data-transfer.test.ts 固定了往返契约。

范围。 一个迁移包携带的是用户数据,而不是目录工具。目录工具和目录资源是分开同步的,并假定目标设备上已经存在(最坏情况下版本更旧一些);用户自己制作的工具则会随 profile.json 一起传输。导入操作绝不会安装或升级目录工具。

目标

外层封装

备份包是一个普通的 .zip。下载文件以所属者命名 - LollyTools-<First>-<Last>-<YYYY-MM-DD>-<n>.zip(例如 LollyTools-Ada-Lovelace-2026-06-26-1.zip)- 这样下载文件夹里的一堆备份仍然清晰可辨。名和姓来自个人资料,未设置时会省略。没有个人资料时得到 LollyTools-2026-06-26-1.zip,只有名字时得到 LollyTools-Ada-2026-06-26-1.zip。每一部分都会被清理为文件名安全的标记(保留 Unicode 字母/数字,去除空格/标点,上限 32 个字符)。<n> 是按设备、按天递增的序号,所以同一天多次导出不会冲突,并保持顺序。shells/web/src/data-transfer.ts 中的 backupFilename() 负责生成这个名称。压缩包内容与名称无关,始终一致。内容包括:

路径是否必需内容
manifest.json是格式 id、版本、数量以及各部分的完整性校验。读取者最先查看的内容。
profile.json设置时用户完整的 me 记录:姓名、联系方式、头像引用和标志位,加上文件夹、回收站、项目蓝图、用户模板和用户自制工具、收藏、隐藏的工具、语言和表情符号选择。通过 host.profile 读取。
sessions.json是每个已保存的会话:插槽、工具 id/版本、标签、缩略图(data-URL)和完整的输入数据。通过 host.state 读取。
assets.json是每个已上传资源(图片、字体、品牌令牌、徽标、下载内容的保存副本)的元数据,各自指向 assets/blobs/ 下的字节内容。
assets/blobs/<n>.<ext>按资源计原始资源字节(图片和字体文件)。未压缩存储(已经是压缩格式)。扩展名仅作外观标识,以 assets.json 中的 MIME 为准。
assets/blobs/<n>.c2pa存在时以精确的二进制字节提取出的 Content Credentials,由资源记录中的 _credentialFile 引用。这些不是设备签名密钥。
design-systems.json存在时本次安装制作或添加的设计系统,以 { active, records } 形式存储。导入时按 id 合并;仅当目标设备没有自己的设计系统时,才会采用迁移包的当前选择。
file-history.json可选带版本的资源快照、终态文件操作报告和完整的批处理清单。该历史部分有自己的版本号;由 shell 内部的 fileHistory 备份适配器提供。
revision-history.json可选,手动备份稳定的创作 ID、保留的检查点、缩略图和滚动的恢复草稿。在支持的地方由 host.state.history.backup 提供。
file-history/versions/按快照计之前的资源字节和提取出的凭据,与当前资源是否仍然存在无关。
file-history/results/按已完成的操作计精确的输出字节。不会保留或包含被选中用于转换的原始文件。
prefs.json是用户自有的本地偏好设置:theme、sidebarWidth 以及 ct-metrics 活动统计。
lolly.txt是迁移包的人类可读摘要(数量、个人资料、文件名),供未使用 Lolly 打开压缩包的人查看。每次导出都会重新生成,导入时可被识别,因此从不计入被跳过的部分。它是在完整性映射之后写入的,因此不在该映射范围内。

迁移包特意采用普通的压缩包格式:它能完好经受任何传输方式,任何解压工具都能查看它。

profile.json 是最小的一部分,也是应用中读取者最先看到的部分:制作者只需填写一次的详细信息,以及允许工具使用这些信息的选择开关。

变成 profile.json 的个人资料详情表单:姓名、联系方式和头像 signed by Lollyvector SVG15 paths~1.2k nodes30 groups1 image40 KB自己验证获取已签名文件

manifest.json

{
  "format": "lolly-backup",
  "formatVersion": 3,
  "minReader": 1,
  "app": "lolly",
  "exportedAt": "2026-06-22T09:30:00.000Z",
  "counts": { "profile": true, "sessions": 2, "userAssets": 4, "prefs": 3, "assetVersions": 1, "fileOperations": 1 },
  "integrity": {
    "profile.json": "sha256-…",
    "sessions.json": "sha256-…",
    "assets.json": "sha256-…",
    "assets/blobs/0.bin": "sha256-…",
    "file-history.json": "sha256-…",
    "file-history/versions/0.bin": "sha256-…",
    "file-history/results/0.bin": "sha256-…",
    "prefs.json": "sha256-…"
  }
}
字段含义
format恒为 lolly-backup。没有该字段的文件会被拒绝,提示“不是 Lolly 备份”。
formatVersion该迁移包写入时所用的布局。对部分集合或结构的任何更改都会使其递增。读取者不会以此作为判断依据。
minReader安全导入该迁移包所需的最低读取器版本。读取者正是以此字段作为判断依据。
app生成该包的应用 id,用于诊断。
exportedAt迁移包创建时的 ISO 时间戳。
counts写入者放入的内容数量,用于显示和合理性检查。
integrity可选。将除 manifest.json 外的每个部分映射为其未压缩字节的 SRI 风格 sha256-<base64> 摘要。

版本策略(向前兼容)

formatVersion 与 minReader 之间的区分,正是让该格式得以演进而不至于抛弃旧版安装的关键:

给作者的经验法则: 如果现有的每个读取者在忽略你新增内容的情况下仍能正确工作,那这项更改就是累加式的 - 递增 formatVersion,保持 minReader 不变。否则就提高 minReader。

完整性

当 manifest.integrity 存在时,读取者会在写入任何内容之前验证列出的每个部分的 SHA-256。不匹配(“未通过完整性检查”)或缺失某个部分(“不完整”)都会中止整个导入 - 不存在部分恢复。这能捕获文件传输过程中可能引入的损坏(被截断的 AirDrop 传输、重新编码附件的邮件网关、损坏的 USB 扇区)。

完整性校验在设计上是尽力而为的:只有在 Web Crypto 可用时(每个安全浏览器上下文和现代 Node)才会写入,只有当映射和 Web Crypto 同时存在时才会验证。没有该映射的迁移包 - 例如完整性机制出现之前生成的包 - 会照常导入。“无法验证”永远不会被当作“已损坏”处理。

清单既不会列出自身,也不会列出重新生成的 lolly.txt 说明文件。摘要只覆盖清单所担保的那些部分。

导入语义

导入是一次合并,绝不会全部替换:

已保存的会话会自动重新关联其图片:资源引用以 id 保留,桥接层会在已上传图片恢复后重新解析它们(无论如何都必须如此,因为 blob: URL 无法在重新加载后存活)。

导入摘要会报告 { profile, sessions, userAssets, prefs, skipped, failedAssets }。failedAssets 统计无法恢复的已上传资源数量(例如设备存储已满)。它与 skipped 不同,后者统计的是当前版本无法识别的、来自向前兼容的更新写入者的部分。界面会展示 skipped(“… · N 项较新内容已跳过”),因此恢复过程会如实说明遗漏了什么。

当文件历史存在时,摘要还会携带 assetVersions、fileOperations 和 failedHistory。存储空间耗尽或不可变 ID 冲突可能导致部分恢复;界面会提示用户保留原始备份。云同步在一次部分或不受支持的恢复之后不会推进其已应用的修订版本,因此该快照仍可用于重试。恢复并不是跨越个人资料/会话/资源/历史存储的单一事务。

创作历史(v3)

来自支持历史记录的网页宿主的手动备份,会包含带有自己的 { version: 1, documents, revisions, recoveries } 模式的 revision-history.json。它携带保留下来的 ID、规范化的输入快照、版本戳、光栅预览和独立的写入者草稿。历史适配器会在一次读取事务中捕获当前会话及其头部;sessions.json 会为较旧的读取者使用这些相同的当前快照。

恢复过程会先检查载荷的 SHA-256 与字节数、唯一身份、文档/头部关系、谱系、时间戳、预览类型和各项限制,然后再以一次事务提交该存档。被压缩过的父级引用可能会缺失。已存在的当前作品绝不会被静默替换:当一份创作两边都有时,未保持为当前版本的一侧会成为一份受保护的草稿。存档 384 MiB 的传输上限会被明确检查,存储限制的强制执行也不会截断已保留的检查点。整体备份目前仍使用内存中的 ZIP 实现,而不是流式存档。

摘要新增了 revisions 和 recoveryDrafts,二者只统计这次导入新增的部分,此外还有 added、kept、replaced、copies 和 hidden,用来说明每份创作是如何被合并的。不具备此能力的 shell 会恢复普通会话,并将历史部分报告为已跳过。原生文件系统历史目前仍不受支持,直到其适配器提供持久的历史事务为止。P2P 访客状态没有持久的历史记录或恢复存档。

个人快照同步明确不包含创作历史。将一个快照应用到带有历史记录的本地文档上,会把它之前的工作状态保留为一份独立的恢复草稿,并使任何打开的编辑器的写入令牌失效。其不可变的检查点仍保留在设备上。这可以在快照替换期间保护本地历史,但不会合并多台设备并发产生的历史记录。

历史性的资源引用会被保留,但渲染时仍会通过目标设备现有的资源库来解析资源。此存档目前还不保证旧的资源字节或旧工具渲染结果完全一致。资源版本和文件结果的字节仍会通过它们各自现有的独立备份部分传输。

已保存的版本和文件结果(v2)

这个可选的历史部分包含 { version: 2, assetVersions: [...], operations: [...], batches: [...] };读取者也接受不含 batches 的早期 history-v1 形态。每个快照都标识出稳定的资源 ID 与确切版本、保存时间、字节长度和十六进制 SHA-256,外加一条资源记录,其 _file 和可选的 _credentialFile 指向二进制部分。操作会携带原始文件事实、请求、报告、时间戳和可选的结果 _file;存储后端名称、OPFS 句柄和执行租约不会随之传输。只支持 history-v1 的较旧读取者,会在导入前拒绝新的历史版本,而不是悄悄丢弃批处理成员关系。

批处理清单会在处理之前记录每一个被选中的源文件,包括从未被读取的文件、被取消的成员、预留结果空间失败的情况以及被中断的工作。每个成员都有一个稳定的操作 ID、源引用/事实、请求的输出名称和终态报告。一个未被读取的源文件会有声明的事实,而不是凭空捏造的摘要。导入会校验成员身份,并与随附的任何操作报告核对一致性。即便单个结果已被明确删除,批处理报告仍然可用,但一份回执并不代表其输出字节仍然存储着。

较旧的读取者仍然接受 v2 封装(minReader: 1)并恢复它们熟悉的部分,把不受支持的历史部分计为已跳过。完整的历史恢复需要一个带有 fileHistory 适配器的 shell;这是 shell 内部的一条接缝,而不是面向工具的新增 HostV1 能力。真实的双设备恢复由本地 Chromium 关卡覆盖;已安装的 Tauri/iOS/Android 恢复验收仍是另外的工作。

不会随迁移包传输的内容

存储用量表列出的正是同样的划分。已保存的会话、“我的图片”和文件结果与版本都会包含在迁移包中。它们下方的资源缓存、工具预览和离线固定项都是可重新生成的,因此不会包含在内。

存储用量表将本设备的数据划分为若干命名类别,已保存的会话和我的图片与资源缓存分开统计,此处为全新安装、每个类别仍为空的状态 signed by Lollyvector SVG37 paths~4.9k nodes85 groups65 KB自己验证获取已签名文件存储用量表将本设备的数据划分为若干命名类别,已保存的会话和我的图片与资源缓存分开统计,此处为全新安装、每个类别仍为空的状态 signed by Lollyvector SVG37 paths~4.9k nodes85 groups65 KB自己验证获取已签名文件

跨 shell 保证

data-transfer.ts 只通过能力桥接层(host.profile、host.state、host.assets)以及共享的 localStorage 偏好设置进行读写。同一个模块在 web 和 Tauri 上读写同一种通用封装,底层分别是 IndexedDB 或文件系统存储。可选的历史部分只会在对应的适配器可用时才会出现;不受支持的部分会在导入时被报告为已跳过。无头测试套件针对内存桥接层验证了通用部分,而历史事务还另有真实浏览器测试。

有两个 shell 出于不同原因不在这个保证范围内:

预留扩展点

这个信封结构在设计上就是一份清单加上一组具名部分,这样新类型的可移植数据以后就能在不引入破坏性变更的情况下搭载进来。它们会以附加部分的形式插入(新的 formatVersion,相同的 minReader),而当前版本的读取器会跳过它不认识的内容。这些内容目前还没有构建。这里先预留这些名称,以便这些功能落地时格式仍保持一致。

除了这些预留名称和上面列出的部分之外的任何内容,对读取器来说都是未知部分:保持原样不动,并计入 skipped。

参考