OVPack 导入导出
OVPack 是 OpenViking 的可恢复内容包格式,用来迁移或备份 viking:// 下的公开内容树。 它保存文件内容、语义侧边文件、可迁移的索引标量,以及可选的 dense 向量快照。
OVPack 不是裸 ZIP 拷贝,也不是可信发布格式。导入会校验 manifest、文件列表、目录列表和 checksum,保证包内容没有偏离 manifest;如果攻击者能同时篡改文件和 manifest,仍需要依赖 外部签名、传输安全和访问控制。
支持范围
普通 export/import 处理一个包根:
viking://resources/...viking://user/...
全量迁移使用单独的 backup/restore,它会把公开 scope root 一起打进备份包:
viking://resourcesviking://user
Session 通过 user 命名空间一起迁移,路径为 viking://user/{user_id}/sessions/{session_id}。viking://session/... 别名不属于 OVPack v3 的导入/导出 scope。
temp、queue、upload、锁文件、watch control 文件、.relations.json 等内部或运行态数据 不属于 OVPack 迁移范围。
与多写存储配合
多写存储只复制启用之后的新写入,不会自动同步启用之前已经存在的历史文件。将已有环境迁移到多写模式时,建议先用 OVPack 完成存量数据迁移,再开启 storage.agfs.backups。
推荐流程:
- 使用
ov backup或ov export导出现有内容。 - 在目标存储环境恢复或导入数据。
- 校验目标环境内容和索引状态。
- 配置并启用多写存储。
- 恢复正常写入,让后续增量由多写机制复制。
更多说明见 多写存储指南。
快速开始
导出和导入资源目录
ov export viking://resources/my-project ./exports/my-project.ovpack
ov import ./exports/my-project.ovpack viking://resources/imported/导入参数是目标父目录,不是最终 root。假设包根名是 my-project,上面的导入结果是:
viking://resources/imported/my-project覆盖已有 root:
ov import ./exports/my-project.ovpack viking://resources/imported/ --on-conflict overwrite导出向量快照
默认导出不保存 dense 向量,导入后由目标环境重新向量化:
ov export viking://resources/my-project ./exports/my-project.ovpack
ov import ./exports/my-project.ovpack viking://resources/imported/如果确认导出环境和导入环境使用同一 embedding 配置,可以显式导出 dense 向量快照:
ov export viking://resources/my-project ./exports/my-project.ovpack --include-vectors
ov import ./exports/my-project.ovpack viking://resources/imported/ --vector-mode auto--vector-mode 控制导入时如何处理包内向量:
| 值 | 行为 |
|---|---|
auto | 默认值。若包内有 dense 快照且 embedding 元数据兼容,则直接恢复;否则重新向量化。 |
recompute | 忽略包内 dense 快照,始终重新向量化。 |
require | 必须恢复兼容 dense 快照;没有快照、快照不完整、模型或维度不兼容都会报错。 |
兼容性校验会比较包内记录的 embedding provider、model、input、query/document 参数和维度。 当前 OVPack 向量快照只支持纯 dense 索引;如果底层向量索引的 VectorIndex.IndexType 是 hybrid,--include-vectors 会直接拒绝导出。导入到 hybrid index 环境时,auto 会重新向量化,require 会报错。
导出 dense 向量快照前,OpenViking 会先做数据一致性检查。也就是检查导出范围内按系统规则 应该进入向量索引的内容,是否已经有对应索引记录。缺失时会拒绝导出,避免生成不完整的 迁移包。
可以单独调用一致性检查来调试当前数据状态:
ov system consistency viking://resources/my-project接口只返回摘要和最多 20 条缺失记录,不返回完整 expected 列表。--include-vectors 导出失败时,错误 details 只携带 1 条缺失 key,避免错误日志过大。
Python SDK:
report = await client.check_consistency("viking://resources/my-project")
print(report["ok"], report["missing_records"])HTTP API:
curl -X POST http://localhost:1933/api/v1/system/consistency \
-H "Content-Type: application/json" \
-H "X-API-Key: your-admin-key" \
-d '{"uri":"viking://resources/my-project"}'全量备份和恢复
全量迁移不要用 export viking://;使用专门的备份包:
ov backup ./backups/openviking.ovpack
ov restore ./backups/openviking.ovpack --on-conflict overwrite备份包只能通过 restore 恢复,不能通过普通 import 导入到任意父目录。
Python SDK
from openviking import AsyncOpenViking
async def migrate_project():
client = AsyncOpenViking()
await client.initialize()
try:
await client.export_ovpack(
uri="viking://resources/my-project",
to="./exports/my-project.ovpack",
include_vectors=False,
)
imported_uri = await client.import_ovpack(
file_path="./exports/my-project.ovpack",
parent="viking://resources/imported/",
on_conflict="overwrite",
vector_mode="auto",
)
print(imported_uri)
await client.wait_processed()
finally:
await client.close()全量备份:
await client.backup_ovpack("./backups/openviking.ovpack", include_vectors=True)
await client.restore_ovpack(
"./backups/openviking.ovpack",
on_conflict="overwrite",
vector_mode="auto",
)HTTP API
HTTP 导出接口直接返回文件流;HTTP 导入和恢复必须先上传本地 .ovpack,再用 temp_file_id 调用 pack 接口。
导出:
curl -X POST http://localhost:1933/api/v1/pack/export \
-H "Content-Type: application/json" \
-H "X-API-Key: your-admin-key" \
-d '{"uri":"viking://resources/my-project","include_vectors":false}' \
--output my-project.ovpack导入:
TEMP_FILE_ID=$(
curl -sS -X POST http://localhost:1933/api/v1/resources/temp_upload \
-H "X-API-Key: your-admin-key" \
-F "file=@./exports/my-project.ovpack" \
| jq -r ".result.temp_file_id"
)
curl -X POST http://localhost:1933/api/v1/pack/import \
-H "Content-Type: application/json" \
-H "X-API-Key: your-admin-key" \
-d "{
\"temp_file_id\": \"$TEMP_FILE_ID\",
\"parent\": \"viking://resources/imported/\",
\"on_conflict\": \"overwrite\",
\"vector_mode\": \"auto\"
}"全量备份:
curl -X POST http://localhost:1933/api/v1/pack/backup \
-H "Content-Type: application/json" \
-H "X-API-Key: your-admin-key" \
-d '{"include_vectors":true}' \
--output openviking-backup.ovpack冲突策略
on_conflict 只在导入 root 已存在时生效。
| 值 | 行为 |
|---|---|
fail | 默认值。目标 root 已存在时返回 409 CONFLICT。 |
overwrite | 删除已有 root,再写入包内容和重建索引。 |
skip | 目标 root 已存在时直接返回该 URI,不写入任何包内容。 |
skip 是 root 级跳过,不是文件级补齐导入。
包结构
OVPack v3 是标准 ZIP。ZIP 内部有一个包根目录:
my-project/
my-project/files/
my-project/files/notes.txt
my-project/files/.abstract.md
my-project/files/.overview.md
my-project/_ovpack/
my-project/_ovpack/index_records.jsonl
my-project/_ovpack/dense.f32 # 仅 --include-vectors 且存在可导出向量时出现
my-project/_ovpack/manifest.jsonfiles/ 下保存用户内容,路径与 OpenViking 中的相对路径完全一致,不再对点文件做 _._ 转义。 _ovpack/ 下保存 OVPack 内部文件,不参与用户内容导入。
manifest 只保存包结构、文件 checksum 和内部索引文件的 checksum,不直接内嵌每个文件的索引记录:
{
"kind": "openviking.ovpack",
"format_version": 3,
"root": {
"name": "my-project",
"uri": "viking://resources/my-project",
"scope": "resources"
},
"entries": [
{"path": "", "kind": "directory"},
{
"path": "notes.txt",
"kind": "file",
"size": 5,
"sha256": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}
],
"content_sha256": "b2a6e9582119c7510d68e3446de3e71a486934bf450d68f65596259ed1cf7997",
"index": {
"records": {
"path": "_ovpack/index_records.jsonl",
"count": 2,
"sha256": "..."
},
"dense": {
"path": "_ovpack/dense.f32",
"count": 1,
"dtype": "float32",
"byte_order": "little",
"dimensions": 1024,
"sha256": "...",
"embedding": {
"provider": "volcengine",
"model": "doubao-embedding-vision-251215",
"input": "multimodal",
"dimensions": 1024
}
}
}
}entries[].path == "" 表示包根目录本身。多层目录使用相对路径,例如:
[
{"path": "", "kind": "directory"},
{"path": "docs", "kind": "directory"},
{"path": "docs/a.md", "kind": "file", "size": 12, "sha256": "..."}
]index_records.jsonl 一行一条索引记录,覆盖文件、目录、根目录:
{"record_id":"r000001","path":"","kind":"directory","level":0,"text":"root abstract","scalars":{"abstract":"root abstract","context_type":"resource","level":0}}
{"record_id":"r000002","path":"notes.txt","kind":"file","level":2,"scalars":{"abstract":"note summary","tags":"demo"},"vector":{"dense":{"offset":0,"dimensions":1024}}}dense.f32 是连续 little-endian float32 数组。index_records.jsonl 中的 vector.dense.offset 是 float 数组偏移,不是字节偏移。示例中 offset=0, dimensions=1024 表示从第 0 个 float 开始读取 1024 个 float。
索引字段
默认会导出可迁移的索引标量:
type, context_type, level, name, description, tags, abstract这些字段不会从包里直接恢复,而是在目标环境重新生成:
id, uri, account_id, owner_user_id, owner_space,
created_at, updated_at, active_count如果使用 --include-vectors,会额外导出纯 dense 向量和 embedding 元数据。导入时即使恢复 dense 快照,也会按目标 URI、目标账号和当前时间重建运行态字段。hybrid index 当前不支持 向量快照导出。
导入校验
导入会先完整校验,再写入目标环境。核心校验包括:
- ZIP 成员必须都在同一个包根下,不能包含绝对路径、反斜杠、盘符或
..。 - 必须存在
<root>/_ovpack/manifest.json。 kind必须是openviking.ovpack,format_version必须等于当前支持版本。root.name必须和 ZIP 根目录一致,root.uri的末段也必须和root.name一致。- manifest 声明的文件集合、目录集合必须和 ZIP 内容一致,不能缺失也不能混入额外内容。
- 每个文件的
size和sha256必须匹配实际内容。 content_sha256必须匹配按路径排序后的文件清单。_ovpack/index_records.jsonl和可选_ovpack/dense.f32必须匹配 manifest 中的 hash、数量和维度。- source scope 和 target scope 必须一致;
user这类结构化 scope 还要求 root 层级一致。 - 校验通过前不会写入包内容;冲突策略也在写入前处理。
典型拒绝示例:
INVALID_ARGUMENT: Missing ovpack manifest
INVALID_ARGUMENT: ovpack file sha256 does not match manifest
INVALID_ARGUMENT: ovpack entries do not match manifest
INVALID_ARGUMENT: ovpack source scope does not match target scope
INVALID_ARGUMENT: ovpack package does not contain a dense vector snapshot导入路径规则
普通子树包导入到同 scope 的父目录,并保留包根:
ov export viking://resources/a ./exports/a.ovpack
ov import ./exports/a.ovpack viking://resources/imported/结果:
viking://resources/imported/a顶级 scope 包只能导入到 viking://:
ov export viking://resources ./exports/resources.ovpack
ov import ./exports/resources.ovpack viking:// --on-conflict overwrite以下导入会被拒绝:
# resources 包不能导入 user
ov import ./exports/a.ovpack viking://user/alice/
# user session 子树不能导入 resources
ov import ./exports/sess_123.ovpack viking://resources/
# session 子树不能把自身路径当父目录,否则会变成 sessions/sess_123/sess_123
ov import ./exports/sess_123.ovpack viking://user/alice/sessions/sess_123/记忆和 Session
记忆目录有固定结构。导入时把包导入到对应目录的父目录,避免产生重复路径。
用户记忆:
ov export viking://user/default/memories ./exports/user-memories.ovpack
ov import ./exports/user-memories.ovpack viking://user/default/ --on-conflict overwriteSession 数据:
ov export viking://user/alice/sessions/sess_123 ./exports/sess_123.ovpack
ov import ./exports/sess_123.ovpack viking://user/alice/sessions/ --on-conflict overwriteSession 只恢复文件状态,不触发向量化。
结果:
viking://user/alice/sessions/sess_123旧包和未来版本
当前实现只接受 OVPack v3。旧版无 manifest 包没有文件集合、目录集合和 checksum 信息,无法判断 是否被删改或混入内容,因此默认拒绝。需要迁移旧包时,应先在可信旧环境中导入,再用当前版本 重新导出为 OVPack v3。
OVPack v2 包也会被当前 OpenViking 拒绝。导入旧包前,需要先用当前版本服务重新导出。
未来版本包也不会静默兼容。处理方式是升级 OpenViking,或在支持该版本的环境中重新导出为当前 支持格式。
常见错误
| 错误 | 常见原因 | 处理方式 |
|---|---|---|
Missing ovpack manifest | 旧版无 manifest 包 | 在可信环境重新导出为 v3。 |
Unsupported ovpack format_version | 包格式版本不是当前支持版本 | 升级 OpenViking 或重新导出。 |
sha256 does not match manifest | 文件或内部索引内容被改动 | 丢弃该包,或从可信源重新导出。 |
ovpack entries do not match manifest | ZIP 中缺文件/目录,或混入额外文件/目录 | 丢弃该包,或重新导出。 |
source scope does not match target scope | 跨 scope 导入,例如 user 导入 resources | 导入到同 scope 的父目录。 |
source path is incompatible with target path | 结构化 scope 的 root 层级会改变 | 导入到正确系统父目录。 |
Top-level scope ovpack packages must be imported to viking:// | 将顶级 scope 包导入了非根父目录 | 改为导入 viking://。 |
Backup ovpack packages must be restored | 用普通 import 导入 backup 包 | 使用 ov restore。 |
Resource already exists | 目标 root 已存在 | 使用 --on-conflict overwrite 或 --on-conflict skip。 |
incomplete OpenViking vector index snapshot | 使用 --include-vectors 时,导出范围内应索引内容缺少索引记录 | 先执行 ov system consistency <uri> 定位问题,再等待处理完成或重新 reindex。 |
dense vector snapshot is incompatible | 包内 embedding 元数据和当前配置不一致 | 用 --vector-mode recompute,或换到兼容配置。 |
常见问题
OVPack 可以手动解压查看吗?
可以。OVPack 是 ZIP 文件,可以用普通解压工具查看。不要手动修改后再导入,修改会破坏 manifest 校验;如果同时修改 manifest 和内容,则需要依赖外部签名和可信来源判断。
为什么不默认导出向量?
向量只在 embedding 模型、输入模式、参数和维度完全兼容时才可直接复用。默认重新向量化更稳; 需要冷迁移加速时,再显式使用 --include-vectors 和 --vector-mode auto/require。
大包导入很慢怎么办?
默认导入会重建目标环境的语义和向量。大包迁移可以使用 --include-vectors 减少重算,或按目录 拆成多个 OVPack 分批导入。
