使用 DSH 自动完成 Halo 博客站点 1.6.1 → 2.27.1 全站迁移实录,可抄作业
发布日期:2026-10-30 | 环境:群晖 NAS + 群晖 Docker;源站 Halo 1.6.1(H2,运行 4 年)→ 目标 Halo 2.27.1(H2 + UPS + 双盘);迁移工具 plugin-migrate 2.0.3;全过程由 DSH(本机 AI Agent)驱动完成 | 结论先行:官方迁移工具显示 1272/1272 全绿 ≠ 迁移完成——真正的活全在"绿灯之后":主题设置、菜单、摘要、封面这四块一块都不迁;本文给出一天内完成全站搬迁并验收的完整路径与 20 个坑的解法。
一、事故现场:1272 个任务全绿,站点却是空壳
2026-10-30,我的个人博客要从跑了 4 年的 Halo 1.6.1 升到 2.27.1。老站数据躺在 H2 里:176 篇文章、65 个分类、101 个标签、4 年的附件和 SEO 收录。
官方迁移插件 plugin-migrate 跑完,进度条完美:1272/1272 任务全绿,六个页面 HTTP 200。我长舒一口气打开首页——
- 主题是 Halo 默认的,不是我的 Dream;
- 导航菜单是主题临时生成的 4 条默认项,我 15 条自建菜单全部消失;
- 文章列表一半有摘要一半空白;
- 配了"交替图文"的封面图全部丢失。
迁移"成功"了,但迁移完的是一个空壳。 老站还在 8090 端口好好跑着,新站却没法见人。这就是这类迁移的真实面貌:工具负责把"内容"搬过去,而决定"这个站像不像我"的那一半——配置——它默认不管。
二、为什么常规手段救不了
| 常规做法 | 为什么失灵 |
|---|---|
| 指望官方插件全包 | plugin-migrate 的任务组只有 文章/页面/分类/标签/评论/菜单/瞬间/图库/友链/附件——没有 theme_settings、没有 options、没有用户;菜单还只导旧模型数据 |
| 直接把 1.x 的 H2 数据目录拷进新容器 | 1.x 的 H2 与 2.x 的 H2 完全不兼容(表结构/驱动/存储格式三重不同),拷过去轻则起不来重则数据损坏——这条路不存在 |
| 手工在后台重录 | 176 篇文章 + 65 分类 + 101 标签 + 4 年附件,人为错误率不可接受 |
| 装回 1.x 老主题硬凑 | 生产目标就是 2.x;回退主题等于放弃升级理由 |
三、方案:三段式"手术台"打法
整个迁移被拆成三个角色、三个阶段:
- 老站只读取源:1.6.1 容器全程不动,导出 9.4MB 的全站 JSON 作为唯一数据源(比数据库文件安全得多——JSON 是平台中立的);
- 本机 Docker 搭双实例手术台:本机起两个容器——老站对照(8090)+ 新站(8091),同屏对照验收——老站长什么样,新站就照着修到什么样;
- "整站备份恢复"搬运全部人工修复:手术台上修好的站,用 Halo 自带备份功能打出 599.1MB 的 zip(数据库 + 610MB 附件 + 主题 + 插件 + 我修的所有配置),LAN 直传 NAS 新站一键恢复——这一步是全案的胜负手,见决策一。
四、三个关键决策(每个都对应一次真实踩坑)
决策 1:修复成果用"备份恢复"搬运,而不是在 NAS 上重跑一遍迁移
踩坑背景:封面回填 73 篇、摘要修复 45 篇、菜单 href 改写 6 条、分类白名单 9 项……这些修复全部只存在于本地演练实例的数据库里,JSON 里根本没有。如果 NAS 重新走一遍插件迁移,所有人工修复都要重放一遍。
为什么不是显然选项:直觉是"JSON 是数据源,重放一遍就好"——但修复不是数据,是操作记录。最终方案:本地站打包 → 恢复到 NAS,一次搬运带走一切(官方恢复功能声明不限制部署方式与数据库,H2→H2 同引擎恢复实测通过)。
决策 2:主题设置写 ConfigMap 分组 JSON,Setting 对象必须双写
踩坑背景:主题配置有两个存储面——前台读 ConfigMap 的 10 个分组 JSON(实时),控制台设置页读 Setting 资源(带 schema 校验)。我先裸写 Setting:缺 FormKit 字段的数组项、格式不合法的 datetime 被静默清洗回默认(POST 没报错,值却没了);只写 ConfigMap 则设置页回显旧值,下次在设置页一点保存就把正确值回滚。
结论:权威链是 ConfigMap → 前台生效;Setting → 回显与保存安全。两边都写才完整,日常维护一律走控制台设置页保存(它同时维护两边)。
决策 3:数据库用 H2,不上 MySQL
踩坑背景:通用生产建议都是"MySQL,H2 仅限测试环境"。但三个事实让我改了主意:① NAS 有 UPS 断电保护 + 双盘备份,H2 唯一的实质风险(暴力断电损坏)被硬件兜底;② 个人博客单人写入,H2 的并发弱点完全踩不到;③ 少维护一个服务、省 400MB 常驻内存。加上 Halo 定期备份策略,最坏结局 = 回滚到上次备份——与 MySQL 的差距只剩"备份间隔"内的那点数据。场景匹配比通用口径重要。
五、时间线与量化(全部发生在 2026-10-30 一天内)
| 阶段 | 产出 | 关键数字 |
|---|---|---|
| 演练环境搭建 | 双 Docker 实例 + 全量附件就位 | 镜像 356MB + 554MB;手动迁移附件 511 个 |
| 插件迁移 | 10 类数据全绿 | 1272/1272 任务、9.4MB JSON、176 文章/65 分类/101 标签/10 页面/7 瞬间/157 图库/3 友链 |
| 二次验收修复 | 菜单/路由/主题/布局/摘要/封面/头像 | 15 条菜单挂组、6 条 href 改路由、主题 9 模块数组、45 篇摘要 + 73 篇封面回填、20 个坑入册 |
| 生产部署与恢复 | NAS 新站 2.27.1 + 恢复 | 599.1MB 备份 zip、2365 条目、对账 176/65/101 全一致 |
| 收尾 | 域名/SEO/301/agent key | externalUrl 定域名、301 双规则 curl 实测、文章管理员 key 冒烟全绿 |
六、抄作业步骤(照手册执行版)
先决条件缺一不可:① NAS 上独立数据目录(与老站物理隔离是回滚能力的根基);② 端口空闲(群晖无 ss,用 netstat -tln);③ 老站已导出全站 JSON + 附件手动迁移目录。
mkdir -p /path/to/halo_data
docker run -d --name halo2 --restart unless-stopped \
-p 18091:8090 -v /path/to/halo_data:/root/.halo2 \
halohub/halo:2.27
rewrite ^/s/(.*)$ /$1 permanent;
rewrite ^/journals$ /moments permanent;七、验证:怎么证明它真的 work
- 对账:176 文章(103 发布、摘要空 0)/ 65 分类 / 101 标签 / 10 页面 / 7 瞬间 —— 与基线逐项一致;ConfigMap 10 组逐键 diff 零差异;插件 14↔14 全同;
- 渲染对照:新老站同屏截图逐卡对比(公告 DOM 逐字一致、右栏模块集一致、封面 82↔82);
- 发布链路冒烟:文章管理员 key 走「建草稿 → PUT 内容 → 读回 → 发布 → 线上 200 → 删除」全绿;
- 301 前后对比:配置前
curl -I /s/2022plan→ 404,配置后 →301 + Location: /2022plan(/journals → /moments同样实测); - 明说未验证的部分:观察期 ≥3 天仍在进行中;邮件通知只做过一次 UI「测试发信」通过(API 直调路径有字段坑,未做自动化发信回归)。
八、生态位对比:为什么是这套打法
| 方案 | 判定 | 原因 |
|---|---|---|
| 官方 plugin-migrate + 配置层补全(本文) | ✅ 选用 | 唯一官方迁移路径;内容层可信(1272 全绿),配置层用"源 JSON + 老站对照"补全 |
| 等官方"全站一键迁移" | ❌ 不存在 | 任务组设计就到内容为止,theme_settings/options 不在规划内 |
| 手工重录 | ❌ 不可行 | 百篇级内容 + 千级附件,人错率压不住 |
| 升级 MySQL 再迁 | ❌ 收益错配 | 个人站负载 + UPS/双盘在手,多养一个服务纯属负担 |
九、踩坑全记录与红线速查(本篇即完整版,不依赖任何外部文档)
迁移期 13 坑(现象 → 解法)
| # | 现象 | 解法 |
|---|---|---|
| 1 | 初始化 /system/setup 自动化提交全 400(email 校验,3 邮箱×中英文×浏览器/curl 全灭) | 根因未定位(全网 issues 无记录)——初始化手动做,生产也只此一次;本迁移最大未解项 |
| 2 | 首轮导入 1101/1272,171 失败全 404 | 迁移插件声明的 3 个必需插件(瞬间/图库/链接)未预装——导入前三件套先装 |
| 3 | 失败后"全部重试"按钮消失 | 导入任务跑在浏览器端(状态存页面内存)——执行期间不关页不跳走 |
| 4 | 半成品数据上重跑报 409 | metadata.name = 1.x 数字 ID 确定性映射,已存在即冲突——一次干净导入,出错换新实例重来,别在半成品上补 |
| 5 | jar 拷进 plugins/ 目录重启,控制台不显示 | Halo 不扫外部投递 jar——插件一律走控制台本地上传安装 |
| 6 | 扩展端点建文章:包装体→NPE、缺字段→400、带 content→被静默忽略 | content 归 console 端点管——带内容只用 console draftPost |
| 7 | 草稿 GET /content → 500 NoSuchElementException | /content 是发布版内容;草稿读回走 /head-content |
| 8 | 猜测的 7 个端点全 403(带着 PAT 也 403) | Halo 先鉴权后路由、fail-closed:无匹配规则=403 而非 404——端点别猜,查官方客户端文档 |
| 9 | DELETE 返回 200 但实体还在(约 15 秒) | 异步 GC(GcReconciler 延迟清除)——删后稍候再对账,别连环重删 |
| 10 | 误删事故:按 slug 删除,删掉的却是迁移文章(与 2.x 种子数据 slug 撞车) | 永远按 metadata.name 删;删错了从源 JSON 恢复、再按 name 重删 |
| 11 | 对账数字屡差 1~3 | 2.x 会种入默认内容(示例文章/默认分类/协议页)——对账前先清种子或把种子计入预期 |
| 12 | 列表 total 在信封顶层而非 pageInfo;公共评论 API 数字对不上基线 | 对账以导入任务计数为准、API 作交叉参考;评论在 2.x 是分层资源带可见性过滤 |
| 13 | 杂项:应用市场首弹协议、主题路由是 /console/theme 单数、启用主题有二次确认 | 按 UI 提示走即可 |
验收与生产期补充坑(本篇新增记录)
| # | 现象 | 解法 |
|---|---|---|
| 14 | 左栏分类 62 个全量展开,布局与老站两样 | plus 主题的分类组件无数量截断,走 categories_names 白名单 9 项(照抄老站顺序,单层级渲染) |
| 15 | 站点标题成了英文测试值(初始化时照抄排查参数) | systemconfigs/basic.title 改回正式标题——初始化表单别照抄任何排查用的临时值 |
| 16 | 文章缩略图大面积缺失 | 1.x 是渲染期自动提首图、plus 只认 spec.cover——从正文首图批量回填 73 篇 cover |
| 17 | UC 用户接口:入参要平铺、返回却给包装体,直写 avatar 三种路径全被静默丢弃 | 认准请求/响应形状不对称;头像必须走专用上传端点(UI:点头像→上传→裁剪→提交) |
| 18 | 2.27.1 上 POST 建文章 content 不落库(spec 成了、内容空→读回/发布 500) | 发布合同固定两段式:POST 建壳 → PUT 灌 content → PUT publish(两站实测通过) |
| 19 | 恢复后 settings/system 的 SEO 三字段没跟过来(唯一恢复缺口) | 恢复必做非内容资源比对(设置/菜单/主题/插件逐域 diff),缺口以本地为权威补写 |
| 20 | API 直建 PAT 不回明文 token(建出来的是废品);verify 邮件 API 报 EOF 假故障 | key 创建走 UI(明文只此一次);邮件 UI 测试即为结论——API 路径的字段坑单独记录不阻塞 |
红线速查(抄作业前必读)
- 删除永远按
metadata.name,绝不按 slug(坑 10 的血泪); - 迁移页面跑在浏览器侧,离开页面丢状态(坑 3);
- 主题设置必须 ConfigMap + Setting 双写(只写一边:要么被 schema 校验静默清洗,要么被设置页一次保存回滚);
- 老 URL 必须在反代层补 301(
/s/*、/journals),菜单 href 只救站内导航,救不了收录和收藏夹; - agent key 最小权限(文章管理员 =
post-editor),创建走 UI;key 值只进文件、不回显、不外发; - 备份三层保底:NAS zip + 本地演练站 + 1.x 源数据终局底牌,永久不删。
十、结语
一行流复盘:导出 JSON → 本机双实例演练 → 插件迁移 1272 全绿 → 对照老站补齐四块配置(菜单/主题/摘要/封面)→ 599MB 备份恢复进 NAS 2.27.1 → 对账冒烟 301 收官——一天之内,4 年老站完成换血。
这次迁移能一天跑完,本质是把"数据"和"操作"分开对待:数据走官方工具(可信),操作走手术台对照(可验),最后整体搬运(不重放)。AI Agent 在其中的角色不是替你做决定,而是把 20 个坑的排查、45+73 篇的批量修复、十几次 API 契约试错,压进可复现的脚本与文档里——每一次踩坑都变成文档里的一行。
链接与致谢:
- Halo(GPL-3.0):https://github.com/halo-dev/halo ,文档 https://docs.halo.run
- 官方迁移插件 plugin-migrate(halo-sigs):https://github.com/halo-sigs/plugin-migrate
- 主题 Dream2.0 Plus(MIT,活跃维护):https://github.com/hcjike/halo-theme-dream2.0-plus
- 1.x 时代主题 Dream(nineya)致谢;本文即完整版——四份过程手册(Runbook/实施手册/数据说明/镜像固化)为私人运维档案不对外,其全部关键坑与解法已收录于第九节
- 迁移驱动:DSH(本机 AI Agent),全程 Docker 化、未在本机安装任何软件包
给你也来一份:可直接使用的 Prompt
如果你也持有一台 1.x 老站想平移到 2.x,把下面这段直接丢给能操作 Docker 和浏览器的 AI Agent(DSH 或同类都行),方括号参数换成你自己的:
目标:把 Halo [1.6.x] 老站全站迁移到 Halo 2.27.x,内容和配置都要,交付可验收的报告。
阶段与硬约束(每条都对应一次真实踩坑,不要省略):
1. 老站只读:只导出全站 JSON 和附件目录,绝不改动老容器与其数据目录——它是回滚的唯一退路。
2. 本机 Docker 起两个实例做手术台:老站对照 + 新站,各自独立数据目录;生产机只在最后接管。
3. 新站先装迁移插件与目标主题,再用 JSON 导入;附件先手动放进 attachments/migrate-from-1.x/(Halo 批量下载不稳定)。
4. 迁移任务跑在浏览器侧,执行期间不要离开页面——重试入口在页面上,切走即丢状态。
5. 迁移完成后必须补齐四块(官方插件不迁):主题设置(写 ConfigMap 分组 JSON 并同步 Setting 对象,两边都写,只写一边会被校验清洗或被设置页保存回滚)、菜单(旧引用需绑定新模型的菜单组字段)、文章摘要(空摘要的开启 autoGenerate)、文章封面(从正文首图回填 spec.cover)。
6. 处理路由差异:老站 /s/ 前缀和 /journals 在新站不存在——菜单 href 改写 + 反代层补 301(保收录与收藏夹)。
7. 删除任何内容只按 metadata.name,绝不按 slug(防同名撞车误删)。
8. 用 Halo 自带整站备份把演练成果打包 zip,恢复到生产新站——一次搬运带走全部人工修复,不要在生产上重放步骤。
9. 生产端口与数据目录用全新独立路径,与老站物理隔离。
10. 交付物:对账报告(文章/分类/标签/页面与基线数量一致)、坑清单文档、反代 301 配置原文、最小权限的文章管理员 token(走 UI 创建,值只存文件不回显不外发)。
参数:老站地址 [..] / JSON 导出路径 [..] / 新站端口 [..] / 数据目录 [..] / 对外域名 [..] / 数据库 [H2 或 MySQL:个人站且有 UPS+双盘可选 H2]老站的最后一句话是它自己说的:数据没丢,就是门牌换了。迁移的最高境界,是访客根本不知道你搬过家。
原文首发:www.firstsaofan.top
本文首发于我的个人博客,其他平台为同步发布。若样式丢失或图片不显示,请到站内阅读完整版。
使用 DSH 自动完成 Halo 博客站点 1.6.1 → 2.27.1 全站迁移实录,可抄作业
https://www.firstsaofan.top/archives/halo-161-to-2271-dsh-migration-2
评论