来源: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 / halodbhalohalodb
容器名HaloPostgreSQL
镜像registry.fit2cloud.com/halo/halo-pro:2.26postgres: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(来自镜像环境)
JVMJDK 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.confweekly + include /etc/logrotate.d,本文件里的 daily 会覆盖它。

★★ 本次演练抓到一个真错误,值得单独记下来。 我第一版写的是 daily + size 20Mlogrotate -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 里的 tagregistry.fit2cloud.com/halo/halo-pro:2.26滚动 tag
实际运行版本2.26.1
本机镜像2.26 → digest sha256:982c8944…,IMAGE ID a6291a728a30CREATED 12 days ago(≈ 2026-09-01)
PostgreSQLpostgres: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 定时备份

用户级 crontabZYJ)。理由(均由实测支撑):backups/ 属主是 ZYJ(700); ZYJdocker 组(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, 而脚本依赖 dockertarsha256sumstat,漏了会表现为「手工能跑、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/readiness200 {"status":"UP"}依赖就绪(DB 等)—— 容器 healthcheck 用的就是它
/actuator/health/liveness200 {"status":"UP"}进程存活
/actuator/health200 {"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 Kumalouislam/uptime-kuma0.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 /portfolio200真实路由
GET /projects404不存在

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/menus404
/apis/api.console.halo.run/v1alpha1/menus404
/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/menuitems200 / 201
菜单项树(控制台用)GET /apis/api.console.halo.run/v1alpha1/menuitems/-/tree?menuName=<name>400(缺参)→ 说明端点存在

⚠️ 注意菜单项 CRUD 不在 /apis/...,而在 /api/v1alpha1/...(无 group 段)。这一点极易踩空。

MenuItemspec 字段(来自官方 api-client 类型定义,与实测写回一致): displayNamehrefmenuNamepriorityparentchildren[]targettargetRef

2.2 改动前(原菜单完整快照,用于回滚)

GET /api/v1alpha1/menuitemstotal=4)+ GET /api/v1alpha1/menus/primary

顺序metadata.namedisplayNamehrefprioritytargetRef
088c3f10b-321c-4092-86a8-70db00251b74首页/0
1c4c814d1-0c2c-456b-8c96-4864965fee94文章/archives1
235869bd3-33b5-448b-91ee-cf6517a59644默认分类/categories/default2Category/76514a40-6ef1-4ed9-b58a-e26945bde3ca
3b0d041fa-dc99-48f6-a193-8604003379cf关于/about3SinglePage/373a5f79-f44f-441a-9df1-85a4f553ece8

Menu.primary 原值:{"displayName":"主菜单","menuItems":[上面 4 个 name]}metadata.version=0

2.3 改动后

新增 2 项(POST /api/v1alpha1/menuitems,均返回 HTTP 201):

displayNamehrefprioritymetadata.name
文档/docs41c023cd8-efb3-412a-936d-540ea88dd48f
项目/portfolio5efdb2d64-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.htmltimeline.htmlportfolio.htmlportfolio-detail.htmlhalo_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

模板确实生效的实测证据(匿名抓取):

路径状态码关键片段
/skills200<title>技能栈 - 俗世客的思行小筑</title>,HTML 含 「技术技能」(= 主题 extendPages.skills.subtitle 的默认值「我的技术技能和专业知识」)
/timeline200<title>履历 - 俗世客的思行小筑</title>,HTML 含 timeline 标识 16 处

「关于」页保持默认模板 page.htmlspec.template=""),未改动——设计文档 E④ 对它的要求是"正文自己写",不属于模板配置范围。

3.3 这两个页面的展示数据仍为空 【实测】

主题设置 extendPages 组的实际值为 {}(空对象),即:

  • 技能页没有技能条目 → 页面能打开,但列表为空;
  • 履历页没有时间线条目 → 同上。

因此本轮没有把这两页挂进导航栏:挂上去等于让访客点进两个空页面。数据属于内容生产,需你提供真实素材(见 §8);素材就位后按 §7.5 的命令挂到「关于」下拉下即可。


4. 主题出厂演示数据修复 【实测】

4.1 主题设置的存储位置与写入端点(实测)

结论
存储ConfigMap Ethereal-configMapdata 的每个键是一个紧凑 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.titleHello,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_urlhttps://example.comhttps://<服务名>.tunnel.sushike.cloud/example.com 占位
links.ownerInfo.owner_rsshttps://example.com/rss.xmlhttps://<服务名>.tunnel.sushike.cloud/rss.xmlexample.com 占位

关于后两项值的依据:站点真实标题为「俗世客的思行小筑」(来自 configmaps/systembasic.title实测);页脚已有真实署名「俗世客的思行小筑」(未改动)。昵称取「俗世客」、【推断】是站名与署名的自然简称;简介与描述的具体措辞为【推断】(主题要求一个非空简介,本轮给了一句与站点定位一致的中性描述,可随时改)。

