站点迁移

本页内容

站点包是 EmDash 站点的内容模型、内容、编辑历史、站点呈现、设置和媒体文件的可移植副本。导入站点包可将站点迁移到另一个 EmDash 部署,包括使用不同数据库的部署:SQLite、PostgreSQL 或 Cloudflare D1。

导入会写入内容区域为空的新站点。EmDash 在写入任何内容之前会检查整个包,以可恢复的小步骤执行导入,回读已导入的站点,并在结果与包一致时签发回执。

站点包不包含用户、凭据或密钥。但它包含站点上的每条条目和评论,包括作者与评论者的电子邮件地址。请像对待数据库备份一样谨慎存储和发送。

选择合适的复制方式

机制用途可导入媒体文件用户与密钥
种子文件初始化内容模型与示例内容是,采用种子语义否否
预览快照填充隔离的预览渲染仅限预览否否
JSON 备份检查选定的数据库形态状态否否否
原始数据库与媒体备份恢复一个部署恢复到同一类数据库需单独复制是
站点包将站点迁移到另一个 EmDash 站点是,导入到空站点是否。仅作者姓名与电子邮件

在数据丢失后恢复部署时,请使用原始数据库备份。要在其他位置创建站点的新副本时,请使用站点包。

站点包包含的内容

站点包包含:

  • 集合、字段、包含每个版本的区块类型、分类法定义、关系定义以及署名字段定义;
  • 每个语言环境下的每条内容条目,包括草稿、定时条目、回收站条目、修订历史和翻译组;
  • 分类学术语与术语分配、署名与署名记录、内容引用和 SEO 记录;
  • 菜单与菜单项、小部件区域与小部件、版块和重定向;
  • 评论与评论反应(除非导出关闭评论);
  • 媒体文件夹、媒体元数据,以及每个就绪媒体文件的字节;以及
  • 下文列出的可移植站点设置。

包以对象键排序的方式存储 JSON 值(例如 JSON 字段和 Portable Text)。因此,导入后的值可能以与源站不同的键顺序列出。除此之外,值保持不变。

可移植设置

仅导出以下设置:site:title、site:tagline、site:logo、site:favicon、site:postsPerPage、site:dateFormat、site:timezone、site:social、site:seo、emdash:site_title、emdash:site_tagline 和 emdash:locale。

目标站点保留自己的站点 URL(site:url 与 emdash:site_url)、站点 ID、设置状态和备份设置。导入绝不会覆盖它们。

导入计划会询问是保留设置向导写入的目标站标题与标语,还是使用包中的值。默认使用包中的值。

主体(principal)

用户账户绝不会随包迁移。对于内容、修订、媒体、署名或评论所引用的每个源站用户,包会携带一个主体:用户 ID、显示名称和电子邮件地址。主体没有角色、密码、通行密钥、会话或令牌。

导入期间,你可将每个主体映射到目标站用户,或保持未映射。参见将作者映射到目标用户。

评论

评论包含作者姓名与电子邮件、正文、状态、线程、时间戳和审核元数据。不导出 IP 地址哈希和用户代理。

反应保留其计数。导出会把每个投票者哈希替换为新的随机值,因此目标站无法将反应与做出反应的访问者对应起来。

站点包不包含的内容

站点包绝不包含:

  • 用户、会话、通行密钥、OAuth 账户、允许的域名、API 令牌、OAuth 客户端、授权码或设备码;
  • 插件存储、插件状态或插件设置(包括插件密钥);
  • 可移植设置以外的设置,例如预览签名密钥;
  • 审计日志、速率限制、编辑锁、定时任务状态、404 日志或迁移历史;
  • 媒体使用记录与搜索索引(导入会重建它们);
  • 源站存储键、存储桶名称、数据库名称或绑定名称;或
  • 未就绪的媒体,例如未完成的上传。

来自外部媒体提供商的媒体仍保持外部。包保留引用,但不复制提供商的文件。

准备目标站点

