备份与迁移
「系统 > 备份与迁移」页(/admin/backup)承担四类工作:完整备份的导出与下载、整库导入还原、轻量的站点配置导出 / 导入,以及 Qdrant 向量库的检查与重建。完整备份是引擎中立的逻辑归档,同一个 zip 可以在 SQLite 与 PostgreSQL 部署之间互相导入,跨机迁移、跨引擎迁移、灾备恢复全部走这一条路径,不需要碰 pg_dump 或数据库文件。
完整备份导出
归档里有什么
导出产物是单个 zip,内部结构:
aivory-docker-backup-20260711-153000-xxxxxxxxxx.zip
├── manifest.json # 格式版本、源引擎方言、各表行数、是否含文件
├── db/
│ ├── users.jsonl # 每表一个 JSONL,每行一个 JSON 对象
│ ├── conversations.jsonl # 按外键安全顺序排列,引擎中立
│ ├── messages.jsonl
│ └── ... # 全部数据表
├── files/ # 可选:勾选「包含上传文件与生成产物」时才有
│ ├── uploads/... # 用户上传的文件
│ └── artifacts/... # 模型生成的产物(图片、文档等)
└── qdrant/ # 可选:部署配置了 QDRANT_URL 时自动包含
└── collections/
└── aivory_c1536.jsonl # 逐 collection 的向量点位导出
数据行是逻辑 JSONL 而非数据库转储:二进制列 Base64 编码,大整数保精度,这正是它能跨 SQLite / PostgreSQL 的原因。向量 collection 按 aivory_c<维度> 命名(如 1536 维模型对应 aivory_c1536)。
导出选项
| 选项 | 默认 | 说明 |
|---|---|---|
| 包含上传文件与生成产物 | 开 | 一并打包 uploads 与 artifacts 目录,归档明显更大;不勾则只有数据库行 |
| Qdrant 向量 | 自动 | 部署配置了 QDRANT_URL 即包含,无需勾选;未配置 Qdrant 的部署自然没有这一段 |
异步任务与归档列表
页面上的「导出备份」走异步任务:点击后任务在后台生成,页面显示进度(准备中、读取数据库、写入归档),期间可以离开页面。要点:
- 同一时间只能有一个导出任务在跑;导出与下文的向量维护任务互斥,一方运行时另一方拒绝启动。
- 导出基于只读事务,是一致的时间点快照,期间用户的正常使用不受影响。
- 完成后归档出现在「已生成的归档」列表,点「下载」保存到本地。归档文件本身存放在服务器的
BACKUP_DIR目录(默认./data/backups,compose 部署映射在数据卷里),列表按生成时间倒序。
另有一个同步流式端点适合脚本化定期备份:GET /api/admin/backup/export(查询参数 files=1 含文件,qdrant=0 可排除向量),响应直接是 zip 流。小库用它一步到位,大库建议走页面上的异步任务,避免下载连接长时间挂着。
异步导出生成的归档会一直留在服务器的 BACKUP_DIR 里,系统不做自动轮换。每个全量归档可能相当大(含文件与向量时尤甚),下载离机保存后,记得定期到该目录删除陈旧归档,避免数据卷被备份占满。
顺带区分:普通用户在「设置 > 隐私」里也有一个"导出全部数据",那是单个用户自己的 GDPR 式 JSON 导出,与这里的全站管理员备份是两回事,见分享与数据管理。
导入(整库替换)
操作步骤
- 在「导入与还原」区选择一个上面导出的 zip 归档。
- 在确认弹窗中输入确认词
REPLACE(必须完全一致)。 - 点击「导入并替换数据」,等待完成。
- 导入成功后当前会话立即失效并自动登出,用备份里的账号密码重新登录。
替换语义
导入是整库替换,不是合并:
- 在一个数据库事务里清空所有表,再按外键安全顺序从 JSONL 逐表重载;归档里存在而当前版本没有的列会被跳过(前向兼容)。中途任何一步失败都不会提交,数据库保持导入前的状态。
- 文件恢复:归档中记录的存储路径前缀会被重写为本机的上传 / 产物目录,换机器换路径不需要手工处理。
- 向量恢复:归档含 Qdrant 段时逐 collection 重建点位;向量恢复的告警不会中断导入,可事后用向量维护补齐。
- PostgreSQL 目标库会自动重置自增序列;SQLite 目标库在恢复期间临时关闭外键检查。因此跨引擎导入开箱即用:SQLite 的备份可以导入 Postgres 部署,反之亦然。
- 导入完成后设置缓存即刻失效,备份里的站点配置立即生效。
导入完成后,除了"执行本次导入的管理员邮箱"之外,备份带来的所有 admin 账号一律被自动降级为普通用户。这是防提权设计:否则任何人拿到一份构造过的备份,就能给自己塞进一个管理员账号。
实际影响:新实例首启时创建的管理员邮箱,应与旧实例的管理员邮箱一致。如果不一致,导入后旧实例的管理员会全部变成普通用户,而执行导入的账号(其邮箱在备份里可能只是普通用户甚至不存在)也未必还能登录管理后台,需要进数据库手工修复。按下文迁移清单操作即可避开这个坑。
大小上限与 Cloudflare
导入上传的硬上限由环境变量 MAX_BACKUP_BYTES 控制,默认 20 GiB(见核心环境变量)。如果站点挂在 Cloudflare 后面,注意 Cloudflare 对请求体另有 100 MB 级别的套餐上限,大归档必须绕开代理直连源站导入,做法见 Cloudflare 代理配置。反向代理层(如 Nginx 的 client_max_body_size)同样要为备份导入放宽,见反向代理。
导入常见报错
| 现象 | 原因与处理 |
|---|---|
| 400,提示确认词不符 | REPLACE 必须逐字符一致(全大写、无空格) |
| 413 / 请求体过大 | 归档超过 MAX_BACKUP_BYTES,或被 Cloudflare / 反代的请求体上限拦截;调大上限或走直连通道 |
| manifest 校验失败 | 上传的不是本系统导出的备份 zip,或下载 / 传输中被截断;重新导出并核对文件大小 |
| restore failed (no changes committed) | 恢复中途出错,事务已整体回滚,数据库仍是导入前的状态;看服务端日志定位具体的表与行后重试 |
| 导入成功但知识库检索不到内容 | 备份未含向量段(旧实例没配 Qdrant)或向量恢复有警告;到「向量检查」跑一次重建即可 |
配置导出 / 导入(轻量路径)
完整备份之下还有一条更轻的路径:只搬站点配置,不搬用户数据。适合把开发机的一套配置复制到生产、跨部署统一配置、或灾备前留档。
| 方向 | 行为 |
|---|---|
| 导出配置 ZIP | 打包站点设置、渠道、模型、技能、OAuth 提供商、图片风格、用户组、模型额度,以及图标与技能资产文件 |
| 导入配置归档 | 按 ID UPSERT 合并:同 ID 的配置行被覆盖,本地多出的配置保留;绝不触碰用户、对话、消息、上传文件、会话与日志。也兼容旧版纯 JSON 配置文件 |
配置导入是非破坏性的、会话安全的:不需要输 REPLACE,导入后不用重新登录,单个确认框即可执行。
配置 ZIP 里带着明文的渠道 API Key、OAuth Client Secret、SMTP 密码、对象存储与搜索密钥(否则导入后无法直接工作)。请像对待密钥本身一样保管这个文件,不要提交进仓库或散发。
跨引擎迁移:SQLite 到 PostgreSQL 实操
以最常见的"单机 SQLite 起步,长大后迁到 Postgres"为例:
- 旧实例:进入「备份与迁移」,勾选「包含上传文件与生成产物」导出完整备份并下载(配置了 Qdrant 的话向量自动在包里)。
- 新实例:按 Docker Compose 部署拉起 Postgres 模式的全套服务,访问站点完成
/setup首启设置。首启管理员的邮箱必须与旧实例管理员一致(密码可以随意设,导入后以备份里的密码为准)。 - 用这个管理员登录,进入「系统 > 备份与迁移」,选择归档、输入
REPLACE、导入。序列、外键、路径重写全部自动处理。 - 导入完成自动登出,用旧实例的账号密码重新登录。
- 验证:用户列表数量、抽查几个对话与上传文件、知识库检索是否命中。若备份未含向量(旧实例没配 Qdrant)或检索异常,用下文「向量维护」重建。
反方向(Postgres 迁回 SQLite)步骤完全相同,见 SQLite 模式。
跨机迁移清单
换服务器(引擎不变)时,按顺序核对:
- 旧机导出完整备份(含文件、含向量)并下载到本地。
- 新机部署同版本或更新版本的镜像(旧版本导入新备份没有保证)。
- 新机首启
/setup用旧实例管理员的邮箱建号(降级规则,见上文红色警告)。 - 归档大于 100 MB 且新机挂了 Cloudflare:准备好直连通道(本机
127.0.0.1:8787或灰云子域),见 Cloudflare 代理配置。 - 导入、重新登录、验证(用户 / 对话 / 文件 / 知识库检索 / 渠道密钥是否工作)。
- DNS 切到新机;旧机数据保留到验证彻底完成之后再下线。
向量维护
「向量检查」区管理 Qdrant 向量库与数据库的一致性,常见使用场景:导入了一份不含向量的备份、从旧品牌归档迁移、Qdrant 数据卷丢失或更换了 Qdrant 实例。
| 操作 | 行为 | 费用 |
|---|---|---|
| 检查向量 | 逐一核对数据库中的文档切块在 Qdrant 里是否有非空向量,产出审计报告:应有 / 正常 / 缺失 / 空向量 / 跳过,并列出问题样例 | 免费,只读比对,不调用嵌入接口 |
| 重建缺失向量 | 对缺失与空向量的切块,用数据库里已存储的切块文本重新调用嵌入模型,把向量写回对应的 aivory_c<维度> collection;按嵌入模型分组分批执行,完成后报告重建 / 失败数量 | 消耗嵌入 API 调用,大知识库一次重建会产生真实的嵌入费用,执行前留意嵌入渠道的计费 |
要点:
- 重建不需要原始文件,切块文本本来就在数据库里,所以"备份未含向量"并不损失任何数据,只是需要花一次嵌入费用恢复检索。
- 两种任务都是异步后台任务,页面轮询进度;同一时间只能跑一个,且与备份导出互斥。
- 前提是部署配置了 Qdrant 向量后端;未配置时按钮直接报"向量后端未配置"。
升级与日常备份习惯
版本升级走滚动更新:
cd /opt/aivory # 你的 compose 目录
docker compose pull
docker compose up -d
数据库结构在新版本首次启动时自动迁移(增量加列、建表),无需手工执行 SQL。即便如此,请养成两个习惯:
- 升级前先导出一份完整备份。自动迁移是前向的,回滚到旧版本不受保证;有归档在手,任何升级事故都能用一次导入回到升级前。
- 定期导出并离机保存。
BACKUP_DIR里的归档和数据库在同一台机器上,磁盘故障会一起丢;把下载的归档放到对象存储或另一台机器上才算真正的灾备。配置 ZIP 也建议在每次大改配置后留一份。
小团队自部署,每周一次全量(含文件)+ 每次升级前一次,已经足以把最坏情况的损失控制在几天之内。归档是普通 zip,可直接用任何对象存储的生命周期策略做保留轮换。