跳转到内容

备份、导出与导入

Control Center 能够复制、移交或接纳的一切,以及每种产物的确切形态。要实际执行一次 备份,请参阅备份与恢复;至于持久化为何这样 拆分,请参阅工作区与隔离。

共有三种数据库副本产物,它们的作用范围和删除策略各不相同。

产物 写入方 形态 覆盖范围
安装快照 server.backupNow 一个带时间戳的目录 该安装上的每一个数据库
工作区备份 Workspace backup 流水线 带时间戳、内含一个 .db 的目录 一个工作区的数据库
工作区导出 workspace.export 单个 .db 文件 一个工作区的数据库

三者都用 SQLite VACUUM INTO 写出,这对活跃的 WAL 数据库是安全的:它会开启一个读 事务,生成一份已整理碎片、事务上一致的副本,并且从不需要停止服务器。

<dataDir>/backups/2026-07-28T09-12-33-000Z/
manifest.json
global.db
<workspaceId>/workspace.db one directory per workspace

该布局与实时数据目录完全一致,这正是让恢复一次安装成为原样复制回来、而不是一次转换 的原因。目录名是 UTC 时间戳,其中的 : 和 . 被替换为 -,因此它在 Windows 上对 文件系统安全,作为普通字符串也能按时间顺序排序。

计划性的工作区备份单独存放在 <dataDir>/backups/workspace-backups/<workspaceId>/<timestamp>/workspace.db。 它们既不包含清单,也不包含 global.db,并不是安装快照。 每周流水线的保留步骤只会删除它自己所属工作区中早于所配置期限(默认 30 天)的备份; 手动创建的安装快照绝不会被该步骤轮换掉。server.listBackups 只列出安装快照, 包括未完成的那些。

字段 类型 含义
version int 清单格式版本。目前为 1
created_at ISO-8601 UTC 快照的生成时间
global.file string 始终为 global.db
global.schema_version int 捕获时 GlobalDatabase 的 schema 版本
global.bytes int 写出的 global.db 的大小
workspace_schema_version int 每个工作区文件共用的 WorkspaceDatabase schema 版本
workspaces[] array 每个被捕获的工作区一个条目
workspaces[].workspace_id string 该文件所属的工作区
workspaces[].file string 相对于快照根目录的路径,<workspaceId>/workspace.db
workspaces[].bytes int 该文件的大小
skipped_workspaces[] array of string 备份未能捕获的工作区

当某个工作区已注册但从未被写入过(因此文件尚不存在),或者它的 VACUUM INTO 失败 时,该工作区就会被跳过。无论哪种情况,快照仍会完成,只是清单会指明缺失了什么。

server.listBackups 会为每个快照报告一个 complete 标志。当清单缺失或无法解析、 global.db 缺失,或者清单指向的工作区文件不在磁盘上时,该标志为 false。

未完成的快照仍会被列出,因为运维人员看不到的快照,就是他们自以为拥有的快照。当清单 无法读取时,列表会退回到目录结构本身,并报告它找到的任何 <workspaceId>/workspace.db 文件——即使整个快照无法恢复一次安装,这些文件仍可以逐个通过 workspace.import 被 接纳。

两种产物都只是数据库。一个工作区目录所持有的东西远不止它的数据库,而其余的都不会 被带走:

路径 在快照中? 在工作区导出中?
global.db 是 否
<workspaceId>/workspace.db 是 是(它就是那个文件)
<workspaceId>/blobs/ 否 否
<workspaceId>/skills/ 否 否
<workspaceId>/agents/ 否 否
<workspaceId>/chat_credentials/ 否 否
<workspaceId>/spaces/<spaceId>/ 否 否
<workspaceId>/pr_clones/ 否 否
secrets.json 否 否
models/ 否 否
meetings/<meetingId>/ 否 否
rigs/ 否 否

其中有些是可丢弃的——spaces/ 保存的是从仓库重新预配的写时复制工作树,pr_clones/ 是缓存,models/ 可重新下载,rigs/ 是机器状态。有些则不是:blobs/ 保存着数据库 行所指向的粘贴图片,skills/ 和 agents/ 保存着智能体运行时使用的文件,而 secrets.json 保存着该安装上的每一个配对密钥、提供商凭据和 SSO 设置。如何覆盖其余 内容,请参阅备份与恢复。

安装级备份 RPC 以及工作区导出/导入仅限 fullClient:已配对的手机无法触发它们。在 没有接入备份端口的服务器上(例如演示服务器),它们会完全不存在——不是被拒绝,而是在 结构上缺失。

操作 类型 范围 最低角色 参数 返回
server.backupNow mutate 安装 安装所有者 无 {ok, path}
server.listBackups read 安装 安装所有者 无 {backups: [...]}
server.deleteBackup mutate 安装 安装所有者 name {ok}
workspace.export read 工作区 admin 无 {ok, path}
workspace.import mutate 工作区 owner source_path {ok, workspace_id}
workspace.delete mutate 安装* owner id 注册表行被标记为已删除
  • workspace.delete 没有作用范围,是因为它标记的那一行位于全局注册表中;其处理程序 会检查调用者对所指工作区的成员身份,并要求 owner。