请导入到满足以下全部要求的站点。当目标站的内容、语言环境、上传限制或支持的格式与包不匹配时,分析会报告阻断项。

  • 管理员账户。 导入以已登录管理员或 API 令牌身份运行。请在设置过程中创建目标站管理员。
  • 存储后端。 源站与目标站都需要已配置的存储。EmDash 会在其中暂存包文件。
  • 无内容。 目标站不得包含条目(含回收站)、修订、媒体或媒体文件夹、署名或署名字段、评论、重定向、术语分配、关系、SEO 记录、在管理后台创建的版块,或设置完成后创建的集合或区块类型。从任意官方模板设置的站点都符合要求。设置过程创建的是设置脚手架:种子集合与区块类型、分类法定义及其未分配术语、菜单及其项、小部件区域及其小部件,以及主题版块。计划会列出脚手架,你确认计划后导入会将其删除。
  • 包使用的每个语言环境。 将包中的每个语言环境添加到目标站的 i18n 配置。没有 i18n 配置的站点仅接受 en。语言环境匹配不区分大小写,导入会以目标站配置的大小写写入每个语言环境,并声明为 locale_recased。
  • 足够大的上传限制。 每个媒体文件都必须符合目标站的 maxUploadSize,默认值为 50 MiB。
  • 格式版本 1。 目标站必须支持包的格式版本以及所有必需功能。

以下请求返回支持的格式版本、功能和限制。其 portableDomain 对象报告站点是否可接收导入,以及不可接收时的原因。

curl https://new.example.com/_emdash/api/admin/transfer/capabilities \
  -H "Authorization: Bearer $EMDASH_TOKEN"

导出站点

导出会以有界步骤读取站点,并将包写入站点的存储。导出完成前,导出器会以与导入相同的方式验证成品包。若导出期间对站点的写入成功,导出器会重新开始。获取或续期条目编辑锁不算写入。三次尝试后会以 TRANSFER_EXPORT_CONCURRENT_WRITES 失败。

导出文件在导出创建后七天内可用。之后下载会返回 TRANSFER_EXPIRED。

在管理后台导出

  1. 打开 Settings → Transfer。该页面仅对管理员可用。

  2. 在 Export 部分关闭 Include comments,以排除评论和反应。

  3. 选择 Export site。页面显示导出进度。请保持页面打开;若离开,返回后导出会继续。

  4. 出现 Export ready 后,选择 Download package 并选择保存 .emdash 文件的位置。页面显示已下载的文件数与字节数,Stop 可取消下载。

该部分还会显示包摘要、各类记录数量,以及站点最近的导出(在过期前各自有下载按钮)。

Download package 会逐个文件获取导出,对照清单检查每个文件的大小与 SHA-256 摘要,并在浏览器中构建 .emdash 文件,因此在 Cloudflare Workers 上对任意大小的站点都可用。若文件不匹配,下载会以错误停止。Chrome、Edge 及其他基于 Chromium 的浏览器会将文件直接写入磁盘。其他浏览器会在下载完成前将整个包保存在内存中;对于约超过 500 MB 的导出,页面建议使用基于 Chromium 的浏览器或 CLI。

Download as one file 改为向服务器请求单次响应中的归档。适合小型站点。在 Cloudflare Workers 上,大型站点可能超出单次请求限制。

使用 CLI 导出

登录源站点,然后导出为包文件:

npx emdash login --url https://example.com
npx emdash site export --url https://example.com --output site.emdash

该命令会将导出推进到完成,逐文件下载包,检查每个文件的大小与摘要,并写入 site.emdash。添加 --no-comments 可排除评论和反应。若命令中断,使用相同选项再次运行即可恢复同一次导出。参见 emdash site export 参考。

使用 REST API 导出

每次调用 advance 会执行一步,并返回 nextRequestInMs(下次调用前的延迟)。当 nextRequestInMs 为 null 时导出完成。

这些示例使用具有 transfer:export 作用域的个人访问令牌。参见令牌作用域。

  1. 开始导出。若要排除评论和反应,请将 { "comments": false } 作为正文发送。Idempotency-Key 标头会使重试请求返回同一导出,而不是开始新的导出。以不同选项重用同一密钥会以 409 TRANSFER_IDEMPOTENCY_CONFLICT 失败。

    curl -X POST https://example.com/_emdash/api/admin/transfer/exports \
      -H "Authorization: Bearer $EMDASH_TOKEN" \
      -H "Idempotency-Key: move-to-new-host"
  2. 推进导出,直到 nextRequestInMs 为 null。在调用之间等待返回的毫秒数。operation.progress 报告已完成与总步骤数(done 与 total)、迄今写入的 records,以及在已知包大小后的 bytesDone 与 bytesTotal。

    curl -X POST https://example.com/_emdash/api/admin/transfer/exports/$EXPORT_ID/advance \
      -H "Authorization: Bearer $EMDASH_TOKEN"
  3. 确认 operation.state 为 complete。失败的导出在 operation.errorCode 中携带原因。

  4. 将包下载为单个 .emdash 文件:

    curl -o site.emdash \
      https://example.com/_emdash/api/admin/transfer/exports/$EXPORT_ID/archive \
      -H "Authorization: Bearer $EMDASH_TOKEN"

