Files
blog/架构总览.md
T
zqlit 448b898e40 feat(远端): 恢复自建 Gitea 代码辅仓,OpenList 降为备选备份
用户定案:「自建 gitea 同步一下吧,openlist 也作为备选备份」。
于是把上一轮「撤销全部 git 辅仓」的决定部分回退:在线那一路回到自建 Gitea,
OpenList 上的加密 bundle 从「唯一异地备份」改为「备选异地备份」。

两层不是重复,而是失效模式不同:
  · 自建 Gitea(在线、可增量、可浏览)—— 不受任何第三方平台规则约束
  · 离线 bundle(离线、单一文件、完整历史)—— 平台全挂也能恢复

一、实际同步
- Gitea 位于 23.254.236.47:3001(Gitea 28.0.0),本机直连即可推,
  **不需要广州中转机**(旧结论「沙箱跑不通 git smart HTTP」只针对当时的代理)
- 远端停在 ed38f938(2026-10-04),落后 57 个提交,且是本地 HEAD 的**祖先**
  → 一次 fast-forward 追平,**未强推、未丢历史**
- 同步后 origin 与 gitea 均指向 5f27ad32

二、git 配置
- 恢复 remote `gitea`(凭据编在 URL 里,只落本机 .git/config,不入库)
- `pushall` 回到双推:
    !git push origin main; ec=$?; git remote | grep -qx gitea && git push gitea main; exit $ec
  · 用 `;` 而非 `&&` → 主仓挂了辅仓照样推
  · `exit $ec` → 退出码仍以主仓为准,辅仓成功不掩盖主仓失败
  · gitea 段带存在性前置判断 → 没配辅仓时静默跳过,不白报错

三、发布链路恢复 gitea(前一轮刚去掉,现按新定案加回)
- `deploy/editor-api/bootstrap.sh`
    · 新增 GITEA_URL / GITEA_USER / GITEA_PASS(凭据必须现给,不写死在脚本里)
    · PUSH_REMOTES 默认 → origin,gitea;没给 GITEA_PASS 则降级为 origin
    · 恢复「配 gitea 远端」一节;退役远端名单去掉 gitea(只留 gh / gitee)
      —— 否则新部署会把现行辅仓当成退役远端清掉
    · 结尾补一句「未启用 Gitea 辅仓」的提示
- `docker-compose.editor.yml`、`editor-api/server.mjs` → 默认 origin,gitea
- `editor-api/README.md` → 变量表同步;`editor-api/Dockerfile` → 注释补 http 情形

四、`scripts/setup-cnb-remotes.sh` 改回双远端模式
- 头部沿革、USAGE、凭据段全部重写;GITEA_PASS 走环境变量,不入库
- 退役远端循环只清 gh / gitee;pushall 写同一份定义

五、文档
- `架构总览.md`:§1.2 地址地图加「代码辅仓」行、备份行降为「备选」;
  §2 旅程图改双推;§5.1 推送重写(含别名实际定义与三条取舍);
  §5.2 远端改双行表 + Gitea 接入细节;§5.3 由「不走 git 远端」改为
  「远端与备份的分工」(三层各自的失效模式);§5.6 定位改备选;
  §6 待办 #2/#4/#7/#10 更新
- `CNB构建落地方案.md` §4.0 目标形态与定案块、§4.1 第 4 条
- `README.md` 发布流程图、推送说明、脚本表
- `scripts/backup-run.mjs` 头部注释:不再是「唯一备份」

实测:
  git push gitea main → ed38f938..5f27ad32  main -> main
  git pushall         → origin / gitea 双向同步,退出码 0
  bash -n 校验 bootstrap.sh 与 setup-cnb-remotes.sh 均通过
2026-10-06 21:42:08 +08:00