4.3 前台验证:演示文案确已消失 【实测】

改后匿名抓取首页 HTML 做关键词计数:

关键词改动前改动后
Hello,Ethereal!10
Lorem ipsum10
俗世客的思行小筑18
记录技术实践与思考的个人小筑。02(新简介已渲染进侧边栏)
NanNan33
demo-avatar55
demo-banner1313

关于残留的 3 处 NanNan【实测】——逐处定位后确认都不是个人简介小组件的昵称

  1. ×2:页脚 Powered by Halo & Ethereal 的链接 https://github.com/AloneNanNan/halo-theme-ethereal —— 这是主题作者的 GitHub 账号,属主题署名,不是演示数据;
  2. ×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-urlhttp://<内网IP>:28090/内网① 基线文档记录 compose command--halo.external-url=http://<内网IP>:28090/;② 本轮 /actuator/envcommandLineArgs 属性源中确实存在 halo.external-url(值被 actuator 脱敏为 ******
B系统设置 configmaps/systembasic.externalUrlhttps://<服务名>.tunnel.sushike.cloud/公网GET /api/v1alpha1/configmaps/system,实测读回

5.2 实际生效的是 B(公网域名)—— 有直接证据

生成绝对 URL 的功能是读取系统设置(B),不是启动参数:

输出实测结果
GET /rss.xml200,其中站点 URL 为 https://<服务名>.tunnel.sushike.cloud/https://<服务名>.tunnel.sushike.cloud/rss.xml
GET /feed.xml200,同上(公网域名)
GET /sitemap.xml200,全部 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

路径状态码大小关键片段
/(首页)200129426含「俗世客的思行小筑」×8、「俗世客」×11
/docs(MiniDocs 知识库)20058065含「技术文档库」×3、「知识库」×16、搜索组件 md-search ×10
/portfolio(项目集)200127353「项目作品集」×3、「共 0 个项目」×1
/about(关于)200128120
/skills(技能栈,模板 skills200124567「技术技能」
/timeline(履历,模板 timeline200125460timeline ×16
/rss.xml2001079<服务名>.tunnel.sushike.cloud(公网域名)

6.2 公网隧道入口 https://<服务名>.tunnel.sushike.cloud

路径状态码
/200
/docs200
/portfolio200
/about200
/skills200

6.3 匿名访问 API 与前台页面的行为差异【实测】

任务要求"分别判断",实测结果如下(必须禁止跟随重定向,否则 302 会被跟成登录页的 200 而误判为"可读"):

路径匿名状态码说明
/apis/content.halo.run/v1alpha1/singlepages302/login?authentication_required内容 API 需登录
/apis/api.console.halo.run/v1alpha1/plugins302/login?authentication_required控制台 API 需登录
/apis/uc.api.halo.run/v1alpha1/users302/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
首页00
文档41
项目52
文章13
默认分类24
关于35

写法: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. 未确认项与遗留

#状态
1style.bannerText.subtitles生而为人,爱而无畏/心之所向,素履以往/保持热爱,奔赴山海)是否为主题出厂演示值【未确认】 中文文案,无法判定,未改
2未启用的三个演示文案(公告 / 欢迎弹窗 / 横幅版权)是否也要清理未改(前台不可见)。你若要一并清掉,改动位置见 §4.4
3Menu.spec.menuItems 与前台渲染的确切关系【实测】 前台不依赖它(新建项未写入该数组,导航仍已出现)。它是否为其它入口(如控制台菜单管理)所用,未确认
4项目集插件是否有 /portfolio-detail 之类详情路由【未确认】 无项目数据,无法验证详情页
5主题 extendPages 全部子字段【未确认】 现值为 {},无样本;字段名以 §8 引用主题设置面板为准
6/skills/timeline 空数据时的观感【未确认】 页面能开、模板生效,但列表为空,实际观感需你目视确认后决定是否挂导航
7容器启动参数 halo.external-url 的确切值【推断】 值取自基线文档记录的 compose;本轮 /actuator/env 只确认该键存在(值被脱敏)

10. 本次改动与验证方式(契约要求)

10.1 改动了什么

Halo 实例(全部为写操作,均已实测回读确认):

  1. 新增 2 个菜单项:文档 → /docs、项目 → /portfolioPOST /api/v1alpha1/menuitems,各 HTTP 201);
  2. 新建并发布 2 个独立页面:skills(技能栈,模板 skills)、timeline(履历,模板 timeline);
  3. 修改主题设置 3 个组共 7 个字段:style.bannerText.titlesidebar.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 三个组

本轮实测输出menustheme 重复执行均报「无需改动(幂等)」;verify 结论为「全部通过」;restore-theme --dry-run 输出「将恢复组:links, sidebar, style」。

技术文档库 / 部署与升级 0 0 sushike
2026-09-13T12:41:58.206209562Z 2026-09-13T13:37:21.877097046Z