.emdash 文件是以 manifest.json 为首个条目的未压缩 tar 归档。归档在一次响应中流式传输所有文件。在 Cloudflare Workers 上,大型站点可能超出单次请求限制。请改为从 exports/{id}/manifest 下载 manifest.json,并从 exports/{id}/files/{path} 下载每个文件。每个下载的文件在流式传输时都会对照其记录的摘要检查。若存储的字节在导出后发生变化,下载会以错误结束而不是完成。

导入站点

导入由包创建,分析成计划,且仅在你通过其摘要确认该计划后才会执行。尚未开始执行的导入会在创建后 24 小时过期。

管理后台、CLI 和 REST API 可运行每一步。AI 代理可通过 MCP 工具分析并启动已上传的导入。

在管理后台导入

  1. 在目标站打开 Settings → Transfer。当站点可接收导入时会出现 Import 部分;否则会列出站点已有的、妨碍导入的内容。

  2. 选择 Choose package file 并选择 .emdash 文件。浏览器会检查包并分片上传。上传期间站点不会发生变化。若上传中断,再次选择同一文件即可从断点继续。

  3. 上传完成后,站点会分析包。你可以离开页面稍后再回来。

  4. 审阅导入:源站点、导出日期与 EmDash 版本、大小、包摘要,以及各类记录数量。阅读 Blockers 与 Warnings、列出计划转换的 Differences from the source site,以及按类型分组的 Starter content that will be removed。参见审阅导入计划。

  5. 在 Authors 下,为每位作者的内容选择本站用户作为所有者,或选择 Don’t map。与用户电子邮件匹配的作者会标记为 Matched by email。参见将作者映射到目标用户。

  6. 在 Site identity 下,选择使用包中的站点标题与标语,还是保留本站的。

  7. 选择 Start import 并确认。计划存在阻断项时按钮会禁用。站点编辑会暂停,直到导入完成。

  8. 跟进进度。导入完成后,页面会显示带有 Verified 徽章的回执,以及回执、包、计划和内容摘要。选择 Copy receipt 可保存回执 JSON 副本。

该页面还提供从上传到导入完成期间的 Cancel import,以及在已开始写入的导入失败或取消后的 Abandon import。两者都需要确认。参见取消导入与放弃未完成的导入。

使用 CLI 导入

登录目标站,然后分析包:

npx emdash login --url https://new.example.com
npx emdash site import site.emdash --url https://new.example.com --analyze

该命令会在本地检查整个包文件、上传、分析,并打印带有计划摘要的计划。当计划有阻断项时以退出码 2 结束。按审阅导入计划所述审阅计划。

要更改计划的决定,请再次运行带决策标志的 --analyze。--map-principal 将主体(按 ID 或电子邮件)映射到目标用户(按 ID 或电子邮件),或映射到 none。--use-target-title 与 --use-target-tagline 会保留目标站的标题与标语:

npx emdash site import site.emdash --url https://new.example.com --analyze \
  --map-principal editor@example.com=editor@example.com \
  --map-principal 01J8ZQ4Y6T2N0D3VJ5R7K9M1PX=none \
  --use-target-title

传入你审阅过的计划摘要以执行:

npx emdash site import site.emdash --url https://new.example.com \
  --plan sha256:3f1c… --confirm

该命令会将导入运行到完成并打印回执。若中断,使用 emdash site import resume <operation-id> 继续。emdash site import status <operation-id> 打印导入状态,emdash site import receipt <operation-id> 再次打印回执。参见 emdash site import 参考。

使用 REST API 导入

服务器处理包内的文件,而不是 .emdash 归档。请先解包。其中包含 manifest.json、index/ 下的索引文件、records/ 下的记录文件,以及 media/ 下的媒体文件。清单固定每个文件的大小与 SHA-256 摘要,因此包摘要可标识整个包。

