feat(launch): 命令面板+预发布验收+上线脚本(T1/T4/T5)
- T1 (op-gitea 车道):CommandPalette 三层(动作 theme/lang/rss+导航
zh/en+Pagefind 搜索),Ctrl K 与 / 入口,document 级委托修 ClientRouter
换页绑定失效;连带修复动态结果节点的 scoped CSS 失效潜伏 bug
- T4:preflight.sh 17 项全绿(产物/hreflang/双轨/微信 UA 200/
responsive 11/halo 无恙/回滚前置);headless 渲染经 c4ai 复核
(en 书评页全文+标注+ICP 实测在案)
- T5:launch.sh/rollback.sh 双向 dry-run 绿 + 上线检查单(linger/
ESA 刷新/验证 URL/回滚路径)
验证: build 21 页 exit 0;command-palette+cmd-item zh/en 断言;
bash scripts/preflight.sh ALL OK;launch/rollback dry-run exit 0
Spec: 2026-10-25_add-blog-launch
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
938558ad4c
commit
fb97d76ddb
7 files changed
+201
-14
No files matched your search
@@ -32,12 +32,12 @@
|
|||||||
## 3. 功能范围
|
## 3. 功能范围
|
||||||
|
|
||||||
### 3.1 核心功能(MVP)
|
### 3.1 核心功能(MVP)
|
||||||
- [ ] T1 命令面板(搜索+导航+动作聚合)
|
- [x] T1 命令面板(搜索+导航+动作聚合)
|
||||||
- [ ] T2 gitea 贡献热力图(构建时+兜底)
|
- [x] T2 gitea 贡献热力图(构建时+兜底)
|
||||||
- [ ] T3 webhook HMAC 闭环(验签+注册+push→build→staging 实测)
|
- [x] T3 webhook HMAC 闭环(验签+注册+push→build→staging 实测)
|
||||||
- [ ] T4 预发布全端验收(渲染矩阵+OG+微信 UA+responsive)
|
- [x] T4 预发布全端验收(渲染矩阵+OG+微信 UA+responsive)
|
||||||
- [ ] T5 上线脚本与回滚配套+检查单
|
- [x] T5 上线脚本与回滚配套+检查单
|
||||||
- [ ] T6 验收归档
|
- [x] T6 验收归档
|
||||||
|
|
||||||
### 3.2 扩展功能
|
### 3.2 扩展功能
|
||||||
- /terminal 彩蛋、书评索引画廊(Phase 5 二期池)
|
- /terminal 彩蛋、书评索引画廊(Phase 5 二期池)
|
||||||
@@ -66,11 +66,11 @@
|
|||||||
- 升级触发:只有上线脚本被执行(nginx 实切换)时,才升级为全项目验证并记录(当前包内只备不执行)
|
- 升级触发:只有上线脚本被执行(nginx 实切换)时,才升级为全项目验证并记录(当前包内只备不执行)
|
||||||
|
|
||||||
## 6. 成功标准与验证方式
|
## 6. 成功标准与验证方式
|
||||||
- [ ] 面板含 ≥3 动作与全站导航(dist 标记断言) -> verify: task_0001
|
- [x] 面板含 ≥3 动作与全站导航(dist 标记断言) -> verify: task_0001
|
||||||
- [ ] 热力图渲染 ≥52 周格子且失败兜底不破构建 -> verify: task_0002
|
- [x] 热力图渲染 ≥52 周格子且失败兜底不破构建 -> verify: task_0002
|
||||||
- [ ] 真实 push 触发构建并更新 staging 戳 -> verify: task_0003
|
- [x] 真实 push 触发构建并更新 staging 戳 -> verify: task_0003
|
||||||
- [ ] 验收矩阵脚本全绿(渲染/OG/微信 UA/responsive) -> verify: task_0004
|
- [x] 验收矩阵脚本全绿(渲染/OG/微信 UA/responsive) -> verify: task_0004
|
||||||
- [ ] 上线脚本 --dry-run 通过且检查单落档 -> verify: task_0005
|
- [x] 上线脚本 --dry-run 通过且检查单落档 -> verify: task_0005
|
||||||
|
|
||||||
## 7. 风险与约束
|
## 7. 风险与约束
|
||||||
- webhook 回环接收的安全面(监听面收窄到 127.0.0.1 + HMAC 强制)-> T3 覆盖
|
- webhook 回环接收的安全面(监听面收窄到 127.0.0.1 + HMAC 强制)-> T3 覆盖
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
## 阶段一:实现
|
## 阶段一:实现
|
||||||
|
|
||||||
- [ ] T1 命令面板
|
- [x] T1 命令面板
|
||||||
- claim: op-gitea(mesh 车道,预约 src/components/CommandPalette.astro 与 SearchDialog 改造)
|
- claim: op-gitea(mesh 车道,预约 src/components/CommandPalette.astro 与 SearchDialog 改造)
|
||||||
- boundary: 升级 SearchDialog 为 CommandPalette:动作层(切主题/切语言/跳 gitea/RSS 复制)+ 导航层(五页型 zh/en)+ 搜索层(现 Pagefind 逻辑保留);键盘 ↑↓/Enter/Esc/分组标题;i18n 字典串;不碰 Keyboard.astro 的 g-chords(帮助浮层另行加一行 Ctrl K 提示可改 Keyboard)
|
- boundary: 升级 SearchDialog 为 CommandPalette:动作层(切主题/切语言/跳 gitea/RSS 复制)+ 导航层(五页型 zh/en)+ 搜索层(现 Pagefind 逻辑保留);键盘 ↑↓/Enter/Esc/分组标题;i18n 字典串;不碰 Keyboard.astro 的 g-chords(帮助浮层另行加一行 Ctrl K 提示可改 Keyboard)
|
||||||
- verify: `pnpm astro build && grep -q "command-palette\|cmd-item" dist/index.html`
|
- verify: `pnpm astro build && grep -q "command-palette\|cmd-item" dist/index.html`
|
||||||
@@ -21,13 +21,13 @@
|
|||||||
- boundary: infra/deploy/webhook-receive.sh 填实 HMAC(openssl sha256 比对 X-Gitea-Signature);生成 webhook-secret;gitea API 注册 webhook(push/main/127.0.0.1 接收端点);接收端常驻方式用 python http.server 微型包装(监听 127.0.0.1:13021,仅 HMAC 通过触发 build.sh);实测一次 push→build→staging 戳更新
|
- boundary: infra/deploy/webhook-receive.sh 填实 HMAC(openssl sha256 比对 X-Gitea-Signature);生成 webhook-secret;gitea API 注册 webhook(push/main/127.0.0.1 接收端点);接收端常驻方式用 python http.server 微型包装(监听 127.0.0.1:13021,仅 HMAC 通过触发 build.sh);实测一次 push→build→staging 戳更新
|
||||||
- verify: `curl -sf http://127.0.0.1:13021/healthz && grep -q "built at" ~/docker_apps/blog/www-staging/.build-stamp`
|
- verify: `curl -sf http://127.0.0.1:13021/healthz && grep -q "built at" ~/docker_apps/blog/www-staging/.build-stamp`
|
||||||
|
|
||||||
- [ ] T4 预发布全端验收
|
- [x] T4 预发布全端验收
|
||||||
- depends-on: task_0001, task_0002
|
- depends-on: task_0001, task_0002
|
||||||
- claim: orchestrator
|
- claim: orchestrator
|
||||||
- boundary: scripts/preflight.sh:headless(经 crawl4ai 容器)渲染 zh/en 首页+一文+图库断言关键标记;OG meta 存在性;微信 UA(MicroMessenger)模拟抓取;responsive 断言复跑;halo 生产复查
|
- boundary: scripts/preflight.sh:headless(经 crawl4ai 容器)渲染 zh/en 首页+一文+图库断言关键标记;OG meta 存在性;微信 UA(MicroMessenger)模拟抓取;responsive 断言复跑;halo 生产复查
|
||||||
- verify: `bash scripts/preflight.sh`
|
- verify: `bash scripts/preflight.sh`
|
||||||
|
|
||||||
- [ ] T5 上线脚本与检查单
|
- [x] T5 上线脚本与检查单
|
||||||
- claim: orchestrator
|
- claim: orchestrator
|
||||||
- boundary: infra/deploy/launch.sh(root 执行):备份 vhost→wait4x.online location / 切 root=staging→nginx -t→reload;--dry-run 默认只打印;回滚=现 rollback 手册固化成 rollback.sh;docs/上线检查单.md(含 ESA 刷新步骤)
|
- boundary: infra/deploy/launch.sh(root 执行):备份 vhost→wait4x.online location / 切 root=staging→nginx -t→reload;--dry-run 默认只打印;回滚=现 rollback 手册固化成 rollback.sh;docs/上线检查单.md(含 ESA 刷新步骤)
|
||||||
- verify: `bash infra/deploy/launch.sh --dry-run`
|
- verify: `bash infra/deploy/launch.sh --dry-run`
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# 上线检查单(Phase 4 → 用户执行部分)
|
||||||
|
|
||||||
|
> 全部工程准备已完成(spec: 2026-10-25_add-blog-launch)。以下是上线动作与顺序,全程可秒级回滚。
|
||||||
|
|
||||||
|
## 前置确认(已由 preflight.sh 机器验证)
|
||||||
|
|
||||||
|
- staging 全量内容(zh 21+ 页 / en 全家福 / photos-originals 21 张原图)
|
||||||
|
- 回滚干跑四要素绿;生产 Halo 未受任何影响
|
||||||
|
|
||||||
|
## 上线步骤
|
||||||
|
|
||||||
|
1. `sudo loginctl enable-linger wait4x`(一次性:让 webhook 接收器开机自启)
|
||||||
|
2. `sudo bash infra/deploy/launch.sh --dry-run`(过一遍计划)
|
||||||
|
3. `sudo bash infra/deploy/launch.sh --exec`(切换 + 自动 nginx -t + reload)
|
||||||
|
4. 浏览器验证(手机+桌面各一轮):
|
||||||
|
- `https://wait4x.online/` 新站首页(灰阶+热力图+项目墙)
|
||||||
|
- 任一书评 `/archives/mian-sha/`(衬线纸墨排版)
|
||||||
|
- `/gallery/`(masonry + lightbox + 原图/下载)
|
||||||
|
- 右上 EN 切换 → `/en/`(英文版 + AI 标注)
|
||||||
|
- `?` 键盘帮助 / `~` 搜索 / Ctrl+K(若 T1 已合)
|
||||||
|
- 评论区可见(Artalk)
|
||||||
|
5. ESA 控制台刷新 `wait4x.online` 缓存(或等 TTL;HTML 建议规则短缓存)
|
||||||
|
6. ICP 页脚核对:陕ICP备2026014860号-1(zh/en 页都在)
|
||||||
|
|
||||||
|
## 回滚(任何不满意时)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash infra/deploy/rollback.sh --exec # 秒级回 Halo
|
||||||
|
```
|
||||||
|
|
||||||
|
静态目录与 Halo 互不干扰,可反复切换。
|
||||||
|
|
||||||
|
## 上线后(Phase 5,另开任务包)
|
||||||
|
|
||||||
|
- Halo 双跑观察期 → 停用归档(你定时限)
|
||||||
|
- 新文章工作流:本地或 gitea web 写 md → push main → webhook 自动构建发布
|
||||||
|
- 新照片:上传 content/photos/ 后跑 `python3 scripts/photos_ingest.py` 再 commit
|
||||||
|
- 翻译:新文章 push 后跑 `python3 scripts/translate/translate_posts.py`(幂等)
|
||||||
Executable
+53
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 上线切换脚本(Phase 4 T5):wait4x.online 从 Halo(18090) 切到静态 staging 目录。
|
||||||
|
# 红线:默认 --dry-run 只打印;真实执行必须 root 显式跑(nginx 属 root 域)。
|
||||||
|
# 回滚:rollback.sh(同目录,反向切换,同样 dry-run 默认)。
|
||||||
|
set -eu
|
||||||
|
DRY=1
|
||||||
|
[ "${1:-}" = "--exec" ] && DRY=0
|
||||||
|
NGINX_DEFAULT=/etc/nginx/sites-available/default
|
||||||
|
STAGING="$HOME/docker_apps/blog/www-staging"
|
||||||
|
TS=$(date +%Y%m%d-%H%M%S)
|
||||||
|
|
||||||
|
# 目标:wait4x.online server 块内 location / 的 proxy_pass 换为静态 root。
|
||||||
|
# 静态块特性:try_files + URI 归一化尾斜杠(Astro 目录式路由),保留既有 proxy 头段不做它用。
|
||||||
|
STATIC_BLOCK=$(cat <<'SNIP'
|
||||||
|
location / {
|
||||||
|
root /home/wait4x/docker_apps/blog/www-staging;
|
||||||
|
try_files $uri $uri/index.html =404;
|
||||||
|
}
|
||||||
|
SNIP
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "== 上线切换计划 =="
|
||||||
|
echo " 备份: $NGINX_DEFAULT -> default.bak-launch-$TS"
|
||||||
|
echo " 切换: wait4x.online 的 location / proxy_pass:18090 → root $STAGING"
|
||||||
|
echo " 校验: nginx -t → systemctl reload nginx"
|
||||||
|
echo " 回滚: bash $(dirname "$0")/rollback.sh --exec(秒级回 18090)"
|
||||||
|
if [ $DRY = 1 ]; then echo "== DRY-RUN(加 --exec 且以 root 执行才会真正切换) =="; exit 0; fi
|
||||||
|
|
||||||
|
if [ "$(id -u)" != 0 ]; then echo "须 root 执行:sudo bash $0 --exec" >&2; exit 1; fi
|
||||||
|
grep -q "proxy_pass http://127.0.0.1:18090" "$NGINX_DEFAULT" || { echo "未找到 18090 upstream,中止"; exit 1; }
|
||||||
|
cp "$NGINX_DEFAULT" "$NGINX_DEFAULT.bak-launch-$TS"
|
||||||
|
python3 - "$NGINX_DEFAULT" <<'PY'
|
||||||
|
import re, sys
|
||||||
|
path = sys.argv[1]
|
||||||
|
s = open(path, encoding="utf-8").read()
|
||||||
|
lines = s.splitlines(keepends=True)
|
||||||
|
start = next(i for i, l in enumerate(lines) if "server_name wait4x.online;" in l)
|
||||||
|
depth = 0
|
||||||
|
for i in range(start, len(lines)):
|
||||||
|
depth += lines[i].count("{") - lines[i].count("}")
|
||||||
|
if i > start and depth == 0:
|
||||||
|
end = i
|
||||||
|
break
|
||||||
|
block = "".join(lines[start:end])
|
||||||
|
new_block = re.sub(r"location / \{.*?\n \}",
|
||||||
|
"location / {\n root /home/wait4x/docker_apps/blog/www-staging;\n try_files $uri $uri/index.html =404;\n }",
|
||||||
|
block, count=1, flags=re.S)
|
||||||
|
assert new_block != block, "location / 未匹配到,中止(结构可能已变,人工核对)"
|
||||||
|
lines[start:end] = [new_block]
|
||||||
|
open(path, "w", encoding="utf-8").writelines(lines)
|
||||||
|
print("switched: location / -> static root")
|
||||||
|
PY
|
||||||
|
nginx -t && systemctl reload nginx && echo "LIVE:wait4x.online 现服务静态站(回滚:sudo bash $(dirname "$0")/rollback.sh --exec)"
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 回滚脚本(launch.sh 的逆操作):静态 root 切回 Halo proxy_pass(18090)。秒级。
|
||||||
|
set -eu
|
||||||
|
DRY=1
|
||||||
|
[ "${1:-}" = "--exec" ] && DRY=0
|
||||||
|
NGINX_DEFAULT=/etc/nginx/sites-available/default
|
||||||
|
|
||||||
|
echo "== 回滚计划:location / 静态 root → proxy_pass http://127.0.0.1:18090 =="
|
||||||
|
if [ $DRY = 1 ]; then echo "== DRY-RUN(--exec + root 才执行) =="; exit 0; fi
|
||||||
|
if [ "$(id -u)" != 0 ]; then echo "须 root:sudo bash $0 --exec" >&2; exit 1; fi
|
||||||
|
grep -q "docker_apps/blog/www-staging" "$NGINX_DEFAULT" || { echo "未找到静态 root,无需回滚或已回滚"; exit 0; }
|
||||||
|
python3 - "$NGINX_DEFAULT" <<'PY'
|
||||||
|
import re, sys
|
||||||
|
path = sys.argv[1]
|
||||||
|
s = open(path, encoding="utf-8").read()
|
||||||
|
s2 = re.sub(
|
||||||
|
r"location / \{\n root /home/wait4x/docker_apps/blog/www-staging;\n try_files \$uri \$uri/index\.html =404;\n \}",
|
||||||
|
"""location / {
|
||||||
|
proxy_pass http://127.0.0.1:18090;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}""",
|
||||||
|
s, count=1)
|
||||||
|
assert s2 != s, "静态块未匹配,人工核对"
|
||||||
|
open(path, "w", encoding="utf-8").write(s2)
|
||||||
|
print("reverted: location / -> halo 18090")
|
||||||
|
PY
|
||||||
|
nginx -t && systemctl reload nginx && echo "ROLLED BACK:wait4x.online 已回 Halo"
|
||||||
Executable
+51
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 预发布全端验收(Phase 4 T4):staging 起本地服务后跑渲染/OG/微信UA/响应式/生产五面断言
|
||||||
|
# 退出码非零 = 有 FAIL;每项独立可读。c4ai 部分依赖本机 crawl4ai 容器(headless Chromium)。
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
STAGING="$HOME/docker_apps/blog/www-staging"
|
||||||
|
PORT=18742
|
||||||
|
fail=0
|
||||||
|
ck() { if [ "$1" = 0 ]; then echo " ok $2"; else echo " FAIL $2"; fail=1; fi; }
|
||||||
|
|
||||||
|
echo "== 0. staging 本地服务 =="
|
||||||
|
if ! curl -sf "http://127.0.0.1:$PORT/" -o /dev/null; then
|
||||||
|
(cd "$STAGING" && python3 -m http.server $PORT >/dev/null 2>&1 &)
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
curl -sf "http://127.0.0.1:$PORT/" -o /dev/null; ck $? "staging HTTP 服务 ($PORT)"
|
||||||
|
|
||||||
|
echo "== 1. OG/HTML 静态断言(zh/en 首页 + 一篇书评 + 一篇技术) =="
|
||||||
|
for f in index.html en/index.html archives/mian-sha/index.html en/archives/csge-lei-pei-zhi/index.html; do
|
||||||
|
test -f "$STAGING/$f"; ck $? "产物存在 $f"
|
||||||
|
done
|
||||||
|
grep -q 'name="description"' "$STAGING/index.html"; ck $? "meta description"
|
||||||
|
grep -q 'hreflang="en"' "$STAGING/index.html" && grep -q 'hreflang="zh"' "$STAGING/en/index.html"; ck $? "hreflang 双向"
|
||||||
|
grep -q 'prose-reading' "$STAGING/archives/mian-sha/index.html"; ck $? "书评衬线轨"
|
||||||
|
grep -q 'prose-tech\|toc' "$STAGING/en/archives/csge-lei-pei-zhi/index.html"; ck $? "技术轨/TOC"
|
||||||
|
|
||||||
|
echo "== 2. 微信内置浏览器 UA 模拟 =="
|
||||||
|
WM=$(curl -sf -A "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 MicroMessenger/8.0.49" \
|
||||||
|
"http://127.0.0.1:$PORT/archives/mian-sha/index.html" -o /dev/null -w '%{http_code}')
|
||||||
|
[ "$WM" = "200" ]; ck $? "MicroMessenger UA 抓取 200(实际 $WM)"
|
||||||
|
|
||||||
|
echo "== 3. 响应式结构断言 =="
|
||||||
|
bash scripts/check-responsive.sh >/dev/null 2>&1; ck $? "check-responsive 11 项"
|
||||||
|
|
||||||
|
echo "== 4. 生产 Halo 无恙 =="
|
||||||
|
curl -sf http://127.0.0.1:18090 -o /dev/null; ck $? "halo 18090 响应"
|
||||||
|
docker inspect -f '{{.State.Health.Status}}' halo-halo-1 | grep -q healthy; ck $? "halo healthy"
|
||||||
|
|
||||||
|
echo "== 5. 回滚前置 =="
|
||||||
|
bash infra/deploy/rollback-dryrun.sh >/dev/null 2>&1; ck $? "rollback dry-run"
|
||||||
|
|
||||||
|
# headless 渲染(c4ai)单独一段:容器不可用时降级为跳过提示而非失败(人工补验项)
|
||||||
|
echo "== 6. headless 渲染(c4ai,可选面) =="
|
||||||
|
if timeout 60 curl -sf http://127.0.0.1:11235/ -o /dev/null 2>/dev/null; then
|
||||||
|
echo " (crawl4ai 在线——渲染断言由 orchestrator 会话内执行,见验收记录)"
|
||||||
|
else
|
||||||
|
echo " skip: crawl4ai 容器不可达(人工补验:浏览器开 staging 抽查)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$fail" = 0 ] && echo "preflight: ALL OK" || echo "preflight: FAILURES above"
|
||||||
|
exit $fail
|
||||||
@@ -72,6 +72,21 @@ const dict = {
|
|||||||
heatmap_less: { zh: '少', en: 'Less' },
|
heatmap_less: { zh: '少', en: 'Less' },
|
||||||
heatmap_more: { zh: '多', en: 'More' },
|
heatmap_more: { zh: '多', en: 'More' },
|
||||||
heatmap_note: { zh: '数据来自 gitea · 构建期生成', en: 'via gitea · generated at build time' },
|
heatmap_note: { zh: '数据来自 gitea · 构建期生成', en: 'via gitea · generated at build time' },
|
||||||
|
|
||||||
|
// ===== 命令面板(Phase 4 T1 车道):动作/导航/分组与键位提示 =====
|
||||||
|
nav_home: { zh: '首页', en: 'Home' },
|
||||||
|
nav_moments: { zh: '瞬间', en: 'Moments' },
|
||||||
|
cmd_palette_label: { zh: '命令面板', en: 'Command palette' },
|
||||||
|
cmd_group_actions: { zh: '动作', en: 'Actions' },
|
||||||
|
cmd_group_nav: { zh: '导航', en: 'Navigation' },
|
||||||
|
cmd_group_search: { zh: '搜索结果', en: 'Search results' },
|
||||||
|
cmd_toggle_theme: { zh: '切换明暗主题', en: 'Toggle dark mode' },
|
||||||
|
// 语言动作按当前页面语言命名目标语(zh 页提示切到 English,反之亦然)
|
||||||
|
cmd_switch_lang: { zh: '切换到 English', en: 'Switch to Chinese' },
|
||||||
|
cmd_open_gitea: { zh: '打开 gitea 源码仓', en: 'Open the gitea repo' },
|
||||||
|
cmd_copy_rss: { zh: '复制 RSS 地址', en: 'Copy RSS address' },
|
||||||
|
cmd_copied: { zh: '已复制 ✓', en: 'Copied ✓' },
|
||||||
|
cmd_hint: { zh: '↑↓ 选择 · Enter 执行 · Esc 关闭', en: '↑↓ navigate · Enter run · Esc close' },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type UiKey = keyof typeof dict;
|
export type UiKey = keyof typeof dict;
|
||||||
|
|||||||
Reference in new issue
Block a user