408 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 优世界博客 · 架构总览
> 更新时间:2026-10-04(CNB 迁移完成当日)
> 状态:**迁移已落地并跑通**(四线路全绿,单次发布约 3.5 分钟)
> 本文档只描述**现状**;迁移前的复杂度审计与选型过程见文末「相关文档」。
---
## 0. 一句话
四个子系统,一条 **3 步**的托管发布链路。构建与发布已从「自建 Gitea + act_runner + 广州中转机」迁到
**腾讯云 CNB(cnb.cool)** —— **需要自己运维的机器从 3 台降到 0 台**。
---
## 1. 全景
### 1.1 四个子系统
| # | 子系统 | 技术栈 | 跑在哪 | 状态 |
|---|---|---|---|---|
| **1** | **内容系统** | Hugo 0.128.2 extended + Ying 主题,138 篇 md;产物约 3200 文件 / 424MB | 产物分发到 3 个 CDN | 正常 |
| **2** | **评论系统** | `blog-admin/` = artalk-cf(Artalk v2 兼容服务端)+ RSS 机器人 | Cloudflare Workers + D1 + KV → `api.200181.xyz` | ✅ 零服务器零运维 |
| **3** | **写作系统** | `write-server/`(Next.js 16,`post.usj.cc`)<br>`write/`(本地 Windows 前端) | 独立 Docker 主机/本机 | 可用(两套前端功能重叠) |
| **4** | **发布基础设施** | **CNB 流水线**(`.cnb.yml`)+ 又拍云 + 多吉云 + EdgeOne | 腾讯云 CNB(**托管**) | **新建,已跑通** |
### 1.2 服务与地址地图
| 角色 | 地址 / 位置 | 说明 |
|---|---|---|
| **代码主仓** | `cnb.cool/zqlit/blog` | CNB,**私有**;构建由它触发;唯一 fetch 源 |
| **代码辅仓** | 自建 Gitea `23.254.236.47:3001/zqlit/blog` | 只推不拉,纯代码留档;**不受第三方平台规则约束**(见 §5.2) |
| **异地备份** | 中兴 F50 上的 OpenList(`/本地/备份/blog-bundle/`) | **备选**异地备份:整仓加密 bundle,计划任务每天 03:30 自动跑,**不依赖任何 git 服务**(见 §5.6) |
| **构建 + 发布** | CNB 流水线(腾讯云国内节点,4 核 8G) | 托管,0 元(免费额度内) |
| **密钥仓库** | `cnb.cool/zqlit/blog-secrets` | CNB「密钥仓库」类型,10 项变量经 `imports` 注入 |
| **境内源站** | 又拍云对象存储 | 由 CNB 国内节点**直传**(原为 `if: false` 的死代码) |
| **国内加速** | 多吉云 CDN | 回源又拍云 |
| **境外线路** | EdgeOne Pages(项目 `hugo-blog`,`--area overseas`) | 腾讯 EdgeOne 国际站 |
| **评论后端** | `api.200181.xyz` | Cloudflare Workers |
| **通知** | QQ 邮件(`imql@qq.com`) | 已收敛为**单一通道**;同时承担备份失败告警(§5.6) |
> 关键点:境内、境外仍是**两条独立线路**,但**由同一条流水线一次推完** ——
> 这是本次迁移最大的结构性改善。
---
## 2. 一次发布的完整旅程(3 步)
```
① 写作 write-server(网页) 或 write/(本地 Windows)
│
② git push git pushall = git push origin main ; git push gitea main
│ ├─ origin → cnb.cool/zqlit/blog (**主仓**,触发构建)
│ └─ gitea → 23.254.236.47:3001/zqlit/blog(自建 Gitea,代码同步辅仓)
│ ★ 备选异地备份不走 git 远端 —— 由本机计划任务每天 03:30
│ 跑整仓加密 bundle 传到 F50 上的 OpenList(§5.6)
▼
③ CNB 流水线(国内节点,约 3.5 分钟,7 个 stage 顺序执行)
├─ 1. Hugo 构建 草稿隐藏预处理 → hugo --minify --gc → 算构建哈希
├─ 2. 同步到又拍云 upx sync(境内直传,非阻断但如实上报)
├─ 3. 刷新又拍云 CDN purge 首页 + sitemap/rss/archives/posts 等固定入口
├─ 4. 刷新多吉云 CDN node scripts/refresh_cdn.js
├─ 5. 部署 EdgeOne edgeone pages deploy --area overseas
├─ 6. 上报部署状态 POST api.200181.xyz/api/deploy-status
└─ 7. 邮件通知 成功 → stages 末尾;失败 → failStages
```
**对照**:迁移前是 **7 步,中间有 3 个环节是你自己运维的服务器**。
---
## 3. 流水线细节(`.cnb.yml`,284 行)
| 触发 | 条件 | 说明 |
|---|---|---|
| push | 推送到 `main` | 主链路 |
| crontab | `0 9 * * *`(Asia/Shanghai) | 每天 09:00,与 push **共用同一组 stage**(YAML 锚点) |
| 机制 | 实现 |
|---|---|
| 构建镜像 | `deploy/Dockerfile`,hugo 二进制**随仓库携带**(`bin/linux/hugo`,linux/amd64) |
| ⚠️ docker 上下文 | `by: [bin/linux/hugo]` —— CNB 的 docker build **只看得见 Dockerfile + by 列出的文件**,漏写会报 `not found` |
| 密钥注入 | `imports: https://cnb.cool/zqlit/blog-secrets/-/blob/main/secrets.yml` |
| stage 间传状态 | 落盘 `.ci_status`(原版靠 `needs.*.outputs`) |
| 并发控制 | `lock.cancel-in-progress`(对应原 `concurrency`) |
| 非阻断 | 又拍云三步 + 状态上报 + 邮件用 `allowFailure: true`,失败仍写状态如实上报 |
| 资源 | `cpus: 4`(4 核 8G) |
---
## 4. 迁移前后对比
| 维度 | 迁移前(Gitea 自建) | 迁移后(CNB) |
|---|---|---|
| 发布链路环节 | **7 步** | **3 步** |
| 自维护机器 / 服务 | **3 台**(Gitea 主机 + act_runner + 广州中转机) | **0**(构建托管) |
| 流水线定义 | GitHub Actions **1169 行** | `.cnb.yml` **284 行** |
| 产物传递 | 打 `tar.zst` → artifact → 中转机跨境下载 | 同一 pipeline **共享工作目录**,无需传递 |
| 又拍云直传 | `if: false`(境外 runner 必挂,**从未跑通**) | **已跑通**(国内节点直传) |
| COS 中转层 | 需要(存储 + 流量账单) | **已砍** |
| `github.server_url` 兼容分支 | **8 处** | **0** |
| 通知 | 3 套(TG / 飞书 / 邮件),约 400 行 | **1 套**(邮件) |
| 图片优化 + 反向 commit | CI 内每轮白跑(15 张老图处理不掉) | **未迁**(如需保留,照搬 `scripts/optimize_images.js`) |
| 构建环境 | 境外 VPS | 腾讯云**国内节点** 4 核 8G |
| 费用 | VPS 月租 | **0 元**(100GiB 仓库 + 160 核时/月) |
| 单次发布耗时 | — | **约 3.5 分钟** |
> 原链路里那些补丁(COS 中转 / `sync.sh` 每分钟轮询 / artifact 打包 / 兼容分支)
> **唯一根因是「runner 在境外」**。CNB 节点在国内,这一整层随之消失。
---
## 5. 运维要点
### 5.1 推送
```bash
git pushall # CNB 与 Gitea 都推 —— 发布用这个
git push origin main # 只推 CNB(触发构建)
git push gitea main # 只推 Gitea 辅仓
```
`git pushall` 的实际定义:
```
!git push origin main; ec=$?; git remote | grep -qx gitea && git push gitea main; exit $ec
```
**本仓有两个 git 远端:`origin`(CNB 主仓)+ `gitea`(自建 Gitea 代码同步辅仓)。**
演变路径:GitHub 双推 →(GitHub 被 AUP 清空)→ 一度只剩 `origin` → 现在回到双远端。
注意 Gitee 从头到尾**没真正建起来过**(卡体积门槛,见 §5.3),别把它记成一代。
三条设计上的取舍:
- **不用「一个 remote 挂多个 pushurl」** —— 实测 git 是「顺序推、遇错即停」,
第一个失败后面的都不推,备份意义就没了。所以用「独立 remote + 别名」。
- 别名用 `;` 串联而非 `&&`,正是为了让一段失败不影响另一段(主仓挂了辅仓照样推);
再 `exit $ec` 把退出码还给主仓,免得辅仓成功掩盖主仓失败。
- gitea 那一段带 `git remote | grep -qx gitea` 前置判断 ——
没配辅仓时静默跳过,而不是白报一行「'gitea' does not appear to be a git repository」。
### 5.2 远端
| remote | 地址 | 角色 | fetch | push |
|---|---|---|---|---|
| `origin` | `https://cnb.cool/zqlit/blog.git` | **主仓**(CNB) | ✅ 唯一 fetch 源 | ✅ 触发 CNB 构建 |
| `gitea` | `http://23.254.236.47:3001/zqlit/blog.git` | **代码同步辅仓**(自建,Gitea 28.0.0) | ⛔ 只推不拉 | ✅ 代码留档,失败不阻断 |
- **Gitea 凭据编在远端 URL 里**(`http://zqlit:<密码>@…`):容器/脚本环境没有交互终端、
也没有可用的凭据助手,这是让 `git push gitea` 免交互跑通的唯一简单写法。
明文只落在本机 `.git/config`(以及服务器上容器内的同名文件),**不进仓库、不随之推送**。
⚠️ 口令里若含 `#` 或 `&` 会被拼 URL 的 `sed` 吃掉 —— 那种情况请改用访问令牌。
- 自建 Gitea **不参与构建**:`.github/` 已删、Actions 早已停用,它纯粹是代码副本。
当前版本 `28.0.0`,走 HTTP + 3001 端口,本机直连即可(**不需要**广州中转机,
与当初「本机跑不通 git smart HTTP」的旧结论不同 —— 那次是沙箱代理的问题)。
- 接入时的同步基线:Gitea 停在 `ed38f938`(2026-10-04),落后 57 个提交,
且是本地 HEAD 的**祖先** → 一次 fast-forward 就追平,**无需强推、不丢历史**。
已移除的远端(改动前的完整配置快照留在 `.workbuddy-backup/git-remotes.*.txt`):
| remote | 地址 | 移除于 | 原因 |
|---|---|---|---|
| `gh` | `github.com/zqlit/blog` | 2026-10-06 | 账号被标记、仓库被按 AUP 清空(见下),**别再往回加** |
| `gitee` | (从未真正建立过) | — | 两条硬门槛过不去(§5.3) |
> ⚠️ **GitHub 退役记录(2026-10-06)**
>
> 起因:GitHub 账号被平台标记,随后仓库被按 AUP 合规条款清空 ——
> 远端留下一条**孤立提交**(无父提交):
>
> ```
> c21d5669 2026-10-06 20:08:32 +0800 zqlit <zqlit@users.noreply.github.com>
> chore: remove repository content (AUP compliance)
> ```
>
> 同时远端 `main` 的历史与本地**完全分叉**(两边只共享 2024-07-11 的 `Initial commit`,
> 之后每个提交 SHA 都不同;远端那份是 989 提交、剥离了 `.env`/私钥/大二进制的变体)。
> 教训:**别把「备份」寄托在会对内容做合规处置的平台上**;
> 且一旦某次历史被重写,SHA 从此再也对不上。
>
> 本项目随后清掉了 GitHub 的**全部**痕迹:remote、推送别名、发布链路里 4 处硬编码
> (`bootstrap.sh` / `docker-compose.editor.yml` / `server.mjs` / `README.md`)、
> 以及 CI 定义本身(整个 `.github/` 目录,1268 行)。
### 5.3 远端与备份的分工(2026-10-06)
**结论:git 辅仓用「自建 Gitea」,备份用「整仓加密 bundle → OpenList」。两者都要,各管一段。**
| 层 | 载体 | 保住什么 | 依赖 |
|---|---|---|---|
| 主仓 `origin` | CNB | 源码 + 触发构建 | CNB 平台 |
| 代码辅仓 `gitea` | 自建 `23.254.236.47:3001` | 随时 `clone` 回来、按提交追溯 | **自己的机器** |
| 离线备份 | F50/OpenList 上的加密 bundle(§5.6) | 完整历史 + 所有对象,平台全挂也能恢复 | 家里的局域网 |
两层的区别不是「多一份」,而是**失效模式不同**:辅仓是**在线、可增量、可浏览**的副本,
但仍在别人的磁盘上;bundle 是**离线、离线介质、单一文件**的全量快照,
连 git 服务都没有也能恢复。所以辅仓不能替 bundle,bundle 也不如辅仓顺手。
**为什么不选「另一家托管平台」当辅仓**(Gitee / 云效 Codeup / GitLab):
把副本放到**另一家同样性质的托管平台**上,只是把鸡蛋从左边口袋挪到右边口袋 ——
GitHub 那次 AUP 清空就是活证。自建 Gitea 是唯一「不受第三方规则约束」的在线选项,
它早在建站初期就在跑,本就该留着。
下面这些实测数据保留下来 —— 下次再冒出「换一家托管平台当辅仓」的念头时,
这几张表就是答案。
体积分布(HEAD,实测 457.4 MB / 2509 文件):
| 目录 | 体积 | 说明 |
|---|---|---|
| `content/`(含 `posts/`) | ≈ 308 MB | 博客正文与媒体(mp4/mp3/大图),**搬不走** |
| `bin/linux/hugo` | **83.1 MB** | CNB 构建用的 hugo 二进制,**可移出 git** |
| `static/` | ≈ 38 MB | 站点静态资源 |
| `themes/` | 20.3 MB | Ying 主题 |
| 其余全部 | ≈ 8 MB | 代码、文档、脚本 |
**Gitee 的两条硬约束 —— 都不满足:**
| 约束 | Gitee 免费版 | 本仓库当前 | 判定 |
|---|---|---|---|
| 单仓库容量 | **≤ 500 MB** | `.git` **620 MB** | ❌ 超 24% |
| 单文件大小 | **≤ 50 MB** | `bin/linux/hugo` **83.1 MB** | ❌ **必删** |
| 用户总仓库容量 | 5 GB | 620 MB | ✅ |
| 私有仓协作人数 | 5 人 | 个人 | ✅ 无关 |
`bin/linux/hugo` 是**硬门槛**:**不管怎么瘦身历史,只要它还跟踪在 HEAD 里,Gitee 一律拒收**。
→ 移出后 HEAD ≈ **374 MB**,才有一份余量。
**对照:阿里云效 Codeup 基础版 —— 两条硬约束一条都不存在:**
| 项 | Gitee 免费版 | 云效 Codeup 基础版 |
|---|---|---|
| 单库容量 | ≤ 500 MB | **Git 5 GiB + LFS 5 GiB** |
| 单文件 | ≤ 50 MB | **Web 50 MB / 命令行 200 MB** |
| 620 MB 的 `.git` | 超 24%,推不上 | 占 **12.1%** ✅ |
| 83.1 MB 的 hugo | 超 66%,一律拒收 | 占 **41.6%** ✅ |
| 价格 | 免费 | 0 元/人/年,不限人数、不限仓库数 |
→ 若选 Codeup,`bin/linux/hugo` **不必出库**,`deploy/Dockerfile` 与 `.cnb.yml` 的 `by:`
字段**一行都不用改**(首推 620 MB 也在其 60 分钟推送超时内)。
注意两点:① 默认「中心组织」**只支持阿里云 RAM 账号登录、且必须绑钉钉**,
不接受的可选 2025-12 上线的「地域组织」(华东2上海,支持自建账号密码);
② 基础版无「删库保护」,容量到 90% 提醒、**超限直接禁止写操作**(连删文件都做不了)。
已有的阿里云 OSS AccessKey **不能**用来建 Codeup 仓库 —— git 推送走独立的克隆账号密码或 SSH Key。
> ★ **这些都只是「换一个篮子」,不是容灾。**
> CNB 与 Gitee/Codeup 同属国内大区;GitHub/GitLab 又执行同一套合规逻辑
> (本轮 GitHub 被按 AUP 清空即为例证)。
> 所以**托管平台这条路一个都没采用** —— 在线辅仓回到**自建 Gitea**(§5.2,
> 不受第三方规则约束),另加一层不依赖任何 git 服务的离线备份(§5.6)。
**顺带一个副作用**:`bin/linux/hugo`「出库」这件事**不用做了**。
那个 83.1 MB 二进制之所以成为问题,只因为它卡在 Gitee 的单文件 50 MB 上限上。
既然不走 Gitee,`deploy/Dockerfile` 的取 hugo 方式与 `.cnb.yml` 的 `by:` 字段
**一行都不用改**。
### 5.4 凭据
- 令牌存**仓库外**:`~/.workbuddy/secrets/cnb-token`
- 本仓 `credential.helper` 指向一个自定义脚本;`.git/config` **无明文**
- ⚠️ 本机全局 helper 是 **GCM**(会弹窗、对第三方 HTTPS 远端还会挂死);wincred 对 cnb.cool 有过「幽灵记录」删不掉 → 故用自定义 helper 绕开
### 5.5 密钥
- 修改密钥 → 编辑 CNB 密钥仓库的 `secrets.yml`(**网页编辑,禁 clone**)
- 本地 `cnb-secrets.yml` 只是粘贴草稿(已 gitignore,**不入库**)
- ⚠️ CNB 的 `imports` 是**一层映射**(key 就是变量名本身);又拍云服务名在 CNB 里必须叫
**`UPYUN_SERVICE`**(旧 Gitea 里叫 `UPYUN_BUCKET`,照抄会「变量未定义」)
### 5.6 整仓离线备份(中兴 F50 / OpenList)★ 备选异地备份
> **定位**:2026-10-06 起这一层是**备选**异地备份 —— 在线那一路已有自建 Gitea 代码辅仓(§5.2)。
> 但它的地位并没有因此变轻:**它是唯一一层「不依赖任何 git 服务」的备份**,
> 平台全挂、账号被封、服务器被回收都影响不到它。所以它的**可靠性**与**失败可见性**
> 依然是最要紧的:一旦悄悄失效,能挡住「平台级事故」的就只剩它。
> 失败告警因此是必备件,不是加分项。
**目标介质**:中兴 F50 5G CPE 内置 256 GB 存储,上面跑 OpenList
(`http://192.168.0.1:5244`,WebDAV 端点**必须带 `/dav` 前缀**)。7×24 常开、走局域网、
**不依赖任何 git 平台账号** —— 这是它比「再选一个代码托管平台」更值钱的地方。
**存放位置**:`/本地/备份/blog-bundle/` —— **专属子目录,不是 `/本地/备份/` 根下**。
根目录由用户自己在用(放着 `github-zqlit-*`、`local-repos-*` 等手工备份),
把文件混进去既容易被误删,也让「清理旧份」多一层风险。
(早期版本确实直接写在根目录下,实测发现文件已被清掉,故改为子目录隔离。)
**为什么是 bundle 而不是直接推 git**:WebDAV 不支持原子的 rename/lock,
把 bare repo 挂上去直接 `git push` 会让对象写坏 —— 表面成功、实际随机损坏,
可能几个月后才发现。bundle 是**单文件顺序写**,没有这个问题。
**为什么加密**:本仓库历史里含 `.env`、TLS 私钥、`GITEA_SECRETS.md`。
介质是一台随身设备的内部存储,明文等于把密钥放在可能丢失 / 刷机 / 送修的设备上。
OpenList 的登录只保护**访问通道**,不保护**存储介质** —— F50 丢了拆开就能读。
**为什么不用 gpg**:本机 gpg 2.4.9 在 Windows 下已损坏(反复 `removing stale lockfile`,
node spawn 直接 `EBUSY`)。改用 Node 内置 `crypto` 的 **AES-256-GCM**:
零外部依赖、带认证标签(能检测篡改/截断)、可流式处理 600 MB 不爆内存。
**加密文件布局**:`magic(8) | salt(16) | iv(12) | 密文(...) | GCM tag(16)`,
密钥由 `scrypt(N=32768, r=8, p=1)` 从口令派生。
**用法**(`scripts/backup-bundle.mjs`,零 npm 依赖):
```bash
# 推荐入口:带失败告警的包装器(计划任务用的就是它)
node scripts/backup-run.mjs # = backup-bundle 的默认参数
node scripts/backup-run.mjs --verify --keep 7 # 计划任务的默认组合
node scripts/backup-run.mjs --dry --notify-success # 试跑并强制发一封成功邮件
# 底层脚本(backup-run.mjs 就是转发到它)
node scripts/backup-bundle.mjs # 加密 + 上传 + 比对字节数
node scripts/backup-bundle.mjs --verify # 额外下载回来比对 sha256(端到端最可靠)
node scripts/backup-bundle.mjs --dry # 只打包加密,不上传
node scripts/backup-bundle.mjs --keep 7 # 远端保留最近 7 份(默认值)
node scripts/backup-bundle.mjs --list # 列出远端现有备份
node scripts/backup-bundle.mjs --decrypt <文件> [--out x.bundle] # 恢复用
```
**配置**(按序查找,先找到生效):环境变量 → `~/.openlist-backup.env`
→ `.workbuddy-backup/openlist-backup.env`(**已在 .gitignore 内**)。
| 键 | 用途 |
|---|---|
| `OPENLIST_URL` / `OPENLIST_USER` / `OPENLIST_PASS` | OpenList 地址与账号 |
| `OPENLIST_DIR` | 远端目录,当前 `/本地/备份/blog-bundle` |
| `BACKUP_PASSPHRASE` | bundle 加密口令 |
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` / `SMTP_TO` | 失败告警发信(复用 CNB 流水线那套 QQ 邮箱授权码) |
> ⚠️ `BACKUP_PASSPHRASE` 是恢复 bundle 的唯一钥匙,**必须另存进密码管理器**。
> 它不构成单点:CNB 主仓与自建 Gitea 辅仓都还是明文副本,口令丢了只是少一份备份。
**失败告警**(`scripts/backup-run.mjs`)—— 这是「离线那一层」的必备件。
跑完备份后,退出码非 0 就通过 `scripts/send_mail.js` 发一封告警邮件
(主题带 `★`,正文附日志尾部与常见原因清单)。成功时默认**不发**(避免每天一封的邮件疲劳),
加 `--notify-success` 才发。
> 为什么非做不可:「每天自动跑」的任务最典型的失败模式恰恰是**静默**的 ——
> F50 被带出门、换了网段、OpenList 重启后没起来、WebDAV 口令改过……
> 没人会主动发现,直到真要用备份那天。
> 发信逻辑独立于备份逻辑:**发信失败不改判备份的退出码**,通知不该掩盖真正的故障。
**定时任务**:Windows 计划任务 `Blog-BundleBackup`,每天 **03:30(本地)**
执行 `scripts/backup-task.cmd`(默认 `--verify --keep 7`)。
设置:`InteractiveToken` + `LeastPrivilege`、`StartWhenAvailable`(错过就补跑)、
`ExecutionTimeLimit=PT1H`、`MultipleInstances=IgnoreNew`(防重入)。
日志追加到 `.workbuddy-backup/logs/backup.log`,超 5 MB 自动轮转。
> 保留 **7 份 = 一周窗口**(每份 605.5 MB ≈ 4.2 GB,F50 有 256 GB)。
> 原本是 3 份;定位成「离线那一层备份」后放宽 —— 空间不值钱,回溯窗口值钱。
★ `backup-task.cmd` **内容必须全 ASCII**:cmd.exe 按**当前代码页**(zh-CN 是 GBK)
解析批处理文件,而 node 输出 UTF-8;UTF-8 中文注释会吞掉 CR/LF、
把下一行当命令执行(实测踩到:一条 `rem` 被当命令跑了)。
ASCII 是 UTF-8 子集,纯英文注释与 node 的中文输出混写不会乱。
**恢复流程**:
```bash
node scripts/backup-bundle.mjs --decrypt blog-<时间戳>.bundle.enc --out blog.bundle
git bundle verify blog.bundle # 应报 "records a complete history"
git clone blog.bundle blog # 或 git fetch blog.bundle 'refs/*:refs/*'
```
**实测数据**(2026-10-06):
| 阶段 | 耗时 |
|---|---|
| `git bundle create --all` | 6.8 ~ 9.9 s(605.5 MB) |
| AES-256-GCM 加密 | 1.3 ~ 2.9 s(体积不变) |
| WebDAV 上传 | 19.2 ~ 20.6 s → **29.4 ~ 31.6 MB/s** |
| 下载回读 + sha256 比对 | ≈ 30 s,**一致** |
端到端退出码 0;恢复链路已演练(解密 → `git bundle verify` → 1008 提交完整一致);
失败告警的邮件链路也已实测(发出一封「备份成功」验证信到 `imql@qq.com`)。
---
## 6. 遗留待办
| # | 项 | 说明 |
|---|---|---|
| 1 | `gh` remote(GitHub) | ✅ **已移除**(2026-10-06)。GitHub 账号被标记、仓库被 AUP 清空 —— **GitHub 已彻底退出本项目**:远端、推送别名、发布链路 4 处硬编码、CI 定义全部清干净 |
| 2 | **远端与备份定案** | ✅ **已定案**(2026-10-06):**CNB 主仓 + 自建 Gitea 代码辅仓 + OpenList 离线 bundle**(§5.2/§5.3/§5.6)。自建 Gitea 不受第三方规则约束,是唯一「平台出事也带不走」的在线副本;离线 bundle 保完整历史。选型依据(Gitee 两道硬门槛、Codeup 对照)保留在 §5.3 备查 |
| 3 | `bin/linux/hugo` 出库 | ✅ **不必做**。那个 83.1MB 二进制只卡 Gitee 的单文件上限;既然不走 Gitee,`deploy/Dockerfile` 的取 hugo 方式与 `.cnb.yml` 的 `by:` 字段一行都不用改 |
| 4 | `gitea` remote | ✅ **已恢复**(2026-10-06):作为代码同步辅仓重新挂上,`pushall` 恢复双推(`origin` + `gitea`)。接入时 Gitea 停在 `ed38f938`,一次 fast-forward 追平(+57 提交),**未强推、未丢历史**。发布链路 5 处默认值同步改回 `origin,gitea`(`bootstrap.sh` / `docker-compose.editor.yml` / `server.mjs` / `README.md` / `setup-cnb-remotes.sh`) |
| 5 | GitHub 侧旧 workflow | ✅ **已删除**(2026-10-06)。整个 `.github/` 目录移除(`deploy.yml` 1169 行 + `aliyun-backup.yml` + `cleanup.yml`),共 −1284 行 —— **GitHub 已不再是任何环节的依赖**。旧实现可在 git 历史中查 |
| 6 | 令牌 scope | 缺 `repo-cnb-history:r`(读构建日志);补上后 agent 可自行排错 |
| 7 | Gitea 主机 / 广州中转机 | **Gitea 主机已回归**(2026-10-06):作为代码辅仓,本机直连 `23.254.236.47:3001` 即可推。广州中转机仍不参与 git 链路 |
| 8 | `README.md` | ✅ 已更新(去 GitHub 化 + 补上备份脚本说明) |
| 9 | **敏感文件仍在跟踪中** | `.env`、`GITEA_SECRETS.md`、`write-server/nginx/ssl/privkey.pem`、`content/posts/2024/.../setup-secrets.png` —— 若还要推任何新平台,先 `git rm --cached` 并轮换凭据。⚠️ 当初「不改写历史」的唯一顾虑是备份会分叉;**GitHub 那份已消失,这个顾虑没有了** → `git filter-repo` 现在是可行窗口(代价:全部提交 SHA 改变)。离线 bundle 已加密(§5.6),不受此影响 |
| 10 | **整仓离线备份** | ✅ **已上线**(2026-10-06),现为**备选**异地备份(在线那一路是自建 Gitea 辅仓),见 §5.6。计划任务 `Blog-BundleBackup` 每天 03:30 跑,端到端已验证;失败会发告警邮件(已实测)。待补三件安全项:把 `BACKUP_PASSPHRASE` 抄进密码管理器;给 OpenList **改掉 admin 密码 + 开两步验证**(现 `otp: false`,且旧密码已出现在对话里);确认 5244 端口**未暴露到公网** |
| 11 | **F50 存储目录的使用约定** | `/本地/` 下的 `刷机` / `系统` / `资料` / `软件` / `驱动` / `备份` 都是用户自己在管的类别。**本项目的 bundle 只写 `备份/blog-bundle/` 子目录**,不与用户手工备份(`github-zqlit-*`、`local-repos-*`)混放 |
---
## 附:相关文档
- `CNB构建落地方案.md` — 迁移实施方案与实测数据
- `代码源与构建平台选型.md` — 平台对比(CNB / Gitee / GitLab / EdgeOne / 阿里云 ESA)
- `砍COS改造步骤.md` — COS 下线记录
- `EdgeOne双区域方案评估.md`、`Gitee方案评估.md`、`阿里云ESA评估.md`、`精简方案-只留CF和Hugo.md` — 决策期评估
**证据出处(现行)**:`.cnb.yml`(284 行)、`deploy/Dockerfile`、`bin/linux/hugo`、
`scripts/send_mail.js`、`scripts/refresh_cdn.js`、`scripts/setup-cnb-remotes.sh`、
`scripts/backup-bundle.mjs`、`scripts/backup-run.mjs`、`scripts/backup-task.cmd`。
原 GitHub Actions 流水线(`.github/workflows/deploy.yml`,1169 行)已于 2026-10-06 删除,
需要查请翻 git 历史。