这些示例使用具有 transfer:analyze 与 transfer:execute 作用域的令牌。

  1. 创建导入。将 manifest.json 的未更改字节作为请求正文发送。响应包含操作以及服务器仍需要的文件的第一页。

    curl -X POST https://new.example.com/_emdash/api/admin/transfer/imports \
      -H "Authorization: Bearer $EMDASH_TOKEN" \
      -H "Idempotency-Key: move-to-new-host" \
      --data-binary @site/manifest.json
  2. 将每个缺失文件上传到 imports/{id}/files/{path}。Content-Length 标头必须等于文件声明的大小,字节必须匹配其声明的摘要。

    curl -X PUT \
      https://new.example.com/_emdash/api/admin/transfer/imports/$IMPORT_ID/files/index/000000.ndjson \
      -H "Authorization: Bearer $EMDASH_TOKEN" \
      --data-binary @site/index/000000.ndjson

    上传索引文件会声明其列出的记录与媒体文件。每批上传后再次请求 imports/{id}/missing,直到它不再返回任何项。

    上传已存储的文件会再次检查它。若已存储副本不再匹配,上传会替换它,响应报告 alreadyVerified: false。

  3. 分析包。调用 imports/{id}/analyze,直到 nextRequestInMs 为 null。最终响应包含 plan 及其 planDigest。

    curl -X POST https://new.example.com/_emdash/api/admin/transfer/imports/$IMPORT_ID/analyze \
      -H "Authorization: Bearer $EMDASH_TOKEN"
  4. 审阅计划,阅读每个阻断项、警告和转换。参见审阅导入计划。

  5. 若默认值不是你想要的,提交决定。每次提交都会返回新的计划与计划摘要。一旦请求执行,计划会被冻结,再提交决定会以 409 TRANSFER_INVALID_STATE 失败。

    curl -X POST https://new.example.com/_emdash/api/admin/transfer/imports/$IMPORT_ID/analyze \
      -H "Authorization: Bearer $EMDASH_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{ "decisions": { "principalMappings": { "01J8ZQ4Y6T2N0D3VJ5R7K9M1PX": null }, "siteTitle": "target" } }'
  6. 使用你审阅过的摘要开始导入:

    curl -X POST https://new.example.com/_emdash/api/admin/transfer/imports/$IMPORT_ID/execute \
      -H "Authorization: Bearer $EMDASH_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{ "packageDigest": "sha256:…", "planDigest": "sha256:…" }'
  7. 推进导入,直到 nextRequestInMs 为 null,在调用之间等待返回的延迟。

    curl -X POST https://new.example.com/_emdash/api/admin/transfer/imports/$IMPORT_ID/advance \
      -H "Authorization: Bearer $EMDASH_TOKEN"
  8. 确认 operation.state 为 complete,然后从 imports/{id}/receipt 读取回执。

当任一摘要与暂存包或当前计划不同时,执行会以 TRANSFER_PACKAGE_DIGEST_MISMATCH 或 TRANSFER_PLAN_DIGEST_MISMATCH 失败。从 imports/{id}/plan 读取当前计划,再次审阅,并用其摘要重试。

将作者映射到目标用户

分析会列出每个主体及其显示名称、电子邮件地址,以及引用它的包记录数量。当恰好有一个目标用户具有相同电子邮件地址(比较不区分大小写)时,计划会建议该用户,并默认将主体映射到该用户。没有建议的主体以未映射开始。

要更改映射,向 emdash site import --analyze 传递 --map-principal,或向 analyze 端点提交 principalMappings。每个映射指定一个目标用户,或保持主体未映射(CLI 中为 none,API 中为 null)。EmDash 会将每个映射应用于条目作者、修订作者、媒体上传者、署名用户链接和评论作者。

未映射主体的引用会被移除。当未映射作者还拥有关联到其账户的署名时,导入器会在该作者每条没有显式署名记录、且其语言环境具有作者署名的条目上,显式署上该署名。因此作者署名会保留在页面上。

以下两种映射会产生 principal_conflict 阻断项:

  • 在同一语言环境中有多个署名的主体被映射到一个用户;或
  • 在同一语言环境中都有署名的两个主体被映射到同一用户。

目标用户每个语言环境只能有一个署名。请将一个主体保持未映射,或将主体映射到不同用户。

审阅导入计划

计划列出导入将创建的内容、将应用的决定,以及三类发现:

  • 阻断项会阻止执行。在计划没有阻断项之前,执行会返回 TRANSFER_PLAN_BLOCKED。更改主体映射以解决 principal_conflict。任何其他阻断项都需要对包或目标站进行更改:取消导入,完成更改,然后创建新的导入。
  • 警告描述不会阻止导入的包问题。它们会被复制到回执中。
  • 转换是源站与导入站点之间精确、已声明的差异。先列出导出器的更改,再列出导入的更改。验证在比较导入站点与包时会应用导入的转换。

计划最多列出 500 个阻断项与警告。issues_truncated 警告会报告另外还发现了多少。

阻断项