每个响应中的 path 都是 服务器主机上的路径。

按最新优先,以目录名排序(目录名就是时间戳,因此即使某个快照的清单无法读取,这个顺序 依然成立)。

{
"backups": [
{
"path": "/…/backups/2026-07-28T09-12-33-000Z",
"name": "2026-07-28T09-12-33-000Z",
"created_at": "2026-07-28T09:12:33.000Z",
"bytes": 481203712,
"complete": true,
"workspaces": [
{
"workspace_id": "w_9f2c…",
"path": "/…/backups/2026-07-28T09-12-33-000Z/w_9f2c…/workspace.db",
"bytes": 190840832
}
],
"skipped_workspace_ids": []
}
]
}

当清单无法读取时,created_at 会被省略。顶层的 bytes 是整个目录在磁盘上的实际 大小,而不是清单自己汇总出的和——运维人员清理快照时,问的是这个目录占了多少空间, 而不是这次备份原本打算写多少。

每个 workspaces[].path 正是 workspace.import 接受为 source_path 的值。这是 刻意的:“从这个快照恢复这个工作区”就是导入操作,因此两者不可能对同一个文件演变出 不同的处理方式。

在替换任何内容之前,候选文件会以一个短生命周期的原生 SQLite 连接以只读方式打开,并 读取它的 workspace_meta 行。

条件 结果
不是可读的 SQLite 数据库,或者没有 workspace_meta 拒绝。不会触碰任何内容
记录的工作区 id ≠ 目标 允许,并记录日志。这正是把一个工作区复制或恢复到一个新 id 之下的方式
记录的安装 id ≠ 本安装 允许,并记录日志。它已配对的设备与用户 id 属于那次安装,在这里无法解析
工作区 id 无效,或者文件不存在 拒绝

一旦接受,目标工作区的数据库会先被关闭并从管理器上卸下,然后才把文件复制覆盖上去 (一个仍然打开的漂移连接会占用该文件及其 -wal),随后再重新打开,好让 beforeOpen 重新装上 FTS 与同步触发器,并重建 FTS 索引——导入文件的内容表是有数据的,但它的索引 可能已经过期。

上面每个操作都以服务器端路径来表达,只有当服务器就是你自己的机器时,这才算一个完整的 答案。这三条路由负责运送字节,走的是媒体代理和 blob 存储所用的同一条 PSK 签名通道。 调用方用自己设备的预共享密钥对一个目标字符串签名,并把签名作为 s 传入。

路由 方法 查询参数 签名目标 要求
/backup/workspace GET w, d, s backup-workspace:<workspaceId> 工作区管理员
/backup/snapshot GET n, d, s backup-snapshot:<name> 安装所有者
/backup/restore POST w, d, s backup-restore:<workspaceId> 工作区所有者

w 是工作区 id,n 是快照目录名,d 是发起调用的设备 id,s 是签名。n 是依据 备份列表解析的,而不是直接拼接到备份根目录上,因此这条路由无法被指向一个列表本不会 展示的目录。

状态码 原因
400 缺少必需的查询参数,或者上传正文为空
403 设备未知或已吊销、签名错误,或者调用方不具备该角色
404 此主机上没有备份端口,或者所指的快照/工作区文件不存在
405 用 POST 以外的方法调用 /backup/restore
413 上传正文超过 8 GiB,无论是按声明的 content-length 还是按实际读取的字节数
500 导出或归档在服务器端失败

两个下载都会返回 Cache-Control: no-store 与 Accept-Ranges: none。每个请求都会 新生成一份全新的副本,并在流式发送完毕后删除它,因此被缓存或被续传的响应会把两次 不同导出的字节拼接成一个看起来有效、实则无效的文件。

/backup/snapshot 返回 <name>.zip,其中的条目都相对于快照根目录,因此解包后会 重现 manifest.json + global.db + <workspaceId>/workspace.db。条目按排序后的 顺序写入,因此同一快照的两次归档会得到完全相同的归档。

/backup/restore 会把正文直接流式写入暂存文件,而不是缓冲它;它在读取前和读取过程中 都对大小设限,并且在每一条退出路径上都会删除暂存文件,包括被拒绝时。

项 值
快照 <dataDir>/backups/<timestamp>/
计划性的工作区备份 <dataDir>/backups/workspace-backups/<workspaceId>/<timestamp>/workspace.db
工作区导出 <dataDir>/backups/exports/<workspaceId>-<timestamp>.db
传输暂存 <dataDir>/backups/transfer/——一块临时工作台,由写入它的那条路由负责清扫
快照归档文件名 <snapshot directory name>.zip
上传上限 每次 POST /backup/restore 8 GiB
保留策略 安装快照:手动删除。工作区备份:每周流水线,默认 30 天

exports/、transfer/ 和 workspace-backups/ 目录都不是安装快照,也不在 server.listBackups 的范围内。只有最后一个目录有计划的保留策略;删除安装快照需要 安装所有者明确操作。