来源:
halo-kb/实例现状基线.md→## A) Halo 版本与运行形态(原文 4821 字符)
A) Halo 版本与运行形态
A.1 确切版本:Halo Pro 2.26.1
三处独立来源互证,结论确定:
证据 1 —— 容器启动日志第一行(最权威):
head -1 /vol1/1000/docker/halo/halo2/logs/halo.log
2026-09-13T18:41:58.626+08:00 INFO 1 --- [main] run.halo.app.Application
: Starting Application v2.26.1 using Java 21.0.12 with PID 1
(/application/application.jar started by root in /application)
证据 2 —— 首页 HTML 的 generator meta:
curl.exe -s --noproxy '*' http://<内网IP>:28090/ | Select-String 'generator'
<meta name="generator" content="Halo 2.26.1"/>
证据 3 —— 镜像 tag 与 digest:
docker inspect Halo --format 'Image={{.Config.Image}} ImageID={{.Image}} Created={{.Created}}'
Image=registry.fit2cloud.com/halo/halo-pro:2.26
ImageID=sha256:a6291a728a3033f0a24bde32ef641d5987180859de57514fde25d06838c84042
Created=2026-09-13T11:02:00.654239422Z
RepoDigest: registry.fit2cloud.com/halo/halo-pro@sha256:982c894493238bc778b9629c7c672c287805c671ddd85702c1aeae20edde85a7
⚠️ 镜像 tag 是
2.26,但实际运行版本是2.26.1—— tag 是滚动 tag,不能当精确版本用。 后续若要做版本锁定/回滚,必须按 digest(sha256:982c8944…)而不是 tag。
镜像来源(存疑点,如实记录):镜像 label org.opencontainers.image.source 指向 https://github.com/lxware-dev/halo-pro,而仓库域名是 registry.fit2cloud.com/halo/halo-pro(官方仓库)。 org.opencontainers.image.version label 值是无意义的 "2"。 本报告不对该镜像的官方性下结论,仅记录事实。
A.2 部署形态
部署目录(NAS 绝对路径):/vol1/1000/docker/halo/
/vol1/1000/docker/halo/
├── docker-compose.yaml (1526 B, 权限 rwx------ ZYJ:Users, mtime 2026-09-13 18:50)
└── halo2/ (Halo 工作目录, root:root, 绑定到容器 /root/.halo2)
├── backups/ (空)
├── indices/ (搜索索引, 含 halo/ 子目录)
├── keys/ (halo2.jks, pat_id_rsa, pat_id_rsa.pub)
├── logs/ (halo.log 671 KB)
├── plugins/ (20 个 jar + disabled.txt + configs/)
└── themes/ (Ethereal/, theme-earth/)
该目录不在 git 管理下(无
.git),也没有.env文件 —— 配置全部内联在docker-compose.yaml里。
compose 文件内容摘要(已脱敏):
services:
halo:
image: registry.fit2cloud.com/halo/halo-pro:2.26
container_name: Halo
restart: on-failure:3
depends_on:
halodb:
condition: service_healthy
networks: [halo_network]
volumes:
- ./halo2:/root/.halo2
ports:
- "28090:8090"
healthcheck:
test: ["CMD","curl","-f","http://<内网IP>:28090/actuator/health/readiness"]
interval: 30s / timeout: 5s / retries: 5 / start_period: 30s
environment:
- JVM_OPTS=-Xmx256m -Xms256m
command:
- --spring.r2dbc.url=r2dbc:pool:postgresql://halodb/halo
- --spring.r2dbc.username=halo
- --spring.r2dbc.password=[redacted] # 原值存在
- --spring.sql.init.platform=postgresql
- --halo.external-url=http://<内网IP>:28090/
halodb:
image: postgres:15.4
container_name: PostgreSQL
restart: on-failure:3
volumes:
- /vol1/1000/docker/PostgreSQL:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=[redacted] # 原值存在
- POSTGRES_USER=halo
- POSTGRES_DB=halo
- PGUSER=halo
networks:
halo_network:
| 配置项 | 值 |
|---|---|
Compose 服务名(halo / halodb) | halo、halodb |
| 容器名 | Halo、PostgreSQL |
| 镜像 | registry.fit2cloud.com/halo/halo-pro:2.26、postgres:15.4 |
| 端口映射 | 0.0.0.0:28090 -> 8090/tcp(宿主机全网卡暴露);PG 5432/tcp 仅容器网络内 |
| Volume | ./halo2 -> /root/.halo2(bind);/vol1/1000/docker/PostgreSQL -> /var/lib/postgresql/data(bind) |
| 网络 | 自定义 bridge halo_network |
| 重启策略 | on-failure:3 |
| 时区 | 容器 TZ=Asia/Shanghai(来自镜像环境) |
| JVM | JDK 21.0.12,-Xmx256m -Xms256m |
| 工作目录 | HALO_WORK_DIR=/root/.halo2 |
容器实况:
docker ps -a --format '{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}'
Halo|registry.fit2cloud.com/halo/halo-pro:2.26|Up About an hour (healthy)|0.0.0.0:28090->8090/tcp
PostgreSQL|postgres:15.4|Up About an hour (healthy)|5432/tcp
PostgreSQL 版本(实测):
docker exec PostgreSQL psql -U halo -d halo -tAc "select version();"
PostgreSQL 15.4 (Debian 15.4-2.pgdg120+1) on x86_64-pc-linux-gnu,
compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
✅ 用户所述 "PostgreSQL 15.x" 已核实为 15.4。
可达性(实测):
curl.exe -s -o NUL -w "HTTP %{http_code} in %{time_total}s" --noproxy '*' http://<内网IP>:28090/actuator/health
curl.exe -s -o NUL -w "HTTP %{http_code} in %{time_total}s" https://<服务名>.tunnel.sushike.cloud/actuator/health
内网直连 HTTP 200 in 0.010253s
公网隧道 HTTP 200 in 0.079831s
/actuator/health 响应体:{"groups":["liveness","readiness"],"status":"UP"} /actuator/health/liveness:{"status":"UP"}
版本探测中不可用的端点(如实记录): /actuator/info、/actuator、/actuator/env、/apis/api.halo.run/v1alpha1/ 均返回 HTTP 302 → /login?authentication_required,不暴露版本。版本只能靠日志 / HTML meta / 镜像三处拿到。
来源:
halo-kb/备份运维与安全.md→## B. 运维方案(原文 8531 字符)
B. 运维方案
B-1 日志轮转
先分清两个"日志",它们的现状完全不同:
① Docker json-file 日志 —— 已经在轮转,不要动。
docker inspect Halo --format 'LogDriver={{.HostConfig.LogConfig.Type}} Opts={{.HostConfig.LogConfig.Config}}'
# LogDriver=json-file Opts=map[max-file:5 max-size:100m]
cat /etc/docker/daemon.json # 全局同样设了 max-size=100m / max-file=5
所以 docker logs Halo 的那份日志上限是 5 × 100 MB = 500 MB,自动滚动。 /vol1/docker/containers 当前合计 415 MB(所有容器加起来),未越界。
改动是否需要重建容器:需要。日志驱动与
log-opts在容器创建时固定,docker restart/docker compose restart都不会重新读取;只有docker compose up -d --force-recreate halo才会生效(会短暂中断站点)。 当前值已合理,结论:无需任何操作。
② Halo 应用日志 halo2/logs/halo.log —— 真正的缺口。
现状(实测):707,963 字节、无任何轮转,且这个文件在容器的工作目录里(bind mount 到宿主可见)。 它是唯一会无限增长的本地日志。方案用 logrotate + copytruncate,不需要重启 Halo。
配置见 H:\Works\halo-kb\scripts\nas\halo-logrotate.conf,安装:
sudo cp /vol1/1000/docker/halo/scripts/halo-logrotate.conf /etc/logrotate.d/halo
sudo chown root:root /etc/logrotate.d/halo && sudo chmod 644 /etc/logrotate.d/halo
logrotate -d /etc/logrotate.d/halo # 先演练(只判定不执行)
系统已有 logrotate.timer(每日 00:00 触发 /etc/logrotate.conf), 而 /etc/logrotate.conf 是 weekly + include /etc/logrotate.d,本文件里的 daily 会覆盖它。
★★ 本次演练抓到一个真错误,值得单独记下来。 我第一版写的是
daily+size 20M,logrotate -d输出:note: 'size' overrides previously specified 'daily' rotating pattern: /vol1/1000/docker/halo/halo2/logs/halo.log 20971520 bytes (14 rotations)
size会覆盖daily—— 规则退化成「仅当文件 ≥20 MB 才轮转」。 而halo.log只有 690 KB,一个月都到不了 20 MB,等于永远不轮转, 与「修好无轮转」的目标完全相反。这个错误只有跑-d才会暴露, 光看配置文件是看不出来的。改成
daily+maxsize 20M后,判定正确:rotating pattern: /vol1/1000/docker/halo/halo2/logs/halo.log after 1 days (14 rotations) empty log files are not rotated, log files >= 20971520 are rotated earlier, old logs are removed即「每天轮转一次,且任何时刻超过 20 MB 也立即轮转」。
同时删掉了
su root root:halo.log 与其父目录都属root:root,root 运行无需 su; 而留着它会让非 root 的-d预演直接失败 (error switching euid from 1000 to 0: Operation not permitted)—— 等于亲手废掉「先演练再上线」这条安全网。判据:
logrotate -d输出必须是after 1 days (14 rotations)且带log files >= 20971520 are rotated earlier。若显示20971520 bytes就是写错了。
验证轮转真的生效:
sudo logrotate -v -f /etc/logrotate.d/halo
ls -la /vol1/1000/docker/halo/halo2/logs/ # 应出现 halo.log-YYYYMMDD(首次不压缩,因有 delaycompress)
sudo logrotate -d /etc/logrotate.d/halo # 再跑一次应为 "log does not need rotating"
B-2 版本升级策略
当前状态
| 项 | 值 |
|---|---|
| compose 里的 tag | registry.fit2cloud.com/halo/halo-pro:2.26(滚动 tag) |
| 实际运行版本 | 2.26.1 |
| 本机镜像 | 2.26 → digest sha256:982c8944…,IMAGE ID a6291a728a30,CREATED 12 days ago(≈ 2026-09-01) |
| PostgreSQL | postgres:15.4(具体 patch 版本,不是滚动 tag —— 这一侧是好的) |
滚动 tag 的风险(这是本部署最大的版本风险)
2.26 是一个会移动的指针。任何一次 docker compose pull && up -d, 或一次镜像清理后重新 pull,都可能静默拿到 2.26.2 / 2.26.3, 而你没有任何操作记录能说明版本变了。后果:
- 插件与 Halo 主体有
requires: >=2.26.0的兼容声明(实测 12 个启用插件里多数要求>=2.26.0),
小版本升级可能让某个插件不再兼容,表现是「站点正常但某个功能悄悄没了」;
- 数据库 schema 迁移不可逆(Halo 升级会迁移数据),回滚镜像不等于回滚数据。
建议:锁定 digest 或具体 patch 版本。
# 1) 先记录当前的 digest(唯一确定的版本标识)
docker image inspect registry.fit2cloud.com/halo/halo-pro:2.26 \
--format '{{index .RepoDigests 0}}'
# → registry.fit2cloud.com/halo/halo-pro@sha256:982c894493238bc778b9629c7c672c287805c671ddd85702c1aeae20edde85a7
# 2) 改 docker-compose.yaml 的 image 行(改前先备份 compose,见 A 节)
# 方式 A:锁 digest(最严格)
image: registry.fit2cloud.com/halo/halo-pro@sha256:982c894493238bc778b9629c7c672c287805c671ddd85702c1aeae20edde85a7
# 方式 B:锁 patch 版本(可读性好,前提是上游确实发布了 2.26.1 这个 tag)
image: registry.fit2cloud.com/halo/halo-pro:2.26.1
# 3) 校验 compose 仍然能解析
docker compose config >/dev/null && echo "compose OK"
# 4) 到升级窗口才执行(**本报告未执行**)
docker compose pull halo && docker compose up -d halo
⚠️
docker compose config不会替你联网校验 tag / digest 是否存在, 它只校验语法。2.26.1这个 tag 是否存在必须自己确认: 本次实测curl https://registry.fit2cloud.com/v2/halo/halo-pro/tags/list返回 403 (该 registry 不允许匿名列举 tag),所以无法用 API 核实—— 请到官方发布页/更新日志确认后再写进 compose,不要照抄本报告里的2.26.1。
升级步骤(只写步骤,未执行)
0) 读更新日志,确认 2.26.x 与已装插件的兼容性
1) ./scripts/halo-backup.sh # 必须有可回滚的备份
2) ./scripts/halo-restore-verify.sh # 并确认它可恢复
3) 记录当前 digest 与 docker compose config 快照
4) 改 compose 的 image 行为目标版本
5) docker compose pull halo && docker compose up -d halo
6) ./scripts/halo-healthcheck.sh # 全绿
7) 抽查:首页 / 文章页 / MiniDocs / 22 个插件状态
8) 复查 halo2/logs/halo.log 有无新的 ERROR
9) 成功则更新 compose 注释里的版本记录;失败则回滚(见下)
回滚:把 image 改回第 3 步记录的 digest → up -d halo → 若发生了 schema 迁移, 还需用第 1 步的 dump 做数据库恢复(--clean --if-exists)。镜像回滚 ≠ 数据回滚。
B-3 定时备份
用用户级 crontab(ZYJ)。理由(均由实测支撑):backups/ 属主是 ZYJ(700); ZYJ 在 docker 组(getent group docker → docker:x:994:ZYJ)因此 docker exec 无需 sudo; 用户级 crontab 不需要 root,而这台 NAS 的 sudo 需要密码。
crontab /vol1/1000/docker/halo/scripts/halo-backup.cron
crontab -l
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
30 3 * * * /vol1/1000/docker/halo/scripts/halo-backup.sh >> /vol1/1000/docker/halo/backups/cron.log 2>&1
30 4 * * 1 /vol1/1000/docker/halo/scripts/halo-restore-verify.sh >> /vol1/1000/docker/halo/backups/cron-verify.log 2>&1
0 8,20 * * * /vol1/1000/docker/halo/scripts/halo-healthcheck.sh --quiet >> /vol1/1000/docker/halo/backups/cron-health.log 2>&1
如何验证它真的会跑(四条,缺一不算验证——「条目在了」不等于「会跑」):
crontab -l # ① 条目存在
date # ② 宿主机时区(cron 用系统时区,绝不换算)
grep -n halo-backup /var/log/syslog | tail # ③ cron 真的触发了该作业
ls -lt /vol1/1000/docker/halo/backups/ | head # ④ 次日看有没有新时间戳产物
⚠️ 不能用
journalctl验证:ZYJ不在systemd-journal组, 实测journalctl -n 0直接返回权限拒绝。cron 的日志走 syslog。 若 syslog 里也看不到 cron 记录,用一个临时自证作业:* * * * * date >> /tmp/cron-alive.log,等两分钟后检查文件是否增长,验完删掉该行。
⚠️ cron 的
PATH必须显式写全(已写):cron 环境的最小 PATH 里没有/usr/local/bin, 而脚本依赖docker、tar、sha256sum、stat,漏了会表现为「手工能跑、cron 报 command not found」。
systemd timer 备选(需 root,本方案未安装):unit 内容见 H:\Works\halo-kb\scripts\nas\README.md 第五节。
B-4 健康检查与告警
halo-healthcheck.sh:10 项判据,只读,支持 --prom / --quiet,退出码 0/1。 实测输出:
检查项 结果 详情
container:Halo PASS status=running health=healthy
container:PostgreSQL PASS status=running health=healthy
actuator:readiness PASS HTTP 200 status=UP
actuator:liveness PASS HTTP 200 status=UP
site:home PASS HTTP 200 (http://127.0.0.1:28090/)
db:connect PASS db=halo size=10085 kB
disk:/vol1 PASS 剩余 23%(阈值 >=10%)
backup:freshness PASS halo-db-20260913-211321.dump 距今 0h(阈值 <=36h)
log:halo.log PASS 0MB(阈值 <=100MB)
halo:restarts PASS RestartCount=1,已稳定运行 132 分钟(历史重启,非抖动)
✅ 全部通过
★ 第 10 项是本次演练改出来的。第一版判据是「
RestartCount != 0即 FAIL」, 结果在一台完全健康的机器上报了 FAIL(实测RestartCount=1)。 查证:该次重启发生于 19:02,halo.log里对应一条Application run failed … UnsatisfiedDependencyException … r2dbcEntityTemplate—— 是部署时刻的一次启动竞争(PostgreSQL 刚起来),此后稳定运行 132 分钟。 判据改成「RestartCount>0且 运行时长 < 30 分钟才算 FAIL」。 理由:「容器重启过」和「容器正在抖动」是两件事,用一个计数当判据必然误报, 而误报的代价是告警疲劳——真出问题时没人再看。
探测端点(实测可达性)
| 端点 | 匿名 | 说明 |
|---|---|---|
/actuator/health/readiness | 200 {"status":"UP"} | 依赖就绪(DB 等)—— 容器 healthcheck 用的就是它 |
/actuator/health/liveness | 200 {"status":"UP"} | 进程存活 |
/actuator/health | 200 {"groups":["liveness","readiness"],"status":"UP"} | 聚合 |
compose 里现有的 healthcheck(已存在,无需改动):
test: ["CMD", "curl", "-f", "http://<内网IP>:28090/actuator/health/readiness"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
小提示:该 healthcheck 从容器内部访问
<内网IP>:28090(绕了一圈宿主 IP)。 能工作,但依赖宿主 IP 不变;用http://127.0.0.1:8090/actuator/health/readiness更稳。 本报告不改(属于既有配置)。
告警接入:该机已运行 Uptime Kuma(louislam/uptime-kuma,0.0.0.0:13001), 可以直接用它做「HTTP(s) 关键字监测」:
- URL:
http://<内网IP>:28090/actuator/health/readiness - 期望关键字:
"status":"UP" - 间隔 60s,重试 3 次
- 另加一条 Push 监测接收
halo-healthcheck.sh --prom的输出(覆盖磁盘、备份新鲜度、日志体积)
--prom 输出样例:
halo_check_result{check="backup:freshness",detail="halo-db-20260913-211321.dump 距今 0h(阈值 <=36h)"} 1
halo_check_overall 1
来源:
halo-kb/页面与展示配置记录.md(整篇)(原文 17487 字符)
Halo 站点「页面与展示配置」记录
实例:Halo Pro 2.26.1 ·
http://<内网IP>:28090· 隧道https://<服务名>.tunnel.sushike.cloud· 主题 Ethereal 1.2.4 执行时间:2026-09-13(UTC 12:49–12:52) 标注约定:【实测】=本轮真实请求/读回的证据;【推断】=有依据但未直接验证;【未确认】=没查到,不下结论。
0. 本轮做了什么(摘要)
| # | 动作 | 对象 | 结果 |
|---|---|---|---|
| 1 | 确认项目集真实路由 | portfolio 插件 | /portfolio(实测 200),/projects 实测 404 |
| 2 | 新增主导航菜单项 | 主菜单 primary | 新增「文档」→/docs、「项目」→/portfolio;原 4 项未改动 |
| 3 | 新建并配置页面模板 | 独立页面 | 新建 skills(技能栈,模板 skills)、timeline(履历,模板 timeline),均已发布 |
| 4 | 修复主题出厂演示数据 | 主题设置 3 个组 | 7 个演示字段改为真实值;ConfigMap version 2 → 3 |
| 5 | 诊断 external-url | 启动参数 + 系统设置 | 只诊断未改:实测生效值已是公网域名,与任务描述不符,详见 §5 |
| 6 | 匿名前台验证 | 7 条路径 + 5 条隧道路径 | 全部 200 |
没有做:未启用 plugin-docsme;未改动任何插件设置;未删除任何既有页面/菜单/内容;未重启容器或服务;未替换头像与横幅图(无真实素材)。
1. 项目集真实前台路由 —— 结论:/portfolio 【实测】
1.1 实测证据
| 请求(匿名,无 Authorization) | 状态码 | 结论 |
|---|---|---|
GET /portfolio | 200 | 真实路由 |
GET /projects | 404 | 不存在 |
GET /portfolio 返回的 HTML 关键片段(原文摘录):
<title>项目作品集 - 俗世客的思行小筑</title>
<meta name="description" content="俗世客的思行小筑作品集展示">
<span class="text-50 page-header-sub">共 0 个项目</span>
<a href="/portfolio?type=" class="btn-card ...">
- 页面标题「项目作品集」、插件自己的筛选链接
?type=、计数器「共 0 个项目」——这三处只有项目集插件会渲染,可排除"主题兜底页"的可能。 - 对照:同一次探测中
/projects返回 404,说明 Halo 对未知路径确实返回 404,因此/portfolio的 200 是真实路由而非 catch-all。
1.2 与既有文档的差异(以实测为准)
| 来源 | 写的路由 | 判定 |
|---|---|---|
| 插件 README | /projects | 与实测不符(实测 404) |
插件 settings.yaml | /portfolio | 与实测一致 |
| 主题官方文档(设计文档引用) | /portfolio | 与实测一致 |
结论:路由为
/portfolio。后续菜单、文档、脚本一律用/portfolio。
1.3 附带发现:项目集当前没有内容 【实测】
页面渲染「共 0 个项目」——项目集插件里尚未录入任何项目。菜单已指向该页,但目前是一张空列表页。录入项目属于内容生产,本轮未做(见 §8)。
2. 导航菜单配置 【实测】
2.1 菜单 API 的真实位置(重要)
初次按常规猜测的端点全部 404:
| 尝试的端点 | 结果 |
|---|---|
/apis/menu.halo.run/v1alpha1/menus | 404 |
/apis/api.console.halo.run/v1alpha1/menus | 404 |
/apis/api.halo.run/v1alpha1/menus(无 /-) | 404 |
正确端点(从官方 @halo-dev/api-client@2.26.0 包的生成代码中提取,并逐个实测):
| 用途 | 端点 | 实测 |
|---|---|---|
| 读菜单(含内嵌菜单项) | GET /apis/api.halo.run/v1alpha1/menus/- | 200 |
| 读单个菜单 | GET /api/v1alpha1/menus/{name} | 200 |
| 列表 / 创建菜单项 | GET / POST /api/v1alpha1/menuitems | 200 / 201 |
| 菜单项树(控制台用) | GET /apis/api.console.halo.run/v1alpha1/menuitems/-/tree?menuName=<name> | 400(缺参)→ 说明端点存在 |
⚠️ 注意菜单项 CRUD 不在
/apis/...下,而在/api/v1alpha1/...(无 group 段)。这一点极易踩空。
MenuItem 的 spec 字段(来自官方 api-client 类型定义,与实测写回一致): displayName、href、menuName、priority、parent、children[]、target、targetRef。
2.2 改动前(原菜单完整快照,用于回滚)
GET /api/v1alpha1/menuitems(total=4)+ GET /api/v1alpha1/menus/primary:
| 顺序 | metadata.name | displayName | href | priority | targetRef |
|---|---|---|---|---|---|
| 0 | 88c3f10b-321c-4092-86a8-70db00251b74 | 首页 | / | 0 | — |
| 1 | c4c814d1-0c2c-456b-8c96-4864965fee94 | 文章 | /archives | 1 | — |
| 2 | 35869bd3-33b5-448b-91ee-cf6517a59644 | 默认分类 | /categories/default | 2 | Category/76514a40-6ef1-4ed9-b58a-e26945bde3ca |
| 3 | b0d041fa-dc99-48f6-a193-8604003379cf | 关于 | /about | 3 | SinglePage/373a5f79-f44f-441a-9df1-85a4f553ece8 |
Menu.primary 原值:{"displayName":"主菜单","menuItems":[上面 4 个 name]},metadata.version=0。
2.3 改动后
新增 2 项(POST /api/v1alpha1/menuitems,均返回 HTTP 201):
| displayName | href | priority | metadata.name |
|---|---|---|---|
| 文档 | /docs | 4 | 1c023cd8-efb3-412a-936d-540ea88dd48f |
| 项目 | /portfolio | 5 | efdb2d64-a1ac-412d-993c-5833e0df8820 |
原 4 项一字未改(name / href / priority / targetRef 全部保持原值)。
最终导航顺序:首页 | 文章 | 默认分类 | 关于 | 文档 | 项目
与设计文档的差异:设计建议的顺序是「首页 | 文档 | 项目 | … | 关于」。本轮故意把新项追加在末尾,理由是不改动既有 4 项的
priority(避免触碰既有数据)。若要让「文档/项目」前移,见 §7.4 的可选重排命令。
2.4 一个反直觉的实测结论:前台导航不依赖 Menu.spec.menuItems
创建两个 MenuItem 后回读 GET /api/v1alpha1/menus/primary,其 spec.menuItems 仍是原来 4 个 name,没有被自动追加;但匿名抓取首页 HTML,导航栏已经出现「文档」和「项目」:
[>文档<] x2 [>项目<] x2 [/docs] x2 [/portfolio] x2
【实测结论】 前台导航按「menuName=primary 的 MenuItem + priority 排序」渲染,Menu.spec.menuItems 并非渲染依据(至少不是唯一依据)。因此本轮没有去改 Menu.spec.menuItems——不需要,也不该动。
3. 页面模板配置 【实测】
3.1 模板字段与取值的实测确认
| 项 | 结论 | 依据 |
|---|---|---|
| 字段路径 | SinglePage.spec.template | 读回 GET /apis/content.halo.run/v1alpha1/singlepages 时,既有页面该字段为 "" |
| 取值格式 | 模板文件名去掉 .html(如 skills,不是 skills.html) | 写入 template=skills 后,/skills 实渲染出技能页专属内容 |
| 可用模板清单 | 27 个,含 skills.html、timeline.html、portfolio.html、portfolio-detail.html | halo_list_theme_templates |
设计文档把"取值是
skills还是skills.html"列为未确认项 —— 本轮已确认取值为skills(去扩展名)。
3.2 已建的页面
| slug | 标题 | spec.template | 状态 | 资源名 |
|---|---|---|---|---|
skills | 技能栈 | skills | 已发布 | skills |
timeline | 履历 | timeline | 已发布 | timeline |
创建命令(Halo CLI 1.3.0,profile local):
& H:\Works\halo-cli\prefix\halo.cmd single-page create --name skills --title "技能栈" --slug skills --template skills --content "..." --publish true
& H:\Works\halo-cli\prefix\halo.cmd single-page create --name timeline --title "履历" --slug timeline --template timeline --content "..." --publish true
模板确实生效的实测证据(匿名抓取):
| 路径 | 状态码 | 关键片段 |
|---|---|---|
/skills | 200 | <title>技能栈 - 俗世客的思行小筑</title>,HTML 含 「技术技能」(= 主题 extendPages.skills.subtitle 的默认值「我的技术技能和专业知识」) |
/timeline | 200 | <title>履历 - 俗世客的思行小筑</title>,HTML 含 timeline 标识 16 处 |
「关于」页保持默认模板
page.html(spec.template=""),未改动——设计文档 E④ 对它的要求是"正文自己写",不属于模板配置范围。
3.3 这两个页面的展示数据仍为空 【实测】
主题设置 extendPages 组的实际值为 {}(空对象),即:
- 技能页没有技能条目 → 页面能打开,但列表为空;
- 履历页没有时间线条目 → 同上。
因此本轮没有把这两页挂进导航栏:挂上去等于让访客点进两个空页面。数据属于内容生产,需你提供真实素材(见 §8);素材就位后按 §7.5 的命令挂到「关于」下拉下即可。
4. 主题出厂演示数据修复 【实测】
4.1 主题设置的存储位置与写入端点(实测)
| 项 | 结论 |
|---|---|
| 存储 | ConfigMap Ethereal-configMap,data 的每个键是一个紧凑 JSON 字符串(键 = 设置组名,共 10 组) |
| 写入端点 | PUT /api/v1alpha1/configmaps/Ethereal-configMap(带 metadata.version,实测 HTTP 200,version 2 → 3) |
| 快照端点 | GET /apis/api.console.halo.run/v1alpha1/themes/Ethereal/setting(Setting 资源,含大量 formSchema,不含 configVersion) |
⚠️ MCP 主题工具本轮不可用【实测】:
halo_update_theme_setting_group要求themeName是小写 DNS label,传ethereal时被拒:CONFLICT: The active theme changed; expected ethereal, actual Ethereal。 本实例主题名是大写开头的Ethereal,故该工具无法用于本实例,改用上述 ConfigMap PUT。
4.2 改动前后对照(原值快照)
改动前的完整主题配置已导出为快照文件(可用于回滚): scripts/out/snapshot-20260913T125003Z-theme-config.json
| 设置组.字段 | 原值(改动前) | 新值(改动后) | 判定依据 |
|---|---|---|---|
style.bannerText.title | Hello,Ethereal! | 俗世客的思行小筑 | 主题出厂演示标题 |
sidebar.profile.name | *未设置*(走 schema 默认 NanNan) | 俗世客 | demo 昵称;NanNan 与站点身份无关 |
sidebar.profile.bio | *未设置*(走 schema 默认 Lorem ipsum dolor sit amet, consectetur adipiscing elit.) | 记录技术实践与思考的个人小筑。 | 占位拉丁文 |
links.ownerInfo.owner_name | 博客名称 | 俗世客的思行小筑 | 占位 |
links.ownerInfo.owner_description | 这是一个基于 Halo 搭建的个人博客 | 记录技术实践、项目与思考 | 占位 |
links.ownerInfo.owner_url | https://example.com | https://<服务名>.tunnel.sushike.cloud/ | example.com 占位 |
links.ownerInfo.owner_rss | https://example.com/rss.xml | https://<服务名>.tunnel.sushike.cloud/rss.xml | example.com 占位 |
关于后两项值的依据:站点真实标题为「俗世客的思行小筑」(来自
configmaps/system→basic.title,实测);页脚已有真实署名「俗世客的思行小筑」(未改动)。昵称取「俗世客」、【推断】是站名与署名的自然简称;简介与描述的具体措辞为【推断】(主题要求一个非空简介,本轮给了一句与站点定位一致的中性描述,可随时改)。
4.3 前台验证:演示文案确已消失 【实测】
改后匿名抓取首页 HTML 做关键词计数:
| 关键词 | 改动前 | 改动后 |
|---|---|---|
Hello,Ethereal! | 1 | 0 |
Lorem ipsum | 1 | 0 |
俗世客的思行小筑 | 1 | 8 |
记录技术实践与思考的个人小筑。 | 0 | 2(新简介已渲染进侧边栏) |
NanNan | 3 | 3 |
demo-avatar | 5 | 5 |
demo-banner | 13 | 13 |
关于残留的 3 处 NanNan【实测】——逐处定位后确认都不是个人简介小组件的昵称:
- ×2:页脚
Powered by Halo & Ethereal的链接https://github.com/AloneNanNan/halo-theme-ethereal—— 这是主题作者的 GitHub 账号,属主题署名,不是演示数据; - ×1:一言(hitokoto)小组件 JS 里的默认兜底值
"楠南NanNan"—— 主题内置常量,该组件未启用。
侧边栏昵称确已变为「俗世客」(首页 HTML 中「俗世客」共 11 处)。
4.4 明确没有改的演示值(及理由)
| 字段 | 现值 | 为什么不改 |
|---|---|---|
sidebar.profile.avatar | /themes/Ethereal/assets/images/demo-avatar.png | 需要真实头像素材,不伪造 → §8 |
style.bannerStyle.src | /themes/Ethereal/assets/images/demo-banner.png | 需要真实横幅图素材,不伪造 → §8 |
sidebar.announcement.content | 欢迎来到我的博客!这是一则示例公告。 | 该公告 enable=false(未启用),前台不可见 |
layout.welcome.title 等 | 欢迎来到我的博客! | 欢迎弹窗 enable=false(未启用),前台不可见 |
style.bannerStyle.credit.text | © 2022 Ethereal | 横幅版权 enable=false(未启用),前台不可见 |
style.bannerText.subtitles | 生而为人,爱而无畏\n心之所向,素履以往\n保持热爱,奔赴山海 | 中文文案,不是明显的占位符,无法判定为演示值【未确认】 |
| 页脚署名「俗世客的思行小筑」 | — | 任务要求:不得改动 |
5. external-url 诊断 —— 与任务描述不符,结论:当前并未指向内网 【实测】
本轮只诊断、未做任何修改。
5.1 存在两处 external-url,且它们的值不一致
| # | 位置 | 值 | 证据 |
|---|---|---|---|
| A | 容器启动参数 --halo.external-url | http://<内网IP>:28090/(内网) | ① 基线文档记录 compose command:--halo.external-url=http://<内网IP>:28090/;② 本轮 /actuator/env 的 commandLineArgs 属性源中确实存在 halo.external-url(值被 actuator 脱敏为 ******) |
| B | 系统设置 configmaps/system → basic.externalUrl | https://<服务名>.tunnel.sushike.cloud/(公网) | GET /api/v1alpha1/configmaps/system,实测读回 |
5.2 实际生效的是 B(公网域名)—— 有直接证据
生成绝对 URL 的功能是读取系统设置(B),不是启动参数:
| 输出 | 实测结果 |
|---|---|
GET /rss.xml | 200,其中站点 URL 为 https://<服务名>.tunnel.sushike.cloud/、https://<服务名>.tunnel.sushike.cloud/rss.xml |
GET /feed.xml | 200,同上(公网域名) |
GET /sitemap.xml | 200,全部 89 条 URL 的 host 均为 <服务名>.tunnel.sushike.cloud |
对全部设置做全文检索:内网地址 <内网IP> 出现 0 次(/api/v1alpha1/settings 全文)。
结论:任务描述中的「
external-url当前指向内网地址」与实测不符。指向内网的是容器启动参数 A,而实际决定前台输出的是系统设置 B,已是公网域名。因此 RSS / Sitemap / 分享链接 / 附件 URL 当前不会生成内网地址。
5.3 改与不改的取舍建议(交由你决定)
| 方案 | 说明 | 利 | 弊/风险 |
|---|---|---|---|
| 不改(推荐) | 保持现状:启动参数仍是内网,系统设置为公网 | 零风险、零停机;当前所有前台输出正确 | 启动参数与生效值长期不一致,属"隐性债务":若系统设置被重置(恢复备份、重新初始化、configmaps/system 被覆盖),URL 会退回内网地址,且故障只在 RSS/Sitemap/分享卡片上暴露 |
| 改 compose(把启动参数同步成公网域名,或整行删除) | 需编辑 NAS 上的 docker-compose.yaml 并 重建容器(restart 不重读 command) | 两处配置一致,消除隐性债务 | 本任务明确禁止重启容器/服务;且这是 NAS 上的生产实例,重建属停机操作 |
建议:本轮不改。若日后要改,建议删除该启动参数整行(而不是改成公网域名),让系统设置成为唯一真相来源——两处都写反而又多一处需要同步的地方。 如果要改,仅在维护窗口执行(本轮未执行,命令供参考):
# NAS 上的 docker-compose.yaml,halo 服务的 command 段:删除下面这一行 - --halo.external-url=http://<内网IP>:28090/改后:
docker compose up -d --force-recreate halo,然后用 §6 的命令复验 RSS/Sitemap 的 host 仍为公网域名。
6. 匿名前台可访问性验证(验收判据)【实测】
方式:不带任何 Authorization 头的请求(脚本 configure_site_display.py verify,含"不跟随重定向"处理)。
6.1 内网入口 http://<内网IP>:28090
| 路径 | 状态码 | 大小 | 关键片段 |
|---|---|---|---|
/(首页) | 200 | 129426 | 含「俗世客的思行小筑」×8、「俗世客」×11 |
/docs(MiniDocs 知识库) | 200 | 58065 | 含「技术文档库」×3、「知识库」×16、搜索组件 md-search ×10 |
/portfolio(项目集) | 200 | 127353 | 含 「项目作品集」×3、「共 0 个项目」×1 |
/about(关于) | 200 | 128120 | — |
/skills(技能栈,模板 skills) | 200 | 124567 | 含 「技术技能」 |
/timeline(履历,模板 timeline) | 200 | 125460 | 含 timeline ×16 |
/rss.xml | 200 | 1079 | 含 <服务名>.tunnel.sushike.cloud(公网域名) |
6.2 公网隧道入口 https://<服务名>.tunnel.sushike.cloud
| 路径 | 状态码 |
|---|---|
/ | 200 |
/docs | 200 |
/portfolio | 200 |
/about | 200 |
/skills | 200 |
6.3 匿名访问 API 与前台页面的行为差异【实测】
任务要求"分别判断",实测结果如下(必须禁止跟随重定向,否则 302 会被跟成登录页的 200 而误判为"可读"):
| 路径 | 匿名状态码 | 说明 |
|---|---|---|
/apis/content.halo.run/v1alpha1/singlepages | 302 → /login?authentication_required | 内容 API 需登录 |
/apis/api.console.halo.run/v1alpha1/plugins | 302 → /login?authentication_required | 控制台 API 需登录 |
/apis/uc.api.halo.run/v1alpha1/users | 302 → /login?authentication_required | 用户中心 API 需登录 |
/console/ | 302 → /login?authentication_required | 控制台需登录 |
/apis/api.halo.run/v1alpha1/menus/- | 200 | 该公开聚合 API 允许匿名读(前台导航需要它) |
| 所有前台页面路径(§6.1) | 200 | 前台页面匿名可读 |
即:**
/apis/*匿名返回 302 到登录页是预期行为**(个别公开聚合 API 除外),它与前台页面的 200 互不矛盾,验收时应分别判断。
7. 回滚步骤
脚本:H:\Works\halo-kb\scripts\configure_site_display.py(幂等,支持 --dry-run,token 从环境变量 HALO_TOKEN 读取,不落盘)。 快照:H:\Works\halo-kb\scripts\out\snapshot-20260913T125003Z-*.json(改动前导出)。
运行前置(token 只在进程内):
[Console]::OutputEncoding=[System.Text.Encoding]::UTF8 # 否则中文输出乱码
$env:PYTHONIOENCODING='utf-8'
$env:HALO_TOKEN=[Environment]::GetEnvironmentVariable('HALO_TOKEN','User')
7.1 回滚主题设置(一键,已验证 dry-run 只命中被改的 3 个组)
python H:\Works\halo-kb\scripts\configure_site_display.py restore-theme `
--from H:\Works\halo-kb\scripts\out\snapshot-20260913T125003Z-theme-config.json
# 先看会改什么(不提交):
# --dry-run -> 实测输出「将恢复组:links, sidebar, style」
7.2 回滚新增的菜单项(删除 2 项即可,原 4 项未被改动)
$t=[Environment]::GetEnvironmentVariable('HALO_TOKEN','User')
curl.exe -s -X DELETE --noproxy "*" -H "Authorization: Bearer $t" `
http://<内网IP>:28090/api/v1alpha1/menuitems/1c023cd8-efb3-412a-936d-540ea88dd48f # 文档
curl.exe -s -X DELETE --noproxy "*" -H "Authorization: Bearer $t" `
http://<内网IP>:28090/api/v1alpha1/menuitems/efdb2d64-a1ac-412d-993c-5833e0df8820 # 项目
7.3 回滚新增的页面
& H:\Works\halo-cli\prefix\halo.cmd single-page delete skills --force
& H:\Works\halo-cli\prefix\halo.cmd single-page delete timeline --force
7.4 (可选)把「文档 / 项目」前移到设计建议的位置
本轮未做。方法是改 4 个既有菜单项的 priority(这是修改既有数据,请自行确认):
| displayName | 现 priority | 目标 priority |
|---|---|---|
| 首页 | 0 | 0 |
| 文档 | 4 | 1 |
| 项目 | 5 | 2 |
| 文章 | 1 | 3 |
| 默认分类 | 2 | 4 |
| 关于 | 3 | 5 |
写法:PUT /api/v1alpha1/menuitems/{name},body 为该菜单项的完整对象(spec.priority 改值,metadata.version 用读回的最新值)。
7.5 (可选)把技能页 / 履历页挂到「关于」下拉下
素材就位后,用 §2.1 的 POST /api/v1alpha1/menuitems 建两个子项:
{"apiVersion":"v1alpha1","kind":"MenuItem","metadata":{"name":"<uuid4>"},
"spec":{"displayName":"技能栈","href":"/skills","menuName":"primary","parent":"b0d041fa-dc99-48f6-a193-8604003379cf","priority":0}}
(parent 填「关于」项的 metadata.name;/timeline 同理。)
8. 待用户提供素材清单
| # | 项 | 需要的素材 | 就位后怎么用 |
|---|---|---|---|
| 1 | 站点头像 | 一张正方形头像图 | 上传到附件 → 把 URL 填入主题设置 sidebar.profile.avatar(现为 demo-avatar.png) |
| 2 | 首页横幅图 | 一张横向大图(建议 1920×1080 以上) | 填入 style.bannerStyle.src(现为 demo-banner.png) |
| 3 | 项目集内容 | 每个项目的名称、简介、封面、技术栈、仓库/下载链接 | 在「项目集」插件后台录入;否则 /portfolio 一直是「共 0 个项目」 |
| 4 | 技能页数据 | 技能名 / 分类 / 熟练度 / 年限 | 主题设置 → 扩展页面 → 技能页面(extendPages.skills,现为 {}) |
| 5 | 履历时间线数据 | 教育/工作/项目经历及起止时间 | 主题设置 → 扩展页面 → 时间轴页面(extendPages.timeline,现为 {}) |
| 6 | 「关于」页正文 | 自我介绍、技术方向、联系方式 | 现仍是 Halo 演示文案「这是一个自定义页面…」【实测,未改】 |
| 7 | 个人简介措辞确认 | 昵称「俗世客」与简介「记录技术实践与思考的个人小筑。」是否合适 | 如不合适,改 sidebar.profile.name / .bio |
9. 未确认项与遗留
| # | 项 | 状态 |
|---|---|---|
| 1 | style.bannerText.subtitles(生而为人,爱而无畏/心之所向,素履以往/保持热爱,奔赴山海)是否为主题出厂演示值 | 【未确认】 中文文案,无法判定,未改 |
| 2 | 未启用的三个演示文案(公告 / 欢迎弹窗 / 横幅版权)是否也要清理 | 未改(前台不可见)。你若要一并清掉,改动位置见 §4.4 |
| 3 | Menu.spec.menuItems 与前台渲染的确切关系 | 【实测】 前台不依赖它(新建项未写入该数组,导航仍已出现)。它是否为其它入口(如控制台菜单管理)所用,未确认 |
| 4 | 项目集插件是否有 /portfolio-detail 之类详情路由 | 【未确认】 无项目数据,无法验证详情页 |
| 5 | 主题 extendPages 全部子字段 | 【未确认】 现值为 {},无样本;字段名以 §8 引用主题设置面板为准 |
| 6 | /skills、/timeline 空数据时的观感 | 【未确认】 页面能开、模板生效,但列表为空,实际观感需你目视确认后决定是否挂导航 |
| 7 | 容器启动参数 halo.external-url 的确切值 | 【推断】 值取自基线文档记录的 compose;本轮 /actuator/env 只确认该键存在(值被脱敏) |
10. 本次改动与验证方式(契约要求)
10.1 改动了什么
Halo 实例(全部为写操作,均已实测回读确认):
- 新增 2 个菜单项:文档 →
/docs、项目 →/portfolio(POST /api/v1alpha1/menuitems,各 HTTP 201); - 新建并发布 2 个独立页面:
skills(技能栈,模板skills)、timeline(履历,模板timeline); - 修改主题设置 3 个组共 7 个字段:
style.bannerText.title、sidebar.profile.{name,bio}、links.ownerInfo.{owner_name,owner_description,owner_url,owner_rss}(PUT /api/v1alpha1/configmaps/Ethereal-configMap,HTTP 200,version 2 → 3)。
本地新增文件(均在 H:\Works\halo-kb\ 内,未写到任务根目录之外):
| 文件 | 说明 |
|---|---|
页面与展示配置记录.md | 本报告 |
scripts\configure_site_display.py | 配置脚本(幂等、--dry-run、token 取自 HALO_TOKEN) |
scripts\out\snapshot-20260913T125003Z-{menu,menuitems,singlepages,theme-config,system-configmap}.json | 改动前的快照(回滚依据) |
未做(硬约束):未启用 plugin-docsme;未改动任何插件设置;未删除任何既有页面/菜单/内容;未重启容器或服务;未改动页脚真实署名;未替换头像/横幅图。
10.2 如何验证
[Console]::OutputEncoding=[System.Text.Encoding]::UTF8
$env:PYTHONIOENCODING='utf-8'
$env:HALO_TOKEN=[Environment]::GetEnvironmentVariable('HALO_TOKEN','User')
# ① 幂等性:三个动作重复执行都不应产生改动
python H:\Works\halo-kb\scripts\configure_site_display.py menus
python H:\Works\halo-kb\scripts\configure_site_display.py theme
# ② 验收判据:匿名(不带 Authorization)验证全部前台入口
python H:\Works\halo-kb\scripts\configure_site_display.py verify
# 期望:/、/docs、/portfolio、/about、/skills、/timeline、/rss.xml 全部 [OK] 200,
# 且 /portfolio 命中片段「项目作品集」、/skills 命中「技术技能」、
# /rss.xml 命中「<服务名>.tunnel.sushike.cloud」;结论行「全部通过」
# ③ 回滚演练(不提交)
python H:\Works\halo-kb\scripts\configure_site_display.py restore-theme `
--from H:\Works\halo-kb\scripts\out\snapshot-20260913T125003Z-theme-config.json --dry-run
# 期望:仅列出 links, sidebar, style 三个组
本轮实测输出:menus 与 theme 重复执行均报「无需改动(幂等)」;verify 结论为「全部通过」;restore-theme --dry-run 输出「将恢复组:links, sidebar, style」。