代码含义
package_invalid包文件或路径未通过验证。
unsupported_format目标站不支持包的格式或格式版本。
unsupported_feature包需要目标站不支持的功能。
limit_exceeded包文件或记录超出限制。
file_missing已声明的包文件尚未上传。
file_mismatch包文件的大小或摘要与其声明不匹配。
record_invalid记录格式错误或不是规范 JSON。
record_count_mismatch某类记录数量与清单不同。
record_order_invalid记录顺序错误,或父项出现在其子项之后。
duplicate_id同类的两条记录共享一个 ID。
dangling_reference记录引用了包中不存在的记录。包括 blocks 字段命名了包中缺少的区块类型,以及当前版本在包中缺失的区块类型。
reference_cycle术语、评论或菜单项是其自身的父项。
media_ref_invalid内容引用了包中不存在的媒体记录。
media_blob_missing媒体记录的文件不在包中。
media_blob_too_large媒体文件大于目标站的 maxUploadSize。
target_not_empty目标站已有内容。阻断项的 detail 会指出发现了什么。
locale_not_configured包使用了目标站 i18n 配置未包含的语言环境。
field_type_unknown字段或署名字段使用了目标站不支持的类型。
principal_conflict主体映射会使一个用户在同一语言环境中拥有两个署名。
integer_out_of_range整数超出目标数据库的整数范围。PostgreSQL 以 32 位存储整数。
value_constraint_violation管理后台 API 会拒绝的值。参见下方列表。
unique_violation记录会在目标站上重复另一条记录的唯一键。

导入器直接写入记录,因此分析会应用管理后台 API 在保存这些记录时应用的相同检查。以下每类值都是 value_constraint_violation:

  • 不符合其字段列的条目值、必填字段没有值,或集合没有的字段的值;
  • 源或目标不是站点路径、类型不受支持、源模式无效,或目标使用了源未捕获的参数的重定向;
  • 不是 http 或 https URL 的署名网站、不符合其字段类型或选项的署名字段值,或选项数超过站点支持上限的署名字段;
  • 无效的集合 URL 模式;
  • 具有保留 slug、标签为空或超过 200 个字符,或区块类型编辑器会拒绝的字段定义的区块类型;
  • 既不是 http/https URL 也不是站点路径的 SEO 规范 URL;以及
  • 具有菜单不允许的方案的菜单项 URL。

警告

代码含义
media_provider_external内容使用外部提供商的媒体。保留引用;不复制文件。
media_row_missing设置引用了包中不存在的媒体。
soft_reference_dangling可选引用无法解析为包中的记录。
redirect_loops_unchecked包中的重定向过多,无法在导入前检查循环。会关闭循环的重定向将以禁用状态导入。
issues_truncated发现的阻断项或警告多于计划列出的数量。

转换

导出器声明其对源站数据所做的更改。以下每个转换都携带记录类型与计数:

代码含义
orphan_dropped父项在源站上已不存在的记录被省略,例如已删除条目的修订。
soft_orphan_dropped指向缺失记录的链接被省略,例如指向已删除术语的术语分配,或指向已删除条目的菜单项。
orphan_reference_nulled对缺失记录的引用被移除,例如媒体文件已删除的文件夹。
avatar_nulled署名头像或版块预览图引用了包中不存在的媒体,因而被移除。
media_not_ready_dropped未就绪的媒体(例如未完成的上传)被省略。
media_ref_unlinked对包中不存在的媒体的引用已从内容中移除。
media_url_relativized指向源站自身媒体文件的绝对 URL 被转换为可在目标站解析的站点相对 URL。
redirect_duplicate_dropped同一源路径的重复重定向被省略。每个源路径保留一个重定向。
unknown_storage_key记录仍引用源站没有的媒体文件。它们被原样导出。

导入声明其自身的更改:

代码含义
principal_mapped主体引用被重写为映射的目标用户。
principal_unmapped对未映射主体的引用被移除。
seeded_scaffold_removed在导入写入之前删除目标站上的设置脚手架。计划会列出每一项。
redirect_loop_disabled形成循环的重定向以禁用状态导入。
search_unsupported由于目标站使用 PostgreSQL,所列集合的搜索被关闭。
float4_rounded小数值被舍入到目标站 PostgreSQL real 列的精度。
locale_recased语言环境以目标站配置的大小写写入,例如将 pt-br 写为 pt-BR。

运行导入

