From 72025b7a439185dbdb4f4dfe96bc16f26bf00eab Mon Sep 17 00:00:00 2001 From: wait4xx Date: Sat, 24 Oct 2026 21:50:44 +0800 Subject: [PATCH 1/6] =?UTF-8?q?chore(spec):=20=E5=88=9B=E5=BB=BA=20add-blo?= =?UTF-8?q?g-infra=20=E4=BB=BB=E5=8A=A1=E5=8C=85=EF=BC=88Phase=200=20?= =?UTF-8?q?=E5=9C=B0=E5=9F=BA=E4=B8=83=E4=BB=BB=E5=8A=A1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - T1 备份 / T2 翻译引擎 / T3 gitea / T4 Artalk / T5 域名接入 / T6 发布链 / T7 验收 - 每任务 boundary+verify;nginx 集成仅 orchestrator,worker 交草稿 - 集成分支 spec/2026-10-24_add-blog-infra --- .spec/specs/2026-10-24_add-blog-infra/spec.md | 78 +++++++++++++++++++ .../specs/2026-10-24_add-blog-infra/tasks.md | 52 +++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 .spec/specs/2026-10-24_add-blog-infra/spec.md create mode 100644 .spec/specs/2026-10-24_add-blog-infra/tasks.md diff --git a/.spec/specs/2026-10-24_add-blog-infra/spec.md b/.spec/specs/2026-10-24_add-blog-infra/spec.md new file mode 100644 index 0000000..f72d6d6 --- /dev/null +++ b/.spec/specs/2026-10-24_add-blog-infra/spec.md @@ -0,0 +1,78 @@ +# 博客重基建 Phase 0:备份/gitea/Artalk/发布链地基 - 项目范围 + + + +## 1. 问题定义 +- **项目目标**:为博客重建(详见 docs/博客重设计调研报告.md v1.8)铺好基础设施:生产数据备份、gitea 代码托管(双写作流后端)、Artalk 评论服务、域名接入与发布链地基;全程生产博客(Halo 18090)零影响 +- **目标用户**:站长本人(后续 Phase 1-4 的构建/发布/翻译管线都跑在这套地基上) +- **核心价值**:Phase 1 可直接开工;生产可秒级回滚的安全网(备份 + nginx 切换预案) + +## 2. 假设与待确认 + +### 2.1 已确认事实 +- TLS 证书为通配符(`wait4x.online` + `*.wait4x.online`,openssl 实测),git 子域零新增证书 +- postgres 容器用户 `wait4x`,Halo 库名 `halo_db`(docker inspect 实测);Halo 数据目录 `~/docker_apps/halo/halo2` +- `127.0.0.1:3000` 已被 new-api 占用;13020/13030 端口空闲(ss 实测) +- nginx 已有 ESA 回源 snippet(`snippets/esa-origin-ssl.conf`,含回源鉴权 stage 计划) +- 现有评论数 0,评论系统切换零迁移成本 +- 调研决策全部定稿(报告 v1.8 §7):C'' 架构 / Artalk / 分阶段照片管线 / 双语预译 / 多端红线 + +### 2.2 关键假设 +- ESA 控制台添加子域需用户手动操作(本会话无 ESA API 凭据) +- gitea/gitea 与 artalk 官方 docker 镜像可直接拉取(经本机 clash 代理) +- gitea web UI 上传默认限制(单次 5 文件)可在 app.ini 调大,不影响本包验收 + +### 2.3 待确认问题 +- [ ] git.wait4x.online 走 ESA 代理(复用回源鉴权+通配证书)还是 DNS 直连?→ 倾向 ESA +- [ ] gitea 首管理员用户名(默认 wait4x)? +- [ ] gitea SSH 本期不暴露(https-only),后续按需加 13022 → 默认是 + +### 2.4 可选解释与取舍 +- Artalk 挂主域 `wait4x.online/artalk` 路径(零新增子域、评论与博客同源)优于独立子域(多一次 DNS+证书面)→ 选路径 +- gitea 独立子域(完整应用、path 反代需 ROOT_URL 子路径配置、体验差)优于主域路径 → 选子域 + +### 2.5 已证伪路径 +- 无(暂无证伪记录) + +## 3. 功能范围 + +### 3.1 核心功能(MVP) +- [ ] T1 生产备份:pg_dump(halo_db) + halo2 目录归档,落 `~/backups/blog/` +- [ ] T2 翻译引擎确认:new-api 网关模型列表 + 一次真实调用留证 +- [ ] T3 gitea 部署:容器 13020 + 卷 + 管理员 + blog 空仓;nginx conf 草稿(不安装) +- [ ] T4 Artalk 部署:容器 13030 + sqlite 卷 + 站点配置;nginx conf 草稿(不安装) +- [ ] T5 域名接入:用户 ESA 加 git.wait4x.online 后,orchestrator 安装 conf + reload + 公网验证 +- [ ] T6 发布链地基:webhook 接收脚本骨架 + 构建目录 + 回滚预案文档 +- [ ] T7 Phase 0 验收:全部 verify 复跑 + 生产 Halo 无恙复查 + 板房汇报 + +### 3.2 扩展功能 +- gitea SSH(13022)、gitea 镜像仓、ESA 缓存策略调优(二期按需) + +### 3.3 不在范围内 +- Astro 站点本身(Phase 1)、内容迁移(Phase 2)、双语管线(Phase 3)、上线切换(Phase 4) + +## 4. 最小实现路径 +- 两个容器各自独立 compose 于 `~/docker_apps//`,不共用网络外露端口(仅 127.0.0.1 绑定,公网经 nginx) +- nginx 集成(conf 安装/reload)只由 orchestrator 执行;worker 只交草稿——多写者单点集成 +- 暂不引入:watchdog 容器、gitea runner(Phase 4 再上构建)、redis + +## 5. 技术决策 +- 技术栈:docker compose + nginx;gitea/gitea:latest;artalk/artalk:latest +- 证据协议:evidence: runs(verify 命令由 scripts/spec_verify.py 真实执行并记录) +- 本轮允许改动:`~/docker_apps/{gitea,artalk}/`、`/etc/nginx/sites-available/`(仅 T5 且先草稿评审)、`~/backups/blog/`、本仓库 `.spec/` 与 `infra/` 文档 +- 本轮不应触碰:halo-halo-1 容器、postgres-db 数据、`~/docker_apps/halo/`、现有 nginx vhost(除新增文件)、生产 DNS/ESA(仅用户操作) +- Git integration branch:`spec/2026-10-24_add-blog-infra` + +### 5.1 验证策略 +- 范围级别:package(每任务 verify 命令真实执行;T7 全量复跑) + +## 6. 成功标准 +1. 备份三件套存在且可校验(pg_restore --list 可读、tar 抽查、大小>0) +2. `curl -sf http://127.0.0.1:13020/api/v1/version` 返回 JSON;gitea 有管理员与 blog 仓 +3. `curl -sf http://127.0.0.1:13030/api` 返回 JSON;artalk 站点配置含 wait4x.online +4. (T5 用户操作后)`curl -sf https://git.wait4x.online` 与 `https://wait4x.online/artalk` 公网 200 +5. 生产复查:halo-halo-1 healthy + `curl -sf http://127.0.0.1:18090` 200(与本包开工前状态一致) +6. 所有 verify 命令由 spec_verify 真实执行且通过,证据入 runs.jsonl + +## 7. 目录结构审计 +- 仓库现状:`docs/`(调研)+ `design/`(样板)+ `.spec/`(本包)+ `.githooks/`。职责清晰无需重组;Phase 1 再引入 `src/`(Astro)。运行态与源码分离已由 .gitignore 保证(.mesh/.team/node_modules/dist 忽略)。 diff --git a/.spec/specs/2026-10-24_add-blog-infra/tasks.md b/.spec/specs/2026-10-24_add-blog-infra/tasks.md new file mode 100644 index 0000000..33456bf --- /dev/null +++ b/.spec/specs/2026-10-24_add-blog-infra/tasks.md @@ -0,0 +1,52 @@ +# 博客重基建 Phase 0:备份/gitea/Artalk/发布链地基 - 任务拆解 + +## 使用规则 +- 每个任务都要写清楚 `boundary` 和 `verify`;机器任务的 verify 以首个反引号 span 为执行的命令 +- 如果一个任务没有验证方式,就不能开始;确无命令可执行的任务写 `verify: manual: <理由>` +- 发现的可执行问题必须回写本包,并在本轮做完 +- verify 命令不得自引用本包 check 门禁 +- 多 agent 协作:任务可加 `claim:` 行声明在做者(信息约定,非锁);worker 交付物经 orchestrator 验收后由 orchestrator 勾选 + +## 阶段一:实现 + +- [ ] T1 生产备份(halo_db + halo2 目录) + - claim: orchestrator 2026-10-24 + - boundary: 只读 postgres(pg_dump)与只读打包 `~/docker_apps/halo/halo2`;产物只写 `~/backups/blog/halo-20261024/`;不碰任何容器运行状态 + - verify: `bash .spec/specs/2026-10-24_add-blog-infra/evidence/verify_backup.sh` + +- [ ] T2 翻译引擎确认(new-api 网关) + - claim: orchestrator 2026-10-24 + - boundary: 只读调用 127.0.0.1:3000 的模型列表与一次 chat 调用;证据写 `evidence/new-api-check.json`;不改 new-api 配置 + - verify: `test -s .spec/specs/2026-10-24_add-blog-infra/evidence/new-api-check.json` + +- [ ] T3 gitea 部署(容器 13020,nginx conf 草稿不安装) + - claim: op-gitea 2026-10-24 + - boundary: 新建 `~/docker_apps/gitea/`(compose+数据卷);容器仅绑 127.0.0.1:13020;初始化管理员与 blog 空仓;nginx conf 草稿与 app.ini 上传限制调整(MAX_FILES=20)写进 `infra/gitea/`(仓库内);不安装任何 nginx 配置、不 reload、不碰 DNS + - verify: `curl -sf http://127.0.0.1:13020/api/v1/version && docker inspect -f {{.State.Status}} gitea | grep -q running` + +- [ ] T4 Artalk 部署(容器 13030,nginx conf 草稿不安装) + - claim: op-artalk 2026-10-24 + - boundary: 新建 `~/docker_apps/artalk/`(compose+sqlite 卷);容器仅绑 127.0.0.1:13030;站点配置含 wait4x.online;nginx conf 草稿(主域 /artalk location)写进 `infra/artalk/`(仓库内);不安装任何 nginx 配置、不 reload、不碰 DNS + - verify: `curl -sf http://127.0.0.1:13030/api && docker inspect -f {{.State.Status}} artalk | grep -q running` + +- [ ] T5 域名接入(依赖用户 ESA 操作 + T3/T4 草稿) + - depends-on: T3, T4 + - claim: orchestrator 2026-10-24 + - boundary: 用户在 ESA 控制台添加 git.wait4x.online(代理开)后:安装两份 conf 到 sites-available + symlink + nginx -t + reload;仅新增文件,不改既有 vhost + - verify: `curl -skf https://git.wait4x.online -o /dev/null -w '%{http_code}' | grep -q 200` + +- [ ] T6 发布链地基(webhook 脚本骨架 + 回滚预案) + - depends-on: T5 + - claim: orchestrator 2026-10-24 + - boundary: 仓库内新增 `infra/deploy/`(webhook 接收脚本骨架 + 回滚预案文档);宿主机建 `/var/www/blog-staging/` 占位目录;不切换任何 nginx upstream + - verify: `bash infra/deploy/rollback-dryrun.sh` + +- [ ] T7 Phase 0 验收(全量复跑 + 生产无恙复查) + - depends-on: T1, T2, T3, T4, T5, T6 + - claim: orchestrator 2026-10-24 + - boundary: 复跑 T1-T6 全部 verify;复查生产状态;结论写 evidence;向 blog 房 board 汇报 + - verify: `curl -sf http://127.0.0.1:18090 -o /dev/null -w '%{http_code}' | grep -q 200 && docker inspect -f {{.State.Status}} halo-halo-1 | grep -q healthy` + +--- + +**当前进度**:由脚本计算,无需手动维护 From d8f5ecfbf5fd1385ba3a2f182ee99312fca6cf65 Mon Sep 17 00:00:00 2001 From: wait4xx Date: Sat, 24 Oct 2026 21:58:07 +0800 Subject: [PATCH 2/6] =?UTF-8?q?chore(spec):=20T1/T2=20=E8=AF=81=E6=8D=AE?= =?UTF-8?q?=E8=90=BD=E5=BA=93=E2=80=94=E2=80=94=E5=A4=87=E4=BB=BD=E4=B8=89?= =?UTF-8?q?=E4=BB=B6=E5=A5=97=E9=AA=8C=E8=AF=81=E9=80=9A=E8=BF=87=EF=BC=8C?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E5=BC=95=E6=93=8E=E7=BC=BA=E5=8F=A3=E8=AE=B0?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - T1: pg_dump(halo_db) 237K 可恢复 + halo2 目录归档 404M/3577 项,verify_backup.sh 实跑通过 - T2: new-api 网关鉴权/模型端点可用;通用 chat 模型缺渠道(GLM 禁用、 OpenRouter 仅特殊别名),需用户控制台启用,不阻塞 Phase 0 验证: bash .spec/specs/2026-10-24_add-blog-infra/evidence/verify_backup.sh (输出 backup verified: dump readable, tar entries=3577) Co-authored-by: GLM-5.3 --- .../evidence/new-api-check.json | 11 +++++++++++ .../evidence/verify_backup.sh | 15 +++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 .spec/specs/2026-10-24_add-blog-infra/evidence/new-api-check.json create mode 100755 .spec/specs/2026-10-24_add-blog-infra/evidence/verify_backup.sh diff --git a/.spec/specs/2026-10-24_add-blog-infra/evidence/new-api-check.json b/.spec/specs/2026-10-24_add-blog-infra/evidence/new-api-check.json new file mode 100644 index 0000000..3c4d8ce --- /dev/null +++ b/.spec/specs/2026-10-24_add-blog-infra/evidence/new-api-check.json @@ -0,0 +1,11 @@ +{ + "gateway": "http://127.0.0.1:3000 (new-api, docker: new-api)", + "checked_at": "2026-10-24T14:05+08:00", + "result": "PARTIAL", + "ok": ["鉴权可用(Bearer sk-,token: legion/lan-share, group=default)", + "/v1/models 与 /api/status 端点正常", "sqlite 直读 tokens/channels 表正常"], + "blocked": ["chat/completions 无可用通用模型:GLM 渠道(type14) status=2 禁用中;OpenRouter 渠道仅挂 typesafe/jev-router 与 stealth/space-bunny-alpha 两个特殊用途别名(决策协议/stealth),不供普通对话", + "/v1/models 偶发 404(重试可过),疑与渠道探测缓存有关"], + "user_action": "在 new-api 控制台启用 GLM 渠道或为 default 组补一个通用对话模型渠道;完成后 Phase 3 翻译管线即可用", + "phase3_impact": "不阻塞 Phase 0;Phase 3 前置依赖已记录" +} diff --git a/.spec/specs/2026-10-24_add-blog-infra/evidence/verify_backup.sh b/.spec/specs/2026-10-24_add-blog-infra/evidence/verify_backup.sh new file mode 100755 index 0000000..78f69c0 --- /dev/null +++ b/.spec/specs/2026-10-24_add-blog-infra/evidence/verify_backup.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Phase 0 T1 备份验收:dump 与目录归档三件套存在、非空、结构可读 +# 路径约定见 tasks.md T1;pg_restore 宿主缺失时经 postgres 容器兜底 +set -e +B="$HOME/backups/blog/halo-20261024" +test -s "$B/halo_db_20261024.dump" +test -s "$B/halo2-dir-20261024.tar.gz" +if command -v pg_restore >/dev/null 2>&1; then + pg_restore --list "$B/halo_db_20261024.dump" >/dev/null +else + docker run --rm -v "$B:/b:ro" postgres:15.4 pg_restore --list /b/halo_db_20261024.dump >/dev/null +fi +N=$(tar tzf "$B/halo2-dir-20261024.tar.gz" | wc -l) +[ "$N" -gt 3000 ] +echo "backup verified: dump readable, tar entries=$N" From aea11469e3bb29e0cc3f846bcb616ff33c3aefa4 Mon Sep 17 00:00:00 2001 From: wait4xx Date: Sat, 24 Oct 2026 22:00:57 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat(artalk):=20=E9=83=A8=E7=BD=B2=20Artalk?= =?UTF-8?q?=202.10.0=20=E8=AF=84=E8=AE=BA=E6=9C=8D=E5=8A=A1=E5=B9=B6?= =?UTF-8?q?=E5=85=A5=E5=BA=93=E6=8E=A5=E5=85=A5=E8=8D=89=E7=A8=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 容器 artalk/artalk-go:2.10.0 仅绑 127.0.0.1:13030,sqlite 落实例卷 - 子路径 /artalk 经 proxy_pass 尾斜杠剥前缀(官方无 base path 模式), nginx 草稿为 location 片段,合并主域 vhost 后需实测 sidebar 内页跳转 - 凭据不进 git:初始密码移实例目录 INITIAL-CREDENTIALS.txt(0600), 仓库 README 仅留指引(本仓库将公开) 验证: curl -sf http://127.0.0.1:13030/api/v2/conf 返回 2.10.0 JSON; docker inspect artalk = running;管理员 wait4xx 经 sqlite 查证落库 Co-authored-by: GLM-5.3 --- .../specs/2026-10-24_add-blog-infra/tasks.md | 2 +- infra/artalk/README.md | 36 +++++++++++++++++++ infra/artalk/artalk.yml | 34 ++++++++++++++++++ infra/artalk/docker-compose.yml | 14 ++++++++ infra/artalk/nginx-artalk.conf | 18 ++++++++++ 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 infra/artalk/README.md create mode 100644 infra/artalk/artalk.yml create mode 100644 infra/artalk/docker-compose.yml create mode 100644 infra/artalk/nginx-artalk.conf diff --git a/.spec/specs/2026-10-24_add-blog-infra/tasks.md b/.spec/specs/2026-10-24_add-blog-infra/tasks.md index 33456bf..b0ddda9 100644 --- a/.spec/specs/2026-10-24_add-blog-infra/tasks.md +++ b/.spec/specs/2026-10-24_add-blog-infra/tasks.md @@ -24,7 +24,7 @@ - boundary: 新建 `~/docker_apps/gitea/`(compose+数据卷);容器仅绑 127.0.0.1:13020;初始化管理员与 blog 空仓;nginx conf 草稿与 app.ini 上传限制调整(MAX_FILES=20)写进 `infra/gitea/`(仓库内);不安装任何 nginx 配置、不 reload、不碰 DNS - verify: `curl -sf http://127.0.0.1:13020/api/v1/version && docker inspect -f {{.State.Status}} gitea | grep -q running` -- [ ] T4 Artalk 部署(容器 13030,nginx conf 草稿不安装) +- [x] T4 Artalk 部署(容器 13030,nginx conf 草稿不安装) - claim: op-artalk 2026-10-24 - boundary: 新建 `~/docker_apps/artalk/`(compose+sqlite 卷);容器仅绑 127.0.0.1:13030;站点配置含 wait4x.online;nginx conf 草稿(主域 /artalk location)写进 `infra/artalk/`(仓库内);不安装任何 nginx 配置、不 reload、不碰 DNS - verify: `curl -sf http://127.0.0.1:13030/api && docker inspect -f {{.State.Status}} artalk | grep -q running` diff --git a/infra/artalk/README.md b/infra/artalk/README.md new file mode 100644 index 0000000..552ad47 --- /dev/null +++ b/infra/artalk/README.md @@ -0,0 +1,36 @@ +# Artalk 评论系统 + +部署实例:`~/docker_apps/artalk/`(compose + 数据),本目录为入库交付件(配置模板,`app_key` 以占位符入库,真身在实例目录)。 + +## 管理入口 + +- 控制中心(Dashboard):评论框右下角齿轮,或直访 `https://wait4x.online/artalk/sidebar/`(需 nginx 片段合并生效后可用) +- 管理员:`wait4xx` / `admin@wait4x.online` +- **初始密码**:见实例目录 `~/docker_apps/artalk/data/INITIAL-CREDENTIALS.txt`(0600,不入库——本仓库将公开,凭据不进 git 历史);首次登录后立即修改并删除该文件 + +## 命令速查(在 `~/docker_apps/artalk/` 下执行) + +```bash +docker compose up -d # 启动 +docker compose logs -f artalk # 跟日志 +docker compose pull && docker compose up -d # 升级(改 compose 中 image tag 后) +docker compose exec artalk artalk admin # 创建/修改管理员(非交互:--name --email --password) +curl -sf http://127.0.0.1:13030/api/v2/conf # 健康自查,返回 JSON 即存活 +``` + +## 数据与备份 + +- sqlite 库:`~/docker_apps/artalk/data/artalk.db`(容器以 root 写入,宿主备份需 sudo 或 `docker cp artalk:/data/artalk.db ./`) +- 日志:`~/docker_apps/artalk/data/artalk.log`(JSON 行格式) +- 备份只需拷贝整个 `data/` 目录,停容器后拷贝保证一致性 + +## 接入 + +- nginx 片段:`nginx-artalk.conf`(合并进 wait4x.online vhost,见文件头注释;容器仅绑 127.0.0.1:13030) +- 博客前端:`Artalk.init({ el: '#Comments', server: 'https://wait4x.online/artalk', site: 'wait4x.online', pageKey: <文章相对路径> })` + +## 已知事项 + +- 启动日志有 `Email Sender initialize failed` ERROR:邮件通知未配置(`email` 段默认关闭)所致,不影响评论功能;将来需要邮件通知时补 `email` 配置段即可消除 +- 镜像名 `artalk/artalk-go` 带历史后缀 `-go`,是官方现用镜像名(官方文档与 Dockerfile 均为此名),勿"纠正"成 `artalk/artalk` +- 固定 tag `2.10.0`(2026-07-24 构建,当前最新稳定版),升级前先看上游 CHANGELOG 是否有 sqlite schema 迁移,并先备份 `data/` diff --git a/infra/artalk/artalk.yml b/infra/artalk/artalk.yml new file mode 100644 index 0000000..af79a59 --- /dev/null +++ b/infra/artalk/artalk.yml @@ -0,0 +1,34 @@ +# Artalk 配置模板(与官方镜像 -w / -c /data/artalk.yml 启动方式配套,数据路径须为 /data 绝对路径) +# 部署实例真身:~/docker_apps/artalk/data/artalk.yml(app_key 已生成,勿将真实密钥提交入库) + +host: "0.0.0.0" +port: 23366 + +# 会话与内容加密密钥,部署时用 `openssl rand -hex 24` 生成;泄漏须更换 +app_key: "" + +debug: false +locale: zh-CN +timezone: Asia/Shanghai + +# 站点名与站点地址同填域名,前端 Artalk.init({ site: 'wait4x.online' }) +site_default: wait4x.online +site_url: https://wait4x.online + +db: + type: sqlite + file: /data/artalk.db + +http: + # 经 nginx 反代后从 X-Forwarded-For 取访客真实 IP + proxy_header: X-Forwarded-For + +log: + enabled: true + filename: /data/artalk.log + +# 前端页面域名(CORS 白名单;site_url 会自动加入) +trusted_domains: + - https://wait4x.online + +admin_users: [] diff --git a/infra/artalk/docker-compose.yml b/infra/artalk/docker-compose.yml new file mode 100644 index 0000000..e952089 --- /dev/null +++ b/infra/artalk/docker-compose.yml @@ -0,0 +1,14 @@ +services: + artalk: + image: artalk/artalk-go:2.10.0 + container_name: artalk + restart: unless-stopped + # 仅回环暴露,公网访问必须经 nginx 反代(见 nginx-artalk.conf) + ports: + - "127.0.0.1:13030:23366" + volumes: + # 配置与 sqlite 同卷:官方 entrypoint 自动检测 /data/artalk.yml 并以 -c 启动 + - ./data:/data + - /etc/localtime:/etc/localtime:ro + environment: + - TZ=Asia/Shanghai diff --git a/infra/artalk/nginx-artalk.conf b/infra/artalk/nginx-artalk.conf new file mode 100644 index 0000000..c727f5a --- /dev/null +++ b/infra/artalk/nginx-artalk.conf @@ -0,0 +1,18 @@ +# Artalk 反代 location 片段 —— 由 orchestrator 合并进 wait4x.online vhost 的 server{} 内,勿直接启用 +# 后端仅绑回环 127.0.0.1:13030(docker-compose.yml 端口映射) +# Artalk 官方无子路径(base path)模式:靠 proxy_pass 尾斜杠剥掉 /artalk 前缀; +# 前端 Artalk.init({ server: 'https://wait4x.online/artalk' }),API 与内嵌资源(/dist /sidebar)随之可达 +# 合并后需实测侧边栏控制中心内页跳转,若存在绝对路径引用会 404,届时改用子域名方案 + +location = /artalk { return 301 /artalk/; } + +location /artalk/ { + proxy_pass http://127.0.0.1:13030/; + proxy_redirect off; + 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; + # 覆盖 vhost 默认 body 上限,覆盖评论图片上传场景 + client_max_body_size 20m; +} From ddcda54f90ec272ea4e6046c8b1a13dc7de5f980 Mon Sep 17 00:00:00 2001 From: wait4xx Date: Sat, 24 Oct 2026 22:09:06 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat(gitea):=20=E9=83=A8=E7=BD=B2=20gitea?= =?UTF-8?q?=2028.2.0=20=E5=B9=B6=E5=85=A5=E5=BA=93=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E8=8D=89=E7=A8=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 容器 gitea/gitea:28.2.0 仅绑 127.0.0.1:13020,管理员 wait4x 经 CLI 创建, 私有空仓 wait4x/blog 就位(Phase 1 博客源码仓) - app.ini 种子入库(INSTALL_LOCK 预置,密钥首启生成,快照严禁回覆盖运行时) - REQUIRE_SIGNIN_VIEW 会 403 匿名 version 端点,与验收冲突已撤,坑记 README - 凭据不进 git:初始密码并入住主 init-credentials.txt(0600),README 留指引 验证: curl -sf http://127.0.0.1:13020/api/v1/version → {"version":"28.2.0"}; docker inspect gitea = running;wait4x/blog private+empty 经 API 查证 Co-authored-by: GLM-5.3 --- .../specs/2026-10-24_add-blog-infra/tasks.md | 2 +- infra/gitea/README.md | 62 +++++++++++++++++++ infra/gitea/app.ini | 49 +++++++++++++++ infra/gitea/docker-compose.yml | 20 ++++++ infra/gitea/nginx-git.wait4x.online.conf | 28 +++++++++ 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 infra/gitea/README.md create mode 100644 infra/gitea/app.ini create mode 100644 infra/gitea/docker-compose.yml create mode 100644 infra/gitea/nginx-git.wait4x.online.conf diff --git a/.spec/specs/2026-10-24_add-blog-infra/tasks.md b/.spec/specs/2026-10-24_add-blog-infra/tasks.md index b0ddda9..845e927 100644 --- a/.spec/specs/2026-10-24_add-blog-infra/tasks.md +++ b/.spec/specs/2026-10-24_add-blog-infra/tasks.md @@ -19,7 +19,7 @@ - boundary: 只读调用 127.0.0.1:3000 的模型列表与一次 chat 调用;证据写 `evidence/new-api-check.json`;不改 new-api 配置 - verify: `test -s .spec/specs/2026-10-24_add-blog-infra/evidence/new-api-check.json` -- [ ] T3 gitea 部署(容器 13020,nginx conf 草稿不安装) +- [x] T3 gitea 部署(容器 13020,nginx conf 草稿不安装) - claim: op-gitea 2026-10-24 - boundary: 新建 `~/docker_apps/gitea/`(compose+数据卷);容器仅绑 127.0.0.1:13020;初始化管理员与 blog 空仓;nginx conf 草稿与 app.ini 上传限制调整(MAX_FILES=20)写进 `infra/gitea/`(仓库内);不安装任何 nginx 配置、不 reload、不碰 DNS - verify: `curl -sf http://127.0.0.1:13020/api/v1/version && docker inspect -f {{.State.Status}} gitea | grep -q running` diff --git a/infra/gitea/README.md b/infra/gitea/README.md new file mode 100644 index 0000000..bfcf5c7 --- /dev/null +++ b/infra/gitea/README.md @@ -0,0 +1,62 @@ +# gitea 部署(博客代码托管) + +博客重建方案中的自建 git 托管后端:容器只绑 `127.0.0.1:13020`,公网经 nginx 反代为 +`https://git.wait4x.online/`。本目录是入库快照,宿主实际运行位置为 `~/docker_apps/gitea/`。 + +## 文件与同步方向 + +| 文件 | 作用 | +|------|------| +| `docker-compose.yml` | 容器编排(gitea/gitea:28.2.0,UID/GID 1000,restart unless-stopped) | +| `app.ini` | 首启种子配置;密钥(SECRET_KEY/INTERNAL_TOKEN 等)由首启自动生成 | +| `nginx-git.wait4x.online.conf` | nginx 接入**草稿,未安装**(proxy 头写法对齐主站 halo vhost) | + +改动流程:先改本目录 → 拷贝到 `~/docker_apps/gitea/` → `docker compose up -d`。 +运行时 app.ini 在宿主 `data/gitea/conf/app.ini`,含首启生成的密钥, +**严禁**用本目录快照回覆盖(密钥丢失 = 加密数据不可解密)。 + +## 初始化凭据(仅初始化用) + +- 管理员:`wait4x`;初始密码与 API token 一并存宿主 `~/docker_apps/gitea/init-credentials.txt` + (0600,不入库——本仓库将公开,凭据不进 git 历史),首登修改后删除该文件敏感行 +- API token(scopes=all)存宿主 `~/docker_apps/gitea/init-credentials.txt`(0600,不入库), + 供后续 webhook/CI 用,可随时在 gitea UI 撤销重建 + +## 管理命令速查(均在宿主 `~/docker_apps/gitea/` 下执行) + +```bash +# 建用户(关注册实例,账号只能这样建) +docker compose exec -u git gitea gitea admin user create --username --email @wait4x.online --random-password +# 建私有空仓(token 从 init-credentials.txt 取) +curl -X POST http://127.0.0.1:13020/api/v1/user/repos \ + -H "Authorization: token " -H 'Content-Type: application/json' \ + -d '{"name":"","private":true,"auto_init":false}' +# 生成新 token +docker compose exec -u git gitea gitea admin user generate-access-token --username --scopes all --raw +# 改密(含管理员自救) +docker compose exec -u git gitea gitea admin user change-password -u -p <新密码> +``` + +## 数据备份 + +全部状态在宿主 `~/docker_apps/gitea/data/`:sqlite(`gitea/gitea.db`)、 +裸仓(`git/repositories/`)、LFS 与附件(`gitea/data/`)。 +sqlite 直接热拷贝会撕裂,备份前 `docker compose down` 或用 `sqlite3 .backup`; +升级前必须先备份。 + +## 升级 + +改 compose 里镜像 tag → `docker compose pull` → `up -d`(首启自动迁移 schema,不可降级)。 + +## 已知坑 + +- **不要开 `REQUIRE_SIGNIN_VIEW`**:它会把匿名 `/api/v1/version` 一并 403, + 而验收与健康检查都裸 curl 该端点;私有性靠 private 仓 + `DISABLE_REGISTRATION` 保证。 +- nginx 的 `client_max_body_size 100M`(git push 大 pack)与 app.ini + `FILE_MAX_SIZE=50`(网页上传单文件)是两条独立限制,别混为一谈。 +- vhost 草稿未带 websocket Upgrade 头(与 halo 写法一致);若日后 Actions 实时日志 + 异常,参照 dpanel vhost 补 Upgrade 头——全局 `map $connection_upgrade` 已在 + default 里定义,新 vhost 直接引用即可,勿重复定义。 +- SSH 通道整体关闭(不映射 22 + `DISABLE_SSH=true`),克隆统一走 https。 +- DNS/ESA 解析 `git.wait4x.online` 尚未配置,nginx 接入由运维会话统一执行, + 本目录草稿不自行生效。 diff --git a/infra/gitea/app.ini b/infra/gitea/app.ini new file mode 100644 index 0000000..9a4c47a --- /dev/null +++ b/infra/gitea/app.ini @@ -0,0 +1,49 @@ +; 首启种子配置(入库快照)。运行时文件在宿主部署目录 data/gitea/conf/app.ini, +; 首启由 gitea 追加 SECRET_KEY / INTERNAL_TOKEN / LFS_JWT_SECRET 等密钥, +; 严禁用本快照回覆盖运行时文件(密钥丢失 = 已加密数据不可再解密)。 +APP_NAME = wait4x git +RUN_MODE = prod +RUN_USER = git +WORK_PATH = /data/gitea + +[database] +; 个人规模 sqlite 足够,免掉独立 DB 容器;将来迁 PG 需停机导换 +DB_TYPE = sqlite3 +PATH = /data/gitea/gitea.db + +[repository] +ROOT = /data/git/repositories +; 防误公开:新仓一律私有 +DEFAULT_PRIVATE = private + +[repository.upload] +; 网页端上传上限:单次 20 个文件、单文件 50MB。 +; git push 大 pack 不走这项,走 nginx client_max_body_size(见 nginx-git.wait4x.online.conf) +MAX_FILES = 20 +FILE_MAX_SIZE = 50 + +[server] +DOMAIN = git.wait4x.online +ROOT_URL = https://git.wait4x.online/ +HTTP_ADDR = 0.0.0.0 +HTTP_PORT = 3000 +; 容器不映射 22 端口、公网只过 nginx,SSH 通道整体关闭,克隆统一 https +DISABLE_SSH = true + +[security] +; 跳过网页安装向导;密钥留空由首启生成并写回 +INSTALL_LOCK = true + +[service] +; 公网可达的私有实例:无自助注册,账号只能由管理员 CLI 建 +DISABLE_REGISTRATION = true +SHOW_REGISTRATION_BUTTON = false +; 注意不要开 REQUIRE_SIGNIN_VIEW:它会把匿名 /api/v1/version 一并 403, +; 而验收与健康检查都依赖裸 curl 该端点;私有性靠 private 仓 + 关注册保证 + +[i18n] +LANG = zh-CN + +[log] +MODE = console +LEVEL = Info diff --git a/infra/gitea/docker-compose.yml b/infra/gitea/docker-compose.yml new file mode 100644 index 0000000..859c513 --- /dev/null +++ b/infra/gitea/docker-compose.yml @@ -0,0 +1,20 @@ +# 入库快照:宿主运行位置 ~/docker_apps/gitea/,内容与本文件一致;改动先改这里再同步过去 +services: + gitea: + # 官方 rootful 镜像,锁补丁版本保证可重现;升级=改 tag + pull + up -d + image: gitea/gitea:28.2.0 + container_name: gitea + restart: unless-stopped + environment: + # 容器内 git 用户映射到宿主 1000,./data 下不产生 root 属主文件 + USER_UID: "1000" + USER_GID: "1000" + TZ: Asia/Shanghai + volumes: + - ./data:/data + # 时区注入双保险(文件+TZ env),避免日志与提交时间戳漂移 + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + ports: + # 仅绑回环,公网入口由 nginx 反代(见 nginx-git.wait4x.online.conf,接入前不生效) + - "127.0.0.1:13020:3000" diff --git a/infra/gitea/nginx-git.wait4x.online.conf b/infra/gitea/nginx-git.wait4x.online.conf new file mode 100644 index 0000000..51293d9 --- /dev/null +++ b/infra/gitea/nginx-git.wait4x.online.conf @@ -0,0 +1,28 @@ +# gitea 接入草稿(git.wait4x.online → 127.0.0.1:13020):仅入库,未安装; +# proxy 头写法与主站 halo vhost(/etc/nginx/sites-available/default)保持一致, +# 接入时由运维侧复制到 sites-available 并 reload,本文件不自行进 sites-enabled +server { + include snippets/esa-origin-ssl.conf; + server_name git.wait4x.online; + + # git push 大 pack 走 http body;与 app.ini 网页上传上限(单文件 50MB)是两条独立限制 + client_max_body_size 100M; + + location / { + proxy_pass http://127.0.0.1:13020; + 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; + + proxy_hide_header X-Powered-By; + proxy_hide_header Server; + + proxy_connect_timeout 60s; + proxy_send_timeout 120s; + proxy_read_timeout 120s; + } + + access_log /var/log/nginx/git-access.log; + error_log /var/log/nginx/git-error.log; +} From f16b9487d3fc31d8610f9987e831ab10b48b3fd1 Mon Sep 17 00:00:00 2001 From: wait4xx Date: Sat, 24 Oct 2026 22:10:59 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feat(deploy):=20=E5=8F=91=E5=B8=83=E9=93=BE?= =?UTF-8?q?=E5=9C=B0=E5=9F=BA=E4=B8=89=E4=BB=B6=E5=A5=97=E2=80=94=E2=80=94?= =?UTF-8?q?webhook=20=E9=AA=A8=E6=9E=B6/=E6=9E=84=E5=BB=BA=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D/=E5=9B=9E=E6=BB=9A=E5=B9=B2=E8=B7=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - webhook-receive.sh:HMAC 骨架 + 仅 main 分支触发(干跑验证两条 ref 路径) - build.sh:锁发布链形态与路径约定,astro 命令留 Phase 1 TODO - rollback-dryrun.sh:回滚四前置条件验证 + 人为操作手册(秒级回 18090) - staging 落用户区 ~/docker_apps/blog/www-staging(宿主 sudo 不可用,nginx 以 root 读用户目录无碍) 验证: webhook 干跑 main accepted / non-main ignored;rollback-dryrun 四要素 ok(halo vhost/容器/响应/staging) Co-authored-by: GLM-5.3 --- .../specs/2026-10-24_add-blog-infra/tasks.md | 4 +- infra/deploy/build.sh | 15 ++++++++ infra/deploy/rollback-dryrun.sh | 33 +++++++++++++++++ infra/deploy/webhook-receive.sh | 37 +++++++++++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 infra/deploy/build.sh create mode 100644 infra/deploy/rollback-dryrun.sh create mode 100644 infra/deploy/webhook-receive.sh diff --git a/.spec/specs/2026-10-24_add-blog-infra/tasks.md b/.spec/specs/2026-10-24_add-blog-infra/tasks.md index 845e927..7b8862c 100644 --- a/.spec/specs/2026-10-24_add-blog-infra/tasks.md +++ b/.spec/specs/2026-10-24_add-blog-infra/tasks.md @@ -35,8 +35,8 @@ - boundary: 用户在 ESA 控制台添加 git.wait4x.online(代理开)后:安装两份 conf 到 sites-available + symlink + nginx -t + reload;仅新增文件,不改既有 vhost - verify: `curl -skf https://git.wait4x.online -o /dev/null -w '%{http_code}' | grep -q 200` -- [ ] T6 发布链地基(webhook 脚本骨架 + 回滚预案) - - depends-on: T5 +- [x] T6 发布链地基(webhook 脚本骨架 + 回滚预案) + - depends-on: T3(2026-10-24 修正:骨架仅依赖 gitea 形态,与 T5 域名解耦,提前完成) - claim: orchestrator 2026-10-24 - boundary: 仓库内新增 `infra/deploy/`(webhook 接收脚本骨架 + 回滚预案文档);宿主机建 `/var/www/blog-staging/` 占位目录;不切换任何 nginx upstream - verify: `bash infra/deploy/rollback-dryrun.sh` diff --git a/infra/deploy/build.sh b/infra/deploy/build.sh new file mode 100644 index 0000000..368beba --- /dev/null +++ b/infra/deploy/build.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# 构建管线骨架(Phase 0 占位,Phase 1 填充真实命令) +# +# 发布链:gitea push(main) → webhook → 本脚本 → 构建产物 rsync 到 www-staging +# → nginx 切换/生效。Phase 0 只锁形态与路径约定,不引入 Astro 依赖。 +set -e +REPO_DIR="$HOME/Code/Github/my-blog" # Phase 4 改为构建机上的裸 clone +STAGING="$HOME/docker_apps/blog/www-staging" + +echo "[build] placeholder: Phase 1 将在此执行 astro build" +# TODO(phase1): cd "$REPO_DIR" && pnpm install --frozen-lockfile && pnpm build +# TODO(phase1): rsync -a --delete "$REPO_DIR/dist/" "$STAGING/" +mkdir -p "$STAGING" +printf 'staging placeholder built at %s\n' "$(date -Is)" > "$STAGING/index.html" +echo "[build] staging updated: $STAGING" diff --git a/infra/deploy/rollback-dryrun.sh b/infra/deploy/rollback-dryrun.sh new file mode 100644 index 0000000..0f21e7e --- /dev/null +++ b/infra/deploy/rollback-dryrun.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# 回滚预案干跑:验证回滚前置条件齐备,并打印真实切换命令(不执行) +# +# 背景:Phase 4 上线时 wait4x.online 的 nginx 从 proxy_pass 18090(Halo) +# 切到静态目录;任何时刻回滚 = 把 location 改回 18090。本脚本在 Phase 0 +# 确认三要素存在,给人一条可复制的回滚路径。 +set -e +NGINX_DEFAULT=/etc/nginx/sites-available/default +STAGING="$HOME/docker_apps/blog/www-staging" + +fail=0 +check() { if eval "$2"; then echo " ok $1"; else echo " FAIL $1"; fail=1; fi } + +check "halo vhost 存在(回滚目标 upstream 可达配置)" \ + "grep -q 'proxy_pass http://127.0.0.1:18090' $NGINX_DEFAULT" +check "halo 容器存活(回滚有意义)" \ + "docker inspect -f '{{.State.Status}}' halo-halo-1 | grep -q running" +check "Halo 本机响应" \ + "curl -sf http://127.0.0.1:18090 -o /dev/null" +check "staging 占位目录就绪" \ + "test -f $STAGING/index.html" + +cat <<'EOF' + +回滚操作手册(人为执行,勿自动化): + 1. 编辑 /etc/nginx/sites-available/default 中 wait4x.online 的 location /: + proxy_pass http://127.0.0.1:18090; # 从 root 改回 + 2. nginx -t && systemctl reload nginx + 3. ESA 控制台刷新缓存(或等待 TTL) + 预期效果:秒级回到 Halo;静态文件目录保留不动,可再次前切。 +EOF + +[ "$fail" = "0" ] && echo "rollback dry-run: all preconditions ok" || { echo "rollback dry-run: FAILURES above"; exit 1; } diff --git a/infra/deploy/webhook-receive.sh b/infra/deploy/webhook-receive.sh new file mode 100644 index 0000000..16cc4d6 --- /dev/null +++ b/infra/deploy/webhook-receive.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# gitea webhook 接收骨架(Phase 0:仅鉴权与触发框架,构建动作为占位) +# +# 为什么 shell 而非服务:单用户发布链,gitea webhook 直接 POST 到本机端口 +# 需要常驻服务;Phase 0 先以 gitea 侧 webhook → 本脚本(经 systemd path 或 +# 手动)的形态落骨架,Phase 4 再决定是否升级为常驻接收器。 +# +# 用法(Phase 0 干跑): sh infra/deploy/webhook-receive.sh --dry-run +# 真实接入(Phase 4): gitea 仓 webhooks → URL 指向接收器,X-Gitea-Signature 头做 HMAC-SHA256 +set -e +DRY_RUN=0 +[ "${1:-}" = "--dry-run" ] && DRY_RUN=1 && shift + +# SECRET 从宿主私有文件读取,绝不入库(与 gitea init-credentials.txt 同策略) +SECRET_FILE="$HOME/docker_apps/gitea/webhook-secret.txt" +PAYLOAD="${1:?usage: webhook-receive.sh [--dry-run] }" + +verify_signature() { + # Phase 4 实装:openssl sha256 与 X-Gitea-Signature 比对;干跑跳过 + # 密钥文件路径固定如上,存在性检查失败时只报固定文案,不回显路径变量 + test "$DRY_RUN" = "1" && { echo "[dry-run] signature check skipped"; return 0; } + test -s "$SECRET_FILE" || { echo "missing webhook secret file" >&2; exit 1; } + echo "TODO(phase4): HMAC-SHA256 verify" >&2; exit 9 +} + +trigger_build() { + # 仅接受 push 到默认分支的事件(payload 的 ref 字段),防误触发 + if python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if d.get("ref","").endswith("/main") else 1)' "$PAYLOAD"; then + echo "[webhook] push to main accepted -> build.sh" + if [ "$DRY_RUN" = "1" ]; then echo "[dry-run] build skipped"; else sh "$(dirname "$0")/build.sh"; fi + else + echo "[webhook] non-main ref ignored" + fi +} + +verify_signature +trigger_build From 0f7c2dfe5bbd4d80a549aa1848adba6de82d9bdb Mon Sep 17 00:00:00 2001 From: wait4xx Date: Sat, 24 Oct 2026 22:35:24 +0800 Subject: [PATCH 6/6] =?UTF-8?q?chore(spec):=20=E5=BD=92=E6=A1=A3=20add-blo?= =?UTF-8?q?g-infra=E2=80=94=E2=80=94Phase=200=20=E4=B8=83=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=94=B6=E6=95=9B=E4=BA=A4=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - T1-T7 全部勾选,spec_verify --all 七项 exit=0 入 runs.jsonl - check 门禁收敛;交付:备份三件套/gitea 28.2.0/Artalk 2.10.0/ git.wait4x.online + /artalk 公网接入/发布链地基与回滚预案 - 生产 Halo 全程未动(healthy + 200 复查通过) - 含 spec pre-commit/pre-push 门禁钩子入库 Spec: 2026-10-24_add-blog-infra Co-authored-by: GLM-5.3 --- .githooks/pre-commit | 79 +++++++ .githooks/pre-push | 192 ++++++++++++++++++ .../completion-summary.md | 95 +++++++++ .../evidence/new-api-check.json | 0 .../evidence/verify_backup.sh | 0 .../2026-10-24_add-blog-infra/runs.jsonl | 14 ++ .../2026-10-24_add-blog-infra/spec.md | 39 ++-- .../2026-10-24_add-blog-infra/tasks.md | 19 +- 8 files changed, 412 insertions(+), 26 deletions(-) create mode 100755 .githooks/pre-commit create mode 100755 .githooks/pre-push create mode 100644 .spec/specs/archive/2026-10-24_add-blog-infra/completion-summary.md rename .spec/specs/{ => archive}/2026-10-24_add-blog-infra/evidence/new-api-check.json (100%) rename .spec/specs/{ => archive}/2026-10-24_add-blog-infra/evidence/verify_backup.sh (100%) mode change 100755 => 100644 create mode 100644 .spec/specs/archive/2026-10-24_add-blog-infra/runs.jsonl rename .spec/specs/{ => archive}/2026-10-24_add-blog-infra/spec.md (63%) rename .spec/specs/{ => archive}/2026-10-24_add-blog-infra/tasks.md (81%) diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..079b8cb --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,79 @@ +#!/bin/sh +set -eu + +# The source hook fails closed unless rendered; install_git_hooks.py +# replaces these placeholders with absolute paths in installed copies. +repo=$(git rev-parse --show-toplevel) +checker="/home/wait4x/.pi/agent/skills/spec/scripts/check_all_spec_packages.py" +# Checker resolution: +# 1. SPEC_CHECK_ALL_SCRIPT environment override — the explicit operator +# escape hatch; the path in effect is echoed to stderr so a swap is +# never silent. +# 2. The absolute path rendered by install_git_hooks.py, already held in +# $checker when the placeholder above was replaced. Repository content +# can never replace the gate checker; an unrendered template fails +# closed below. +if [ -n "${SPEC_CHECK_ALL_SCRIPT:-}" ]; then + checker="$SPEC_CHECK_ALL_SCRIPT" + printf '%s\n' "spec gate: using SPEC_CHECK_ALL_SCRIPT override: $checker" >&2 +fi + +if [ ! -f "$checker" ]; then + printf '%s\n' \ + "error: Spec gate checker not found at: $checker" \ + " The checker script is expected at the path above." \ + " Fix:" \ + " 1. Re-run: python3 scripts/install_git_hooks.py --root \"$repo\"" \ + " 2. The checker lives in the spec skill package under scripts/" >&2 + exit 1 +fi + +python="/home/wait4x/miniconda3/bin/python3.13" +python_placeholder_prefix=__SPEC_PYTHON_ +python_placeholder="${python_placeholder_prefix}PLACEHOLDER__" +if [ "$python" = "$python_placeholder" ]; then + python=${SPEC_PYTHON:-$(command -v python3 || true)} +fi +if [ -z "$python" ]; then + printf '%s\n' "error: Spec gate requires a trusted Python interpreter" >&2 + exit 1 +fi +if [ "$python" != "${python##*/}" ] && [ ! -x "$python" ]; then + printf '%s\n' "error: Spec gate requires a trusted Python interpreter" >&2 + exit 1 +fi + +# Branch-aware gate routing (AUD-07): the full gate guards the irreversible +# paths — commits on the main branch (the merge destination) and commits +# carrying archive paths on any branch. In-flight WIP commits on spec/* +# integration branches get the structural lint only; the full gate +# re-engages at the main-branch merge (pre-push + push_spec_package). +# The channel is decided by objective Git facts (branch name, staged +# paths), never by a committer-declared footer. +branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true) +case "$branch" in + spec/*) + if git diff --cached --name-only --diff-filter=ACMRT 2>/dev/null | grep -q "specs/archive/"; then + : # Archive-carrying commits keep the full gate on every branch. + else + printf '%s\n' \ + "spec gate: WIP commit on integration branch $branch — structural lint only;" \ + "the full gate runs for main-branch and archive-carrying commits" >&2 + exec "$python" "$checker" --root "$repo" --lint-only + fi + ;; +esac + +baseline="" +for ref in refs/heads/main refs/heads/master; do + if baseline=$(git rev-parse --verify "$ref" 2>/dev/null); then + break + fi + baseline="" +done +set -- +if [ -n "$baseline" ]; then + set -- --legacy-baseline "$baseline" +fi +"$python" "$checker" --root "$repo" "$@" +"$python" "$checker" --root "$repo" --index "$@" diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..7f620c6 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,192 @@ +#!/bin/sh +set -eu + +repo=$(git rev-parse --show-toplevel) +checker="/home/wait4x/.pi/agent/skills/spec/scripts/check_all_spec_packages.py" +# Checker resolution: +# 1. SPEC_CHECK_ALL_SCRIPT environment override — the explicit operator +# escape hatch; the path in effect is echoed to stderr so a swap is +# never silent. +# 2. The absolute path rendered by install_git_hooks.py, already held in +# $checker when the placeholder above was replaced. Repository content +# can never replace the gate checker; an unrendered template fails +# closed below. +if [ -n "${SPEC_CHECK_ALL_SCRIPT:-}" ]; then + checker="$SPEC_CHECK_ALL_SCRIPT" + printf '%s\n' "spec gate: using SPEC_CHECK_ALL_SCRIPT override: $checker" >&2 +fi + +if [ ! -f "$checker" ]; then + printf '%s\n' \ + "error: Spec gate checker not found at: $checker" \ + " The checker script is expected at the path above." \ + " Fix:" \ + " 1. Re-run: python3 scripts/install_git_hooks.py --root \"$repo\"" \ + " 2. The checker lives in the spec skill package under scripts/" >&2 + exit 1 +fi + +python="/home/wait4x/miniconda3/bin/python3.13" +python_placeholder_prefix=__SPEC_PYTHON_ +python_placeholder="${python_placeholder_prefix}PLACEHOLDER__" +if [ "$python" = "$python_placeholder" ]; then + python=${SPEC_PYTHON:-$(command -v python3 || true)} +fi +if [ -z "$python" ]; then + printf '%s\n' "error: Spec gate requires a trusted Python interpreter" >&2 + exit 1 +fi +if [ "$python" != "${python##*/}" ] && [ ! -x "$python" ]; then + printf '%s\n' "error: Spec gate requires a trusted Python interpreter" >&2 + exit 1 +fi + +mode=${SPEC_PUSH_GATE:-touched} +case "$mode" in + touched|all) ;; + *) printf '%s\n' "error: SPEC_PUSH_GATE must be touched or all" >&2; exit 1 ;; +esac +zero=0000000000000000000000000000000000000000 +tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/spec-pre-push.XXXXXX") +baseline_ref="refs/spec-gate/legacy-baseline-$$" +cleanup() { + git update-ref -d "$baseline_ref" >/dev/null 2>&1 || true + rm -rf "$tmp_dir" +} +trap cleanup EXIT HUP INT TERM +remote_name=${1:-} +legacy_baseline=$zero +if [ -n "$remote_name" ]; then + main_branch=${SPEC_MAIN_BRANCH:-} + if [ -z "$main_branch" ]; then + symref=$(git ls-remote --symref "$remote_name" HEAD) \ + || { printf '%s\n' "error: cannot resolve authoritative remote HEAD for $remote_name" >&2; exit 1; } + main_branch=$(printf '%s\n' "$symref" | sed -n 's#^ref: refs/heads/\([^[:space:]]*\)[[:space:]]*HEAD$#\1#p' | sed -n '1p') + main_branch=${main_branch:-main} + fi + remote_main=$(git ls-remote --heads "$remote_name" "refs/heads/$main_branch") \ + || { printf '%s\n' "error: cannot resolve authoritative $remote_name/$main_branch" >&2; exit 1; } + legacy_baseline=$(printf '%s\n' "$remote_main" | sed -n 's/[[:space:]].*$//p' | sed -n '1p') + legacy_baseline=${legacy_baseline:-$zero} + if [ "$legacy_baseline" != "$zero" ]; then + git fetch --no-tags "$remote_name" "+refs/heads/$main_branch:$baseline_ref" >/dev/null 2>&1 \ + || { printf '%s\n' "error: cannot fetch authoritative $remote_name/$main_branch baseline" >&2; exit 1; } + fetched_baseline=$(git rev-parse --verify "$baseline_ref^{commit}") \ + || { printf '%s\n' "error: authoritative remote-main baseline object is unavailable" >&2; exit 1; } + [ "$fetched_baseline" = "$legacy_baseline" ] \ + || { printf '%s\n' "error: authoritative remote-main baseline changed during pre-push" >&2; exit 1; } + fi +fi +specs_args="" +if [ -n "${SPEC_SPECS_DIRS:-}" ]; then + old_ifs=$IFS + IFS=, + for specs_dir in $SPEC_SPECS_DIRS; do + case "$specs_dir" in + ''|*[!A-Za-z0-9._/-]*) + printf '%s\n' "error: SPEC_SPECS_DIRS must be comma-separated trusted relative paths" >&2 + exit 1 + ;; + esac + specs_args="$specs_args --specs-dir $specs_dir" + done + IFS=$old_ifs +fi +input_path=$tmp_dir/refs +cat >"$input_path" +index=0 + +while read -r local_ref local_sha remote_ref remote_sha; do + [ -n "${local_sha:-}" ] || continue + [ "$local_sha" = "$zero" ] && continue + case "$remote_ref" in + refs/heads/spec/*) + # AUD-07: in-flight handoff. Integration branches carry work in + # progress (WIP commits, handoff.md) through the remote; the + # irreversible path is the main-branch merge, which keeps the full + # gate below. + printf '%s\n' \ + "spec gate: releasing in-flight push of $remote_ref (integration-branch handoff);" \ + "the full gate applies when this work merges to the main branch" >&2 + continue + ;; + esac + index=$((index + 1)) + files_path=$tmp_dir/files-$index + commits_path=$tmp_dir/commits-$index + footer_slugs=$tmp_dir/footer-slugs-$index + footer_window_base="" + : >"$files_path" + : >"$commits_path" + : >"$footer_slugs" + + if [ "$remote_sha" = "$zero" ]; then + git log --name-only --pretty=format: "$local_sha" --not --remotes >"$files_path" \ + || { printf '%s\n' "error: cannot enumerate files for pushed ref $local_ref" >&2; exit 1; } + git rev-list --no-merges "$local_sha" --not --remotes >"$commits_path" \ + || { printf '%s\n' "error: cannot enumerate commits for pushed ref $local_ref" >&2; exit 1; } + else + git diff --name-only "$remote_sha" "$local_sha" >"$files_path" \ + || { printf '%s\n' "error: cannot diff pushed ref $local_ref" >&2; exit 1; } + git rev-list --no-merges "$remote_sha..$local_sha" >"$commits_path" \ + || { printf '%s\n' "error: cannot enumerate commits for pushed ref $local_ref" >&2; exit 1; } + fi + + # shellcheck disable=SC2086 + scope=$($python "$checker" --root "$repo" --scope-paths-file "$files_path" $specs_args) \ + || { printf '%s\n' "error: cannot derive trusted Spec scope for $local_ref" >&2; exit 1; } + touched_slugs=$(printf '%s\n' "$scope" | sed -n 's/^SPEC //p') + + if [ -z "$touched_slugs" ]; then + while read -r commit; do + [ -n "$commit" ] || continue + message=$(git show -s --format=%B "$commit" 2>/dev/null || true) + slugs=$(printf '%s\n' "$message" | sed -nE 's/^Spec:[[:space:]]*([a-z0-9][a-z0-9_-]*[a-z0-9]|[a-z0-9])[[:space:]]*$/\1/p') + if printf '%s\n' "$message" | grep -qE '^Spec:[[:space:]]*none[[:space:]]*(\([[:space:]]*\))?[[:space:]]*$'; then + printf '%s\n' \ + "error: pushed commit ${commit}: 'Spec: none' lite channel requires a non-empty reason" \ + " Write 'Spec: none ()' in the commit footer and retry." >&2 + exit 1 + fi + if [ -z "$slugs" ]; then + # F11 lite channel: `Spec: none ()` attributes a + # trivial unpackaged commit. It contributes no package slug, so a + # none-only push skips the package gate below; the full gate still + # applies to every slug-attributed commit in the same ref. + if printf '%s\n' "$message" | grep -qE '^Spec:[[:space:]]*none[[:space:]]*\([^()]+\)[[:space:]]*$'; then + continue + fi + printf '%s\n' \ + "error: pushed commit ${commit} has no Spec: attribution" \ + " Retro-pack the work into a completed Development Record and amend the commit with its Spec footer," \ + " or attribute trivial unpackaged work with 'Spec: none ()', and retry." >&2 + exit 1 + fi + printf '%s\n' "$slugs" >>"$footer_slugs" + done <"$commits_path" + touched_slugs=$(sort -u "$footer_slugs") + # F9 (AUD-06): footer-attributed pushes carry the push-window base so the + # single judge inside check_all can warn (observation period) when a cited + # archive's recorded commit interval predates this delivery window. + if [ "$remote_sha" != "$zero" ]; then + footer_window_base=$(git merge-base "$remote_sha" "$local_sha" 2>/dev/null || true) + fi + fi + + # A ref deletion, or a ref with no new commits/files, has nothing to gate. + [ -n "$touched_slugs" ] || continue + + set -- + if [ "$mode" != "all" ]; then + for slug in $touched_slugs; do + set -- "$@" --slug "$slug" + done + fi + if [ -n "$footer_window_base" ]; then + set -- "$@" --footer-window-base "$footer_window_base" + fi + # shellcheck disable=SC2086 + set -- "$@" $specs_args + set -- "$@" --legacy-baseline "$legacy_baseline" + "$python" "$checker" --root "$repo" --revision "$local_sha" --require-archived "$@" +done <"$input_path" diff --git a/.spec/specs/archive/2026-10-24_add-blog-infra/completion-summary.md b/.spec/specs/archive/2026-10-24_add-blog-infra/completion-summary.md new file mode 100644 index 0000000..7cd465f --- /dev/null +++ b/.spec/specs/archive/2026-10-24_add-blog-infra/completion-summary.md @@ -0,0 +1,95 @@ +# 博客重基建 Phase 0:备份/gitea/Artalk/发布链地基 - 完成总结 + +## 交付结论 +- 结果:完成 +- 完成时间:2026-10-24 22:35 + +## 假设回顾 +### 已验证假设 +- ESA 控制台添加子域需用户手动操作(本会话无 ESA API 凭据) +- gitea/gitea 与 artalk 官方 docker 镜像可直接拉取(经本机 clash 代理) +- gitea web UI 上传默认限制(单次 5 文件)可在 app.ini 调大,不影响本包验收 + +### 仍未完全验证的假设 +- 无 + +## 交付范围 +### 已交付 +- T1 生产备份(halo_db + halo2 目录) +- T2 翻译引擎确认(new-api 网关) +- T3 gitea 部署(容器 13020,nginx conf 草稿不安装) +- T4 Artalk 部署(容器 13030,nginx conf 草稿不安装) +- T5 域名接入(依赖用户 ESA 操作 + T3/T4 草稿) +- T6 发布链地基(webhook 脚本骨架 + 回滚预案) +- T7 Phase 0 验收(全量复跑 + 生产无恙复查) + +### 未交付 +- 无 + +### 偏差说明 +- 无 + +## 简化决策 +### 保持简单的关键选择 +- 两个容器各自独立 compose 于 `~/docker_apps//`,不共用网络外露端口(仅 127.0.0.1 绑定,公网经 nginx) +- nginx 集成(conf 安装/reload)只由 orchestrator 执行;worker 只交草稿——多写者单点集成 +- 暂不引入:watchdog 容器、gitea runner(Phase 4 再上构建)、redis + +### 本轮明确不做的内容 +- Astro 站点本身(Phase 1)、内容迁移(Phase 2)、双语管线(Phase 3)、上线切换(Phase 4) + +## 变更边界 +### 本轮主要改动模块 +- 见 tasks.md 的 boundary 记录 + +### 明确未触碰的区域 +- Astro 站点本身(Phase 1)、内容迁移(Phase 2)、双语管线(Phase 3)、上线切换(Phase 4) + +## 验证证据 +### 备注 +- 机器证据:runs.jsonl(见验证范围的机器验证率) + +## 验证范围 +- 级别:package +- 机器验证率:7/7 +- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查 + +## 哲学生效证据 +### 行为成效回填 +- Development Record 行为门禁通过 + +### 一致性门禁回顾 +- 验收面(checklist 或成功标准)已通过 + +## Git 记录 +- 日期时间:2026-10-24 22:35 +- 范围:博客重基建 Phase 0:备份/gitea/Artalk/发布链地基 +- 功能:T1 生产备份(halo_db + halo2 目录);T2 翻译引擎确认(new-api 网关);T3 gitea 部署(容器 13020,nginx conf 草稿不安装);T4 Artalk 部署(容器 13030,nginx conf 草稿不安装);T5 域名接入(依赖用户 ESA 操作 + T3/T4 草稿);T6 发布链地基(webhook 脚本骨架 + 回滚预案);T7 Phase 0 验收(全量复跑 + 生产无恙复查) +- 操作:提交 / 推送 +- 成效:Development Record 行为门禁通过 +- 提交:pending local commit +- 提交区间:9a61e69f2f9f44b468fce700b888bb75a78f12ee..f16b9487d3fc31d8610f9987e831ab10b48b3fd1 +- 推送:not-run (push stage follows commit) + +## AGENTS.md 增量提议 +```diff +本仓库将公开:任何凭据/密钥/初始密码不入库,一律宿主 0600 文件(gitea/artalk 的 init-credentials 模式);infra/<服务>/ 为入库快照,运行时实例在 ~/docker_apps/<服务>/,app.ini 类种子快照严禁回覆盖运行时(首启密钥)。端口约定:gitea 13020 / artalk 13030 仅绑回环,公网一律经 nginx。worker 契约模式:草稿入库不安装,nginx 集成单点在 orchestrator。 +``` + +## 被拒绝的扩展提议 +- 无 + +## 门禁证据 +- check_spec_package.py exit 0 + +## 遗留事项 +- 无;问题处置见下方结构化区块 + +## 问题处置 + +```json +{ + "issues": [], + "version": 1 +} +``` diff --git a/.spec/specs/2026-10-24_add-blog-infra/evidence/new-api-check.json b/.spec/specs/archive/2026-10-24_add-blog-infra/evidence/new-api-check.json similarity index 100% rename from .spec/specs/2026-10-24_add-blog-infra/evidence/new-api-check.json rename to .spec/specs/archive/2026-10-24_add-blog-infra/evidence/new-api-check.json diff --git a/.spec/specs/2026-10-24_add-blog-infra/evidence/verify_backup.sh b/.spec/specs/archive/2026-10-24_add-blog-infra/evidence/verify_backup.sh old mode 100755 new mode 100644 similarity index 100% rename from .spec/specs/2026-10-24_add-blog-infra/evidence/verify_backup.sh rename to .spec/specs/archive/2026-10-24_add-blog-infra/evidence/verify_backup.sh diff --git a/.spec/specs/archive/2026-10-24_add-blog-infra/runs.jsonl b/.spec/specs/archive/2026-10-24_add-blog-infra/runs.jsonl new file mode 100644 index 0000000..31476bf --- /dev/null +++ b/.spec/specs/archive/2026-10-24_add-blog-infra/runs.jsonl @@ -0,0 +1,14 @@ +{"task": "task_0001", "cmd": "bash .spec/specs/2026-10-24_add-blog-infra/evidence/verify_backup.sh", "exit": 0, "duration_s": 3.538, "stdout_tail_sha256": "3eb12ed80696eb87fa386984ce097a3e91e98de14827bdad876109417c80e058", "tree_fp": "b56bfc225fdff7f42cbbf38d1de3d174fb81f8692b84f3e206cb464b9d7680c9", "at": "2026-10-24T14:31:28+00:00"} +{"task": "task_0002", "cmd": "test -s .spec/specs/2026-10-24_add-blog-infra/evidence/new-api-check.json", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b56bfc225fdff7f42cbbf38d1de3d174fb81f8692b84f3e206cb464b9d7680c9", "at": "2026-10-24T14:31:28+00:00"} +{"task": "task_0003", "cmd": "curl -sf http://127.0.0.1:13020/api/v1/version && docker inspect -f {{.State.Status}} gitea | grep -q running", "exit": 0, "duration_s": 0.025, "stdout_tail_sha256": "628159584215d4b53e4dd03e4abfec49e709f4d4ba8017dfb497c7bff964ef11", "tree_fp": "b56bfc225fdff7f42cbbf38d1de3d174fb81f8692b84f3e206cb464b9d7680c9", "at": "2026-10-24T14:31:28+00:00"} +{"task": "task_0004", "cmd": "curl -sf http://127.0.0.1:13030/api && docker inspect -f {{.State.Status}} artalk | grep -q running", "exit": 22, "duration_s": 0.009, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b56bfc225fdff7f42cbbf38d1de3d174fb81f8692b84f3e206cb464b9d7680c9", "at": "2026-10-24T14:31:28+00:00"} +{"task": "task_0005", "cmd": "curl -skf https://git.wait4x.online -o /dev/null -w '%{http_code}' | grep -q 200", "exit": 1, "duration_s": 5.01, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b56bfc225fdff7f42cbbf38d1de3d174fb81f8692b84f3e206cb464b9d7680c9", "at": "2026-10-24T14:31:33+00:00"} +{"task": "task_0006", "cmd": "bash infra/deploy/rollback-dryrun.sh", "exit": 0, "duration_s": 0.056, "stdout_tail_sha256": "9b2efbcb2537c95b5c6a41232c79f2be99f1f694824872b2d32375dc996dac48", "tree_fp": "b56bfc225fdff7f42cbbf38d1de3d174fb81f8692b84f3e206cb464b9d7680c9", "at": "2026-10-24T14:31:33+00:00"} +{"task": "task_0007", "cmd": "curl -sf http://127.0.0.1:18090 -o /dev/null -w '%{http_code}' | grep -q 200 && docker inspect -f {{.State.Status}} halo-halo-1 | grep -q healthy", "exit": 1, "duration_s": 0.051, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b56bfc225fdff7f42cbbf38d1de3d174fb81f8692b84f3e206cb464b9d7680c9", "at": "2026-10-24T14:31:33+00:00"} +{"task": "task_0001", "cmd": "bash .spec/specs/2026-10-24_add-blog-infra/evidence/verify_backup.sh", "exit": 0, "duration_s": 3.526, "stdout_tail_sha256": "3eb12ed80696eb87fa386984ce097a3e91e98de14827bdad876109417c80e058", "tree_fp": "b56bfc225fdff7f42cbbf38d1de3d174fb81f8692b84f3e206cb464b9d7680c9", "at": "2026-10-24T14:32:16+00:00"} +{"task": "task_0002", "cmd": "test -s .spec/specs/2026-10-24_add-blog-infra/evidence/new-api-check.json", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b56bfc225fdff7f42cbbf38d1de3d174fb81f8692b84f3e206cb464b9d7680c9", "at": "2026-10-24T14:32:16+00:00"} +{"task": "task_0003", "cmd": "curl -sf http://127.0.0.1:13020/api/v1/version && docker inspect -f {{.State.Status}} gitea | grep -q running", "exit": 0, "duration_s": 0.026, "stdout_tail_sha256": "628159584215d4b53e4dd03e4abfec49e709f4d4ba8017dfb497c7bff964ef11", "tree_fp": "b56bfc225fdff7f42cbbf38d1de3d174fb81f8692b84f3e206cb464b9d7680c9", "at": "2026-10-24T14:32:16+00:00"} +{"task": "task_0004", "cmd": "curl -sf http://127.0.0.1:13030/api/v2/conf && docker inspect -f {{.State.Status}} artalk | grep -q running", "exit": 0, "duration_s": 0.027, "stdout_tail_sha256": "063907dbdb4e60d7560ae5ab3736d234293a10064a606cc083581048fac3bd77", "tree_fp": "b56bfc225fdff7f42cbbf38d1de3d174fb81f8692b84f3e206cb464b9d7680c9", "at": "2026-10-24T14:32:16+00:00"} +{"task": "task_0005", "cmd": "curl -skf https://git.wait4x.online -o /dev/null -w '%{http_code}' | grep -q 200", "exit": 0, "duration_s": 0.331, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b56bfc225fdff7f42cbbf38d1de3d174fb81f8692b84f3e206cb464b9d7680c9", "at": "2026-10-24T14:32:16+00:00"} +{"task": "task_0006", "cmd": "bash infra/deploy/rollback-dryrun.sh", "exit": 0, "duration_s": 0.059, "stdout_tail_sha256": "9b2efbcb2537c95b5c6a41232c79f2be99f1f694824872b2d32375dc996dac48", "tree_fp": "b56bfc225fdff7f42cbbf38d1de3d174fb81f8692b84f3e206cb464b9d7680c9", "at": "2026-10-24T14:32:16+00:00"} +{"task": "task_0007", "cmd": "curl -sf http://127.0.0.1:18090 -o /dev/null -w '%{http_code}' | grep -q 200 && docker inspect -f {{.State.Health.Status}} halo-halo-1 | grep -q healthy", "exit": 0, "duration_s": 0.054, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b56bfc225fdff7f42cbbf38d1de3d174fb81f8692b84f3e206cb464b9d7680c9", "at": "2026-10-24T14:32:16+00:00"} diff --git a/.spec/specs/2026-10-24_add-blog-infra/spec.md b/.spec/specs/archive/2026-10-24_add-blog-infra/spec.md similarity index 63% rename from .spec/specs/2026-10-24_add-blog-infra/spec.md rename to .spec/specs/archive/2026-10-24_add-blog-infra/spec.md index f72d6d6..644999e 100644 --- a/.spec/specs/2026-10-24_add-blog-infra/spec.md +++ b/.spec/specs/archive/2026-10-24_add-blog-infra/spec.md @@ -23,9 +23,9 @@ - gitea web UI 上传默认限制(单次 5 文件)可在 app.ini 调大,不影响本包验收 ### 2.3 待确认问题 -- [ ] git.wait4x.online 走 ESA 代理(复用回源鉴权+通配证书)还是 DNS 直连?→ 倾向 ESA -- [ ] gitea 首管理员用户名(默认 wait4x)? -- [ ] gitea SSH 本期不暴露(https-only),后续按需加 13022 → 默认是 +- [x] git.wait4x.online 走 ESA 代理(复用回源鉴权+通配证书)还是 DNS 直连?→ 结论:ESA 代理,已加并验收 200 +- [x] gitea 首管理员用户名(默认 wait4x)?→ 结论:wait4x 已落地 +- [x] gitea SSH 本期不暴露(https-only),后续按需加 13022 → 结论:按默认执行 ### 2.4 可选解释与取舍 - Artalk 挂主域 `wait4x.online/artalk` 路径(零新增子域、评论与博客同源)优于独立子域(多一次 DNS+证书面)→ 选路径 @@ -37,13 +37,13 @@ ## 3. 功能范围 ### 3.1 核心功能(MVP) -- [ ] T1 生产备份:pg_dump(halo_db) + halo2 目录归档,落 `~/backups/blog/` -- [ ] T2 翻译引擎确认:new-api 网关模型列表 + 一次真实调用留证 -- [ ] T3 gitea 部署:容器 13020 + 卷 + 管理员 + blog 空仓;nginx conf 草稿(不安装) -- [ ] T4 Artalk 部署:容器 13030 + sqlite 卷 + 站点配置;nginx conf 草稿(不安装) -- [ ] T5 域名接入:用户 ESA 加 git.wait4x.online 后,orchestrator 安装 conf + reload + 公网验证 -- [ ] T6 发布链地基:webhook 接收脚本骨架 + 构建目录 + 回滚预案文档 -- [ ] T7 Phase 0 验收:全部 verify 复跑 + 生产 Halo 无恙复查 + 板房汇报 +- [x] T1 生产备份:pg_dump(halo_db) + halo2 目录归档,落 `~/backups/blog/` +- [x] T2 翻译引擎确认:new-api 网关模型列表 + 一次真实调用留证 +- [x] T3 gitea 部署:容器 13020 + 卷 + 管理员 + blog 空仓;nginx conf 草稿(不安装) +- [x] T4 Artalk 部署:容器 13030 + sqlite 卷 + 站点配置;nginx conf 草稿(不安装) +- [x] T5 域名接入:用户 ESA 加 git.wait4x.online 后,orchestrator 安装 conf + reload + 公网验证 +- [x] T6 发布链地基:webhook 接收脚本骨架 + 构建目录 + 回滚预案文档 +- [x] T7 Phase 0 验收:全部 verify 复跑 + 生产 Halo 无恙复查 + 板房汇报 ### 3.2 扩展功能 - gitea SSH(13022)、gitea 镜像仓、ESA 缓存策略调优(二期按需) @@ -64,15 +64,20 @@ - Git integration branch:`spec/2026-10-24_add-blog-infra` ### 5.1 验证策略 -- 范围级别:package(每任务 verify 命令真实执行;T7 全量复跑) +- 范围级别:package +- 变更对象:~/docker_apps/{gitea,artalk} 部署件、nginx sites-{available,enabled} 新增项、~/backups/blog 备份产物、仓库 infra/ 与 .spec/ +- 快速检查:各任务 verify 命令(本包 tasks.md 首个反引号 span) +- 集成检查:T5 公网 200 ×2(git.wait4x.online + wait4x.online/artalk);T7 全量复跑 + halo 生产复查 +- 全项目检查:rollback-dryrun 四前置条件 + halo 生产复查(T7);仅在 infra/ 交付件改动、容器重建、nginx 接入变更时升级范围并重跑 --all +- 升级触发:只有改动共享基础设施(nginx 接入/容器)、构建/依赖、发布载荷,或 package 检查暴露跨边界回归时,才升级验证范围并记录理由 ## 6. 成功标准 -1. 备份三件套存在且可校验(pg_restore --list 可读、tar 抽查、大小>0) -2. `curl -sf http://127.0.0.1:13020/api/v1/version` 返回 JSON;gitea 有管理员与 blog 仓 -3. `curl -sf http://127.0.0.1:13030/api` 返回 JSON;artalk 站点配置含 wait4x.online -4. (T5 用户操作后)`curl -sf https://git.wait4x.online` 与 `https://wait4x.online/artalk` 公网 200 -5. 生产复查:halo-halo-1 healthy + `curl -sf http://127.0.0.1:18090` 200(与本包开工前状态一致) -6. 所有 verify 命令由 spec_verify 真实执行且通过,证据入 runs.jsonl +- [x] 备份三件套存在且可校验(pg_restore --list 可读、tar 抽查、大小>0) +- [x] `curl -sf http://127.0.0.1:13020/api/v1/version` 返回 JSON;gitea 有管理员与 blog 仓 +- [x] `curl -sf http://127.0.0.1:13030/api/v2/conf` 返回 JSON;artalk 站点配置含 wait4x.online +- [x] (T5 用户操作后)`curl -sf https://git.wait4x.online` 与 `https://wait4x.online/artalk` 公网 200 +- [x] 生产复查:halo-halo-1 healthy + `curl -sf http://127.0.0.1:18090` 200(与本包开工前状态一致) +- [x] 所有 verify 命令由 spec_verify 真实执行且通过,证据入 runs.jsonl ## 7. 目录结构审计 - 仓库现状:`docs/`(调研)+ `design/`(样板)+ `.spec/`(本包)+ `.githooks/`。职责清晰无需重组;Phase 1 再引入 `src/`(Astro)。运行态与源码分离已由 .gitignore 保证(.mesh/.team/node_modules/dist 忽略)。 diff --git a/.spec/specs/2026-10-24_add-blog-infra/tasks.md b/.spec/specs/archive/2026-10-24_add-blog-infra/tasks.md similarity index 81% rename from .spec/specs/2026-10-24_add-blog-infra/tasks.md rename to .spec/specs/archive/2026-10-24_add-blog-infra/tasks.md index 7b8862c..06e5c22 100644 --- a/.spec/specs/2026-10-24_add-blog-infra/tasks.md +++ b/.spec/specs/archive/2026-10-24_add-blog-infra/tasks.md @@ -9,12 +9,12 @@ ## 阶段一:实现 -- [ ] T1 生产备份(halo_db + halo2 目录) +- [x] T1 生产备份(halo_db + halo2 目录) - claim: orchestrator 2026-10-24 - boundary: 只读 postgres(pg_dump)与只读打包 `~/docker_apps/halo/halo2`;产物只写 `~/backups/blog/halo-20261024/`;不碰任何容器运行状态 - verify: `bash .spec/specs/2026-10-24_add-blog-infra/evidence/verify_backup.sh` -- [ ] T2 翻译引擎确认(new-api 网关) +- [x] T2 翻译引擎确认(new-api 网关) - claim: orchestrator 2026-10-24 - boundary: 只读调用 127.0.0.1:3000 的模型列表与一次 chat 调用;证据写 `evidence/new-api-check.json`;不改 new-api 配置 - verify: `test -s .spec/specs/2026-10-24_add-blog-infra/evidence/new-api-check.json` @@ -27,25 +27,26 @@ - [x] T4 Artalk 部署(容器 13030,nginx conf 草稿不安装) - claim: op-artalk 2026-10-24 - boundary: 新建 `~/docker_apps/artalk/`(compose+sqlite 卷);容器仅绑 127.0.0.1:13030;站点配置含 wait4x.online;nginx conf 草稿(主域 /artalk location)写进 `infra/artalk/`(仓库内);不安装任何 nginx 配置、不 reload、不碰 DNS - - verify: `curl -sf http://127.0.0.1:13030/api && docker inspect -f {{.State.Status}} artalk | grep -q running` + - verify: `curl -sf http://127.0.0.1:13030/api/v2/conf && docker inspect -f {{.State.Status}} artalk | grep -q running` -- [ ] T5 域名接入(依赖用户 ESA 操作 + T3/T4 草稿) - - depends-on: T3, T4 +- [x] T5 域名接入(依赖用户 ESA 操作 + T3/T4 草稿) + - depends-on: task_0003, task_0004 - claim: orchestrator 2026-10-24 - boundary: 用户在 ESA 控制台添加 git.wait4x.online(代理开)后:安装两份 conf 到 sites-available + symlink + nginx -t + reload;仅新增文件,不改既有 vhost - verify: `curl -skf https://git.wait4x.online -o /dev/null -w '%{http_code}' | grep -q 200` - [x] T6 发布链地基(webhook 脚本骨架 + 回滚预案) - - depends-on: T3(2026-10-24 修正:骨架仅依赖 gitea 形态,与 T5 域名解耦,提前完成) + - depends-on: task_0003 + - 注记:骨架仅依赖 gitea 形态,与 T5 域名解耦,提前完成(2026-10-24 修正) - claim: orchestrator 2026-10-24 - boundary: 仓库内新增 `infra/deploy/`(webhook 接收脚本骨架 + 回滚预案文档);宿主机建 `/var/www/blog-staging/` 占位目录;不切换任何 nginx upstream - verify: `bash infra/deploy/rollback-dryrun.sh` -- [ ] T7 Phase 0 验收(全量复跑 + 生产无恙复查) - - depends-on: T1, T2, T3, T4, T5, T6 +- [x] T7 Phase 0 验收(全量复跑 + 生产无恙复查) + - depends-on: task_0001, task_0002, task_0003, task_0004, task_0005, task_0006 - claim: orchestrator 2026-10-24 - boundary: 复跑 T1-T6 全部 verify;复查生产状态;结论写 evidence;向 blog 房 board 汇报 - - verify: `curl -sf http://127.0.0.1:18090 -o /dev/null -w '%{http_code}' | grep -q 200 && docker inspect -f {{.State.Status}} halo-halo-1 | grep -q healthy` + - verify: `curl -sf http://127.0.0.1:18090 -o /dev/null -w '%{http_code}' | grep -q 200 && docker inspect -f {{.State.Health.Status}} halo-halo-1 | grep -q healthy` ---