Merge branch 'spec/2026-10-24_add-blog-infra'
Phase 0 地基交付:备份/gitea 28.2.0/Artalk 2.10.0/git.wait4x.online 接入/发布链地基;七任务机器验证收敛归档。 Spec: 2026-10-24_add-blog-infra
This commit is contained in:
commit
dca5fe0e5e
19 files changed
+888
No files matched your search
Executable
+79
@@ -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 "$@"
|
||||||
Executable
+192
@@ -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 (<reason>)' in the commit footer and retry." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "$slugs" ]; then
|
||||||
|
# F11 lite channel: `Spec: none (<non-empty reason>)` 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: <slug> 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 (<reason>)', 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"
|
||||||
@@ -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/<name>/`,不共用网络外露端口(仅 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
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -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 前置依赖已记录"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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"}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# 博客重基建 Phase 0:备份/gitea/Artalk/发布链地基 - 项目范围
|
||||||
|
|
||||||
|
<!-- spec-format: 2 -->
|
||||||
|
|
||||||
|
## 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 待确认问题
|
||||||
|
- [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+证书面)→ 选路径
|
||||||
|
- gitea 独立子域(完整应用、path 反代需 ROOT_URL 子路径配置、体验差)优于主域路径 → 选子域
|
||||||
|
|
||||||
|
### 2.5 已证伪路径
|
||||||
|
- 无(暂无证伪记录)
|
||||||
|
|
||||||
|
## 3. 功能范围
|
||||||
|
|
||||||
|
### 3.1 核心功能(MVP)
|
||||||
|
- [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 缓存策略调优(二期按需)
|
||||||
|
|
||||||
|
### 3.3 不在范围内
|
||||||
|
- Astro 站点本身(Phase 1)、内容迁移(Phase 2)、双语管线(Phase 3)、上线切换(Phase 4)
|
||||||
|
|
||||||
|
## 4. 最小实现路径
|
||||||
|
- 两个容器各自独立 compose 于 `~/docker_apps/<name>/`,不共用网络外露端口(仅 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
|
||||||
|
- 变更对象:~/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. 成功标准
|
||||||
|
- [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 忽略)。
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# 博客重基建 Phase 0:备份/gitea/Artalk/发布链地基 - 任务拆解
|
||||||
|
|
||||||
|
## 使用规则
|
||||||
|
- 每个任务都要写清楚 `boundary` 和 `verify`;机器任务的 verify 以首个反引号 span 为执行的命令
|
||||||
|
- 如果一个任务没有验证方式,就不能开始;确无命令可执行的任务写 `verify: manual: <理由>`
|
||||||
|
- 发现的可执行问题必须回写本包,并在本轮做完
|
||||||
|
- verify 命令不得自引用本包 check 门禁
|
||||||
|
- 多 agent 协作:任务可加 `claim:` 行声明在做者(信息约定,非锁);worker 交付物经 orchestrator 验收后由 orchestrator 勾选
|
||||||
|
|
||||||
|
## 阶段一:实现
|
||||||
|
|
||||||
|
- [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`
|
||||||
|
|
||||||
|
- [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`
|
||||||
|
|
||||||
|
- [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`
|
||||||
|
|
||||||
|
- [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/v2/conf && docker inspect -f {{.State.Status}} artalk | grep -q running`
|
||||||
|
|
||||||
|
- [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: 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`
|
||||||
|
|
||||||
|
- [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.Health.Status}} halo-halo-1 | grep -q healthy`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**当前进度**:由脚本计算,无需手动维护
|
||||||
@@ -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/`
|
||||||
@@ -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: "<openssl rand -hex 24 生成>"
|
||||||
|
|
||||||
|
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: []
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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 <staging> 改回
|
||||||
|
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; }
|
||||||
@@ -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 <payload.json>
|
||||||
|
# 真实接入(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] <payload.json>}"
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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 <u> --email <e>@wait4x.online --random-password
|
||||||
|
# 建私有空仓(token 从 init-credentials.txt 取)
|
||||||
|
curl -X POST http://127.0.0.1:13020/api/v1/user/repos \
|
||||||
|
-H "Authorization: token <TOKEN>" -H 'Content-Type: application/json' \
|
||||||
|
-d '{"name":"<repo>","private":true,"auto_init":false}'
|
||||||
|
# 生成新 token
|
||||||
|
docker compose exec -u git gitea gitea admin user generate-access-token --username <u> --scopes all --raw
|
||||||
|
# 改密(含管理员自救)
|
||||||
|
docker compose exec -u git gitea gitea admin user change-password -u <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 接入由运维会话统一执行,
|
||||||
|
本目录草稿不自行生效。
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user