执行按以下阶段依次进行:

  1. 预留目标站并再次确认其为空。
  2. 移除计划中列出的设置脚手架。
  3. 创建区块类型、集合、字段、分类法定义、关系定义和署名字段。
  4. 将媒体文件复制到目标站存储并创建媒体记录。
  5. 写入术语与署名。
  6. 写入修订与条目。
  7. 写入术语分配、署名记录、内容引用和 SEO 记录。
  8. 写入菜单、小部件、版块、重定向、评论、反应和设置。
  9. 重建搜索索引与缓存,并将媒体使用重新索引加入队列。
  10. 验证结果。

每次 advance 调用执行一个有界步骤,以适应 D1 上 Cloudflare Workers 的请求限制。进度保存在服务器上。中断的请求最多只会丢失进行中的步骤,且每次写入都是幂等的,因此再次运行某步不会重复记录。

当另一请求正在运行某步,或另一请求在某步期间接管操作时,advance 会返回带有较短 nextRequestInMs 的操作。存储或数据库错误会重试:操作记录错误,且 nextRequestInMs 随连续失败增加。在没有进展的情况下反复失败后,导入会失败。

导入期间阻止写入

从第一个执行步骤到导入完成,EmDash 会以 503 TRANSFER_IMPORT_IN_PROGRESS 拒绝对其 API 的写入请求。这涵盖管理后台、REST API、插件路由、公开评论提交、定时发布以及插件内容写入。登录、用户与 API 令牌管理、条目编辑锁,以及传输 API 本身仍可用。读取请求不会被阻止。

MCP 写入工具(包括插件 MCP 工具)会在通常的工具错误中以 TRANSFER_IMPORT_IN_PROGRESS 失败。只读 MCP 工具与 site_* 传输工具仍可工作,因此通过 MCP 启动的导入可通过 MCP 恢复、检查并完成。

中断后恢复

管理后台页面仅在打开时推进导入。要恢复,请重新打开 Settings → Transfer,运行 emdash site import resume <operation-id>,或再次对该操作调用 advance。服务器从最后一个已完成步骤继续。若中断的请求仍持有该操作,下一次调用会等待该持有过期,最多五分钟。

失败或已取消的导入无法恢复。

取消导入

在 Settings → Transfer 中选择 Cancel import,运行 emdash site import cancel <operation-id>,或发送 POST imports/{id}/cancel。进行中的步骤会在当前批次后停止。取消不会移除已写入的记录。

放弃未完成的导入

已开始写入的失败或已取消导入会继续阻止写入,以免未完成的站点被误编辑。要解除阻止,在 Settings → Transfer 中选择 Abandon import,运行 emdash site import abandon <operation-id>,或发送 POST imports/{id}/abandon。放弃会保留已导入的数据。

放弃后,站点不再为空,因此无法再接收另一次导入。请改为导入到新建完成的站点。

从未开始写入的失败或已取消导入不会阻止写入,也无需放弃。

验证结果

验证使用与导出器相同的代码回读每条已导入记录,将计划声明的转换应用于包的记录,然后比较两者。它还会检查每类记录的数量,并再次下载每个已导入媒体文件以检查其摘要。任何差异都会使导入以 TRANSFER_VERIFICATION_FAILED 失败。操作的 errorDetail 最多列出 50 处差异。

成功的导入会生成回执:

{
	"operationId": "01J8ZR2C4S6D8F0G2H4J6K8M0N",
	"packageDigest": "sha256:…",
	"planDigest": "sha256:…",
	"targetSiteId": "01J8ZR0A2B4C6D8E0F2G4H6J8K",
	"originSiteId": "01J1A3C5E7G9J1L3N5Q7S9U1W3",
	"formatVersion": "1",
	"importerEmDashVersion": "0.38.0",
	"completedAt": "2026-09-23T10:15:00.000Z",
	"logicalDigest": "sha256:…",
	"counts": { "entry": 412, "media": 96 },
	"warnings": [],
	"verification": "verified",
	"receiptDigest": "sha256:…"
}

回执记录:在验证完成时,由 targetSiteId 标识的目标站在应用由 planDigest 标识的计划后,恰好持有由 packageDigest 标识的包的内容。logicalDigest 汇总已验证的记录。

receiptDigest 是移除 receiptDigest 属性后回执规范 JSON 的 SHA-256 摘要。它可检测签发后被更改的回执。回执未签名,因此不能证明由哪台服务器签发。若这一点很重要,请通过已认证连接从目标站获取回执。

回执描述验证完成那一刻的站点。它不说明之后的编辑。

在数据库之间迁移

包不依赖源站的数据库。可从 SQLite、PostgreSQL 或 D1 导出并导入到其中任意一种。当目标站使用 PostgreSQL 时,请规划以下差异:

  • PostgreSQL 以 32 位存储整数。超出该范围的整数是 integer_out_of_range 阻断项。
  • PostgreSQL 将 number 字段与媒体焦点存为 32 位浮点值。发生变化的值会声明为 float4_rounded,验证会比较舍入后的值。
  • 全文搜索仅在 SQLite 与 D1 上可用。启用了搜索的集合会在关闭搜索的情况下导入,并声明为 search_unsupported。

导入器会将媒体写入目标站存储后端的新存储键下,并重写内容、设置与 SEO 记录中的媒体引用以匹配。对源站没有的媒体文件的引用会原样导出,并声明为 unknown_storage_key。

安全

  • 将包视为敏感数据。 它包含所有内容(包括草稿与回收站),以及作者与评论者的电子邮件。请勿放在公共存储桶或共享文件夹中,并删除不再需要的副本。
  • 将包视为不可信输入。 导入在写入前会检查路径、大小、摘要、记录模式、引用和限制。它绝不会运行包中的代码或 SQL,也绝不会从包中获取 URL。
  • 有意地授予传输访问权限。 传输需要管理员角色。具有 admin 作用域的令牌可运行所有传输操作,因此请只给代理令牌它所需要的传输作用域。
  • 审阅审计日志。 EmDash 会在站点的审计日志中记录传输操作:transfer_export_create、transfer_import_create、transfer_import_execute、transfer_import_cancel、transfer_import_abandon、transfer_import_complete、transfer_import_fail、transfer_approval_approve 和 transfer_approval_deny。每条记录会指出操作用户以及操作或审批(资源类型为 transfer_operation 或 transfer_approval)。其详情仅包含 ID、摘要、记录计数和错误代码,绝不包含包内容。传输错误详情同样绝不包含包内容。
  • 保持暂存私有。 EmDash 在存储桶的 transfers/ 前缀下暂存包文件,并拒绝通过其媒体路由提供该前缀。若存储桶有公共域名,请像备份一样将其范围限定为媒体。操作结束或过期后会删除暂存文件。

令牌作用域

传输使用三种 API 令牌作用域:

作用域允许
transfer:export开始、推进和下载导出。
transfer:analyze创建导入、上传包文件、分析和读取计划。
transfer:execute开始、推进、取消和放弃导入。

admin 作用域包含全部三种,因此 emdash login 保存的令牌可运行所有传输。每种传输作用域仅授予其自身操作,且只有管理员可以签发。用它们为令牌提供比 admin 更窄的访问权限,例如只能分析包但不能导出或导入的代理。参见作用域参考。

代理审批

AI 代理通过 site_* MCP 工具驱动传输。这些工具开始、推进并报告操作。它们从不携带包字节,因此代理的用户需用 CLI 或 REST API 下载导出并上传包。每个工具都需要 Admin 角色。

令牌既没有 admin 也没有匹配传输作用域的 MCP 客户端(例如仅被授予 transfer:analyze 的代理)无法单独开始导出或导入。其 site_export_start 或 site_import_start 调用会创建待处理的审批请求,并以 TRANSFER_APPROVAL_REQUIRED 与审批 ID 失败。管理员在 Settings → Transfer 的 Approval requests 下批准或拒绝请求,其中会列出每个待处理请求的请求者、操作和过期时间。仅限会话的 POST /_emdash/api/admin/transfer/approvals/{id}/approve 与 …/deny 端点作用相同。API 令牌不能批准请求。客户端随后使用审批 ID 重复调用。审批仅适用于这些 MCP 工具;REST API 没有审批参数。

一次审批向请求它的用户授予一次调用,须来自同一令牌、相同参数。导出审批绑定到导出选项。导入审批绑定到操作与两个摘要,因此更改后的计划需要新的审批。待处理请求 15 分钟后过期,已批准的在批准后 15 分钟过期。启动操作的重试会消耗它;若操作未能启动,可在过期前用同一审批重试。同一用户与令牌随后可在没有该作用域的情况下检查并推进那一次操作。

仅当代理必须在无人逐次批准的情况下运行传输时,才向代理令牌授予 transfer:export、transfer:execute 或 admin。

限制

限制值
manifest.json8 MiB
一条记录1,900,000 字节
一个记录或索引文件4 MiB 与 1,000 条记录
每个包的记录数5,000,000
每个包的文件数1,000,000
JSON 嵌套深度64
一个媒体文件目标站的 maxUploadSize,默认 50 MiB

capabilities 端点报告站点强制执行的值。

面向托管提供商

托管控制平面可以仅用 REST API 将客户站点迁移到生产环境:

  1. 预配具有其存储、语言环境和 maxUploadSize 的新 EmDash 站点,并完成设置。确认 capabilities 将 portableDomain.empty 报告为 true。

  2. 为控制平面签发具有 transfer:analyze 与 transfer:execute 的令牌。不要将其放入任何代理或建站工具。

  3. 运行导入,并在执行前对计划的警告强制执行你自己的策略。拒绝任何有阻断项的计划。

  4. 获取回执并在推广站点前检查:

    • verification 为 verified;
    • packageDigest 是你打算发布的包的摘要;
    • planDigest 是你接受的计划;
    • targetSiteId 是你即将推广的站点;并且
    • receiptDigest 与回执的规范 JSON 匹配。
  5. 推广站点,例如将其域名路由到该站点。

在第 4 步成功之前,请保持目标站不可达。EmDash 不会对访客隐藏部分导入的站点。

故障排除

传输错误使用稳定代码。HTTP 状态与每个代码一并出现。

代码状态如何处理
TRANSFER_TARGET_NOT_EMPTY409目标站已有内容。请导入到新建完成的站点。Settings → Transfer 与 capabilities 会列出使站点不合格的内容。
TRANSFER_IMPORT_IN_PROGRESS503此站点上有导入正在运行,或未完成的导入仍在阻止写入。等待其完成,或放弃失败或已取消的导入。
TRANSFER_FENCE_CHECK_FAILED503EmDash 无法检查是否有导入正在运行。请重试写入。
TRANSFER_EXPORT_CONCURRENT_WRITES409导出运行期间站点持续变化。请在编辑较静默时再次导出。
TRANSFER_EXPIRED410导出文件在七天后被删除,或导入未在 24 小时内执行。请重新开始。
TRANSFER_FILE_MISSING422部分已声明文件未上传。请上传 imports/{id}/missing 列出的全部内容。
TRANSFER_FILE_NOT_DECLARED422上传路径不在包中。请仅上传列出的路径。
TRANSFER_FILE_SIZE_MISMATCH422Content-Length 或上传的字节与声明的大小不同。请原样上传文件。
TRANSFER_FILE_DIGEST_MISMATCH422上传的字节与声明的摘要不同,或导出文件在导出后发生了变化。请上传原始文件,或再次导出。
TRANSFER_LIMIT_EXCEEDED413文件超出限制。对于媒体,请提高目标站的 maxUploadSize。
TRANSFER_MANIFEST_INVALID422请求正文不是有效清单。请逐字节发送 manifest.json。
TRANSFER_UNSUPPORTED_FORMAT422升级目标站上的 EmDash。
TRANSFER_UNSUPPORTED_FEATURE422升级目标站上的 EmDash。
TRANSFER_CONTAINER_INVALID422.emdash 文件不是有效的包归档。请重新下载。
TRANSFER_PLAN_BLOCKED409计划有阻断项。参见审阅导入计划。
TRANSFER_PACKAGE_DIGEST_MISMATCH409摘要与暂存包不匹配。请使用操作的 packageDigest。
TRANSFER_PLAN_DIGEST_MISMATCH409自你审阅以来计划已更改。请读取当前计划并再次审阅。
TRANSFER_DECISIONS_INVALID422决定命名了未知主体,或不存在的目标用户。请更正映射。
TRANSFER_INVALID_STATE409操作不处于允许该请求的状态。请读取操作并遵循其 state。
TRANSFER_LEASE_ACTIVE409另一请求正在运行某步。请等待并重试。
TRANSFER_IDEMPOTENCY_CONFLICT409Idempotency-Key 已用于其他选项的导出,或另一包的导入。请使用新密钥。
TRANSFER_RUNTIME_MISMATCH409不兼容的 EmDash 版本启动了该操作。请用启动它的版本完成,或开始新的操作。
TRANSFER_VERIFICATION_FAILED422已导入站点与包不匹配。请阅读 errorDetail 中的差异,放弃导入,并导入到新站点。
TRANSFER_APPROVAL_REQUIRED403管理员必须批准该请求。参见代理审批。
TRANSFER_APPROVAL_INVALID403审批未知、已拒绝、已过期、已使用,或绑定到其他参数。请请求新的审批。
TRANSFER_SCHEMA_UNCLASSIFIED500数据库有导出器无法识别的表或列。请运行与数据库迁移匹配的 EmDash 版本。
INSUFFICIENT_SCOPE403令牌既没有 admin,也没有请求所需的传输作用域。请签发具有该作用域的令牌。