Files
blog/docs/CNB构建落地方案.md
T
zqlit 7abef4ac13 docs: 全量梳理文档结构;补 Gitea 迁移指南 / 架构精简候选;架构总览补证书管家子系统
用户四问:「把项目文件全部理一遍 还有readme文件」「gitea 后面可能还要迁移 帮我写一个迁移文档」
「证书管家做完了吗」「感觉现在架构还是复杂了」

一、文档梳理:根目录 15 个 md → 2 个

根目录只留 README.md(入口)+ 架构总览.md(现状唯一事实源),其余全部归位:

  docs/
  ├── README.md              ← 新增:文档地图(入口 / 当前有效 / 历史归档 三层)
  ├── 证书管家.md             ← 原「函数版证书管家-方案.md」改名
  │                              (它早已是「现状+沿革」文档,标题名不副实 —— 签发早已不在 CF Worker)
  ├── Gitea迁移指南.md        ← 新增
  ├── 架构精简候选.md         ← 新增
  ├── CNB构建落地方案.md / GITEA_SECRETS.md
  └── archive/{平台与选型,功能与修复,主题与内容}/   ← 32 份

- 32 个归档文档统一加 `> 📦 本文档已归档` banner,并指向架构总览
  —— 这个仓库历史文档里全是「权威口径」,混看很容易拿废弃结论当现状
- gitea-backup/ → deploy/gitea/(原名「backup」不准,它是部署包;与其它 deploy 单元并列)
- 新增 md 链接校验(python 脚本,中文路径用 sed 不安全)→ 首轮 18 处断链
  (多一层目录要让 banner 里的相对路径补一个 ../)→ 修正后 0 断链

二、新增 docs/Gitea迁移指南.md(★ 有死线:境外 VPS 11 月到期)

★ 核心价值是「指出迁移面已大幅缩小」:仓库原有的 deploy/gitea/迁移前检查清单.md
(2026-10-04)是为「Gitea + act_runner + 又拍云同步 *整套* 搬迁」写的,**那个前提已经不存在**——
构建归 CNB,runner / upyun-sync / 中转机 / COS 都不需要了。迁移实际只剩「搬数据卷 + 改地址」。

★★ 全文最重要:本仓有 9 处写死了旧地址,按易漏程度排序(1-3 本机,4-5 线上)
  1 本机 .git/config 的 gitea remote
  2 scripts/setup-cnb-remotes.sh 的 GITEA_URL 默认值
  3 deploy/editor-api/bootstrap.sh 的 GITEA_URL 默认值
  4 ★ /srv/editor-api/.env 的 GITEA_URL      ← 漏了会「持续报错但只警告不阻断」,没人发现
  5 ★ /srv/blog 的 gitea remote               ← 同上
  6 docs/GITEA_SECRETS.md
  7-9 架构总览.md / README.md / editor-api/README.md
(不用改:docker-compose.editor.yml、server.mjs、pushall 别名 —— 只引用 remote 名字)

另含:建议新实例改用域名而非 IP(以后搬机器只改 DNS;⚠️ Gitea 28 起只读 ROOT_URL 不读
[server] DOMAIN);rsync 而非 dump(属主必须 uid/gid 1000);切换顺序「先建新的→验证→再拆旧的」;
验证清单强调 `git push gitea --dry-run`;第九节给出「干脆不留这个辅仓」的选项与判断依据。

顺带修掉两个硬伤:
- deploy/gitea/docker-compose.yml 的镜像 tag `1.28.0-rootless` 根本不存在
  (Gitea 28 起去掉 1. 前缀)→ 改 28.0.0-rootless(pin 死,不用 latest)
- deploy/gitea/.gitignore 漏了 backups/(跑一次备份就会把含 secrets 的 dump 写进历史)

三、证书管家核实(结论:已上线运行,2 个遗留)

线上实测:容器 Up (healthy);/preflight 7/7 全过;3 组域名;下次自动续期 04:10。

遗留 ① writeapi.usj.cc 证书没纳管(真问题):nginx 配置在 /www/conf.d/writeapi.usj.cc.conf,
不在 /www/sites/ 管理树里 → 1Panel 的 ssl/upload+sslID 物化碰不到它。
实测仍是 RSA / CN=usj.cc / 到期 2026-12-07,本项目续期不会更新 → 12 月会断。
遗留 ② dnsapi.usj.cc 没上 HTTPS。
(澄清假问题:200181.xyz 公网看到的 LE 证书是 CF 自家边缘证书,与本项目无关)

四、新增 docs/架构精简候选.md(回应「架构还是复杂了」)

结论:复杂度不在件数,在「跨 6 个环境,其中 4 个要自己维护」。国内机 9 个容器里属于本项目的
只有 3 个,能动的只有 2 个。5 个候选 + 建议执行顺序:
  1 停 certimate 容器(工作流已全停用)——先 stop 观察,别急着删
  2 cn-dns-helper 大概率已是遗留(「Worker 侧签发」时代产物;现在签发在国内机且自带 dnsprovider,
    /preflight 显示 CF 凭据可读;Worker 侧 dnsremoted.ts 已无任何引用)
  3 Gitea:迁 or 不留
  4 两套写作前端收敛(本轮不动,先看使用频率)
  5 writeapi.usj.cc 证书纳管(12 月死线)
并明确列出 6 项「必要复杂度,不建议动」。

五、架构总览补齐证书管家子系统(★ 之前完全缺席)

一个完整子系统在「事实源文档」里一个字都没有 —— 这本身就是文档债。补:
- §0 一句话(四→五个子系统)、§1.1 子系统表、§1.2 地址地图两行
- **新增 §5.7 证书管家**:职责划分 / 为什么非要这么分(Worker 免费版 CPU 10ms)/
  三条不能破的红线(Worker 不得签发 · 证书路由只认 Bearer 会话 · 角色必须正着枚举放行)/
  纳管域名表 / 与 certimate 的关系 / 已知遗留
- .cnb.yml 行数 284 → 337(数字漂了);头部更新时间 → 2026-10-06;附录表改指 docs/
- §6 待办:#13(证书遗留)、#14(复杂度盘点)新增;#7 改写为「Gitea 11 月到期,★ 有死线」

六、记忆

.workbuddy/memory/MEMORY.md 新增「文档结构」「Gitea 迁移死线」两节;证书管家节补实测与遗留。
2026-10-06 22:27:36 +08:00

464 lines
23 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.
# CNB 构建落地方案:双 CDN 架构不动,砍掉中转链
> 状态:2026-10-04 · 待你确认后落地
> 回答的问题:「CNB 构建的前提下,能不能继续现在的架构(境外 EdgeOne + 境内多吉云 → 又拍云源站)?」
---
## 一句话结论
**能,而且一个字都不用改。** 域名解析、备案、EdgeOne 项目、多吉云配置、又拍云存储全部保持原样。
CNB 只替换「构建 + 搬运」这一段,顺带砍掉广州中转机上那个每分钟轮询的 `sync.sh`。
---
## 零、GitHub 依赖审计:全链路零依赖
回答「GitHub 账号被标记了,还能连上 CNB 吗」——**能,而且这条链路从头到尾不需要 GitHub。**
逐环节核实(来源:腾讯云官方文档「社区版快速入门」「个人中心」、CNB 官方 Wiki):
| 环节 | CNB 的做法 | 碰 GitHub 吗 |
|---|---|---|
| 注册 / 登录 | **微信扫码**,扫码即自动注册,无邮箱验证、无登录密码 | ❌ |
| 实名认证 | 微信扫码授权手机号 | ❌ |
| 建组织 / 仓库 | CNB 内建(无「个人私有仓库」,仓库必须挂在组织下) | ❌ |
| 导入代码 | 标准 git:`git push --mirror <cnb-url>`,源仓可以是**自建 Gitea** | ❌ |
| 拉构建依赖 | CNB 自带「国内外依赖源默认加速」(腾讯云 CDN + Docker/NPM/Maven 镜像) | ❌ |
| hugo 二进制 | **已随仓库携带**(`bin/linux/hugo`),构建时零跨境下载 | ❌ |
| 部署 | EdgeOne API + 又拍云 upx v0 | ❌ |
官方原文:
> 云原生构建默认使用微信扫码注册登录,未注册用户在扫码登录后,将自动完成注册。
> —— 腾讯云文档《社区版快速入门》
> 云原生构建为微信扫码登录注册,没有登录密码。
> —— 腾讯云文档《个人中心》
### 唯一会碰到 GitHub 的两个场景,你都不走
1. **用 CNB 的「从 GitHub 导入」一键迁移工具** —— 那个需要 `GITHUB_TOKEN`。
**但你从自建 Gitea 迁,用 `git push --mirror`,不需要它。**
2. **构建时在线拉 GitHub 资源** —— 这正是**已经提前拆掉**的雷:hugo 二进制改为随仓库携带。
### 顺带纠正一个概念
**「账号被标记」≠「IP 被墙」。** 标记是账号级限制;匿名 HTTP 访问 github.com 的公开资源不受影响
(实测国内节点 `curl` hugo release 仍返回 302 正常)。所以即便真要在构建里拉 GitHub,也只是**慢**,不是**不通**。
### 你真正要做的三件准备(都与 GitHub 无关)
1. 微信扫码注册 CNB
2. 微信扫码完成**实名认证**(官方说明:未实名用户禁止写行为 —— 创建仓库 / 推送代码)
3. 先建一个**组织**,仓库建在组织下
---
## 一、决定性发现:答案你自己早就写在代码里了
`deploy.yml` 第 290 行起,那段被 `if: false` 停用的 `Upload to UpYun`,注释原话:
```
│ 2026-10-03 起永久跳过海外直传:本 runner 在境外,对 UpYun v0 接口
│ 无论 -w 10/--strong 还是 -w 3 均为 70 秒左右 EOF,3 轮重试全败,
│ 每次部署白耗 5 分钟(2026-10-03 build #25 日志实锤)。
│ 国内线路由腾讯云广州中转机兜底(1Panel 计划任务「又拍云同步(国内
│ 中转)」每分钟比对 COS build hash → 下载 355M → upx sync →
│ 又拍云 purge → 多吉云刷新,实测 5 分钟内完成且它自己会刷 CDN)。
│ 将来若把 runner 挪到国内,把下面的 if: false 删掉即可恢复。
```
**当初被迫上中转机,唯一的原因是「runner 在境外」。**
链路是这样被逼出来的:
| 环节 | 为什么存在 |
|---|---|
| runner 在境外 | Gitea 跑在境外 VPS |
| 直传又拍云失败 | 境外 → 又拍云 v0 接口,**跨境 + 并发敏感 → 70 秒 EOF,必败** |
| COS 中转 | 境外的产物,国内机器得有个地方取 |
| 广州中转机 + sync.sh | 只有它在国内,能直传又拍云 |
**CNB 的构建节点就在国内(腾讯云)。** 那句注释的复活条件直接成立 —— 中转机、COS、artifact 传递全都失去了存在理由。
---
## 二、目标架构
```
写作 / 本地
│ git push
▼
CNB(腾讯云,构建节点在国内 4 核 8 GiB)
│ 一次构建,三条出口
├──► 又拍云存储(境内源站) upx sync + upx purge
│ │ 多吉云回源自动跟随
│ └──► 多吉云 CDN 刷新(API 精确刷新)
│
└──► EdgeOne Pages(境外) npx edgeone pages deploy --area overseas
```
**保留不动**(零改动):
- 又拍云存储 `imzql`(境内源站)
- 多吉云 CDN(境内解析,回源又拍云)
- EdgeOne Pages 项目 `hugo-blog`(境外解析,`--area overseas`)
- 域名解析、ICP 备案、Ying 主题、content 内容
**替换掉**:
- Gitea Actions `build` job → CNB 流水线
- act_runner → 不需要
- COS / Gitea artifact 传递 → 不需要(含那串 v3/v4 坑)
- 广州中转机 `sync.sh` 轮询 → 不需要
- `deploy.yml` 里 8 处 `github.server_url` 兼容分支 → 不需要(另写 `.cnb.yml`)
---
## 三、CNB 能力核对(全部官方文档核实)
| 能力 | 值 | 对本项目的意义 |
|---|---|---|
| 构建节点 | 默认 8 核 16 GiB(**内存 = CPU 核数 × 2**) | 全面优于广州机 2 核 1.9 G |
| 免费额度 | **160 核时/月**(0.125 元/核时超出) | 见下方换算 |
| 构建最大时长 | 20 小时;单任务无输出 10 分钟 / 超 1 小时触发超时(可声明,上限 12 小时) | 传 372MB 绰绰有余 |
| 自定义环境 | `docker.image` 或 `docker.build`(Dockerfile) | 可固定 hugo 0.128.2 extended |
| 密钥管理 | 密钥仓库 + `imports` 导入环境变量 | 又拍云/多吉云/EdgeOne 凭据不落仓库 |
| 定时任务 | 支持 cron,最小间隔 5 分钟,时区 Asia/Shanghai | 可替代每日定时构建 |
| **出网限制** | **无白名单限制**(官方:出口 IP 动态,明确不建议白名单) | 又拍云 / 多吉云 / EdgeOne API 都可直连 |
| Docker-in-Docker | 支持(`services: docker`) | 备用 |
| 官方集成 | 腾讯云官方有 **CNB → EdgeOne** 的部署文档示例(`npx edgeone makers deploy`) | 已验证路径 |
### 额度换算(关键决策数字)
| 规格 | 内存 | 每次构建耗(按 10 分钟) | 160 核时可用次数 |
|---|---|---|---|
| 8 核 | 16 GiB | 1.33 核时 | **120 次/月** |
| **4 核** | **8 GiB** | **0.67 核时** | **240 次/月** |
| 2 核 | 4 GiB | 0.33 核时 | 480 次/月 |
你的触发频率是「每天 1 次定时 + 每次发文」≈ **60–90 次/月**。
**建议用 4 核 8 GiB** —— 240 次/月的余量,即使构建耗时翻倍到 20 分钟也还有 120 次。hugo 构建吃 CPU(4 核够并行编译),`upx sync` 吃网络不吃 CPU。
---
## 四、落地骨架
### 4.0 第一步:仓库迁移与远端配置(CNB 主仓 + 自建 Gitea 辅仓)
**目标形态**
```
origin → https://cnb.cool/<组织>/<仓库> ← **主仓**(fetch + push),CNB 构建由它触发
gitea → http://23.254.236.47:3001/zqlit/blog ← **代码同步辅仓**(只推不拉,不参与构建)
```
> **2026-10-06 定案:CNB 主仓 + 自建 Gitea 辅仓,另加一份离线 bundle 备份。**
>
> | remote | 地址 | 角色 |
> |---|---|---|
> | `origin` | `https://cnb.cool/zqlit/blog.git` | **主仓** —— 唯一的 fetch 源,推送即触发 CNB 构建 |
> | `gitea` | `http://23.254.236.47:3001/zqlit/blog.git` | **代码同步辅仓** —— 只作代码留档与找回,不参与构建 |
>
> - **GitHub**(原 `gh` remote):账号被平台标记、仓库被按 AUP 清空 → 已彻底移除,**别再往回加**
> - **Gitee**:单仓 500MB / 单文件 50MB 两道硬门槛过不去(本仓 `bin/linux/hugo` 83.1MB)→ 未采用
>
> **备选异地备份 = 「整仓加密 bundle → 中兴 F50 上的 OpenList」**,与 git 远端无关。
> 它和 Gitea 辅仓不是一回事、也不能互相替代:
> 辅仓保「随时能 `clone` 回来」,bundle 保「平台全挂/账号被封时仍有一份完整历史」。
> 选型依据见 `架构总览.md` §5.3,实现与恢复流程见 §5.6。
`git pushall` 别名 = `git push origin main; git push gitea main` ——
用 `;` 而非 `&&`,主仓失败时辅仓照样推;退出码以主仓为准(辅仓失败不阻断发布)。
**★ 为什么不用「一个 remote 挂多个 pushurl」**(这条教训仍然有效)
早期的 `origin` 就是那种写法(pushurl 同时挂了 GitHub 和自建 Gitea)。但它有个被忽视的缺陷,我做了实测:
| 场景 | 结果 |
|---|---|
| 两个 pushurl 都可用 | ✅ 两个都收到,一次 push 搞定 |
| **第一个失败** | ❌ **git 立即中止,第二个根本不会被推** |
也就是说:**主仓推失败时,辅仓那份备份也不会更新** —— 备份的意义就没了。
所以当时才拆成两个独立 remote,别名里用 `;` 而不是 `&&`,保证互不阻塞。
**现行方案正是回到这个写法**:`origin`(CNB) 与 `gitea`(自建) 各自独立成 remote,
别名 `;` 串联、退出码取主仓 —— 这就是现在的 `git pushall`。
**脚本**(已就位,可直接用)
```bash
bash scripts/setup-cnb-remotes.sh https://cnb.cool/<组织>/<仓库>
```
它会先把你现有的远端配置(含明文凭据)备份到 `.workbuddy-backup/git-remotes.<时间戳>.txt`,
再重建 `origin`、把 `pushall` 设为 `git push origin main; git push gitea main`,
并顺手清掉 `gh` / `gitee` 这两个已退役的远端 —— 全程可回退。
> Gitea 的凭据**不写死在脚本里**,要现给:
> `GITEA_PASS='<Gitea 密码或访问令牌>' bash scripts/setup-cnb-remotes.sh <CNB_URL>`
> 没给就只配 `origin`(`pushall` 里的 gitea 段会被自动跳过)。
**★ CNB 不支持 SSH(官方明确)**
> SSH Not Supported —— 平台不支持 SSH 协议访问。
> 认证方式:用户名**固定填 `cnb`**,密码填**访问令牌**。
> —— docs.cnb.cool《Git Address and Authentication Instructions》
这跟 GitHub 的 SSH 不同,所以两段要各用各的认证:
```bash
git config --global credential.helper manager # Windows 的 Git Credential Manager
# 首次 push 时:用户名 cnb,密码 = 访问令牌(个人设置 → 访问令牌 → 勾「代码仓库/读写」)
```
仓库地址直接用仓库页地址即可(`https://cnb.cool/组/仓`),带不带 `.git` 都支持。
**建仓顺序不能乱**(官方要求)
1. 微信扫码注册 → 2. 微信扫码**实名认证**(未实名禁止建仓/推码)→ 3. 先建**组织**(CNB 无「个人私有仓库」,仓库必须挂在组织下)→ 4. 建仓库 → 5. 跑上面的脚本 + push
### ⚠️ 推 CNB 之前必须先知道的敏感文件(已审计)
这些文件**当前就被 git 跟踪**,会在首次 push 时连**全部历史**一起进 CNB:
| 文件 | 内容 | 状态 |
|---|---|---|
| `write-server/nginx/ssl/privkey.pem` | **真实 TLS 私钥**(EC P-256,`BEGIN PRIVATE KEY`),2026-06-22 起在库 | 已跟踪 |
| `.env` | `SILICONFLOW_API_KEY`、`ZHIPU_API_KEY` | 已跟踪 |
| `docs/GITEA_SECRETS.md` | Gitea 相关凭据说明 | 已跟踪 |
| `content/posts/2024/.../setup-secrets.png` | 疑似密钥配置截图 | 已跟踪 |
**关于暴露面**:该仓一直是**私有**的(匿名 API 返回 404)。2026-10-06 账号被平台标记后
仓库更被按 **AUP 合规**清空、本地也移除了 `gh` remote → **这条威胁面已彻底消失**。
**但两件事仍建议做掉**:
1. **CNB 仓库保持「私有」** —— 这些文件在私有仓里影响可控
2. **`git rm --cached` 停止继续跟踪**(历史里的删不掉,但至少不再新增):
```bash
git rm --cached .env docs/GITEA_SECRETS.md write-server/nginx/ssl/privkey.pem write-server/nginx/ssl/fullchain.pem
```
⚠️ 注意:`.gitignore` 里其实**已经有** `.env` / `.env.*` 规则 —— 它们失效的原因不是规则写错,
而是**忽略规则对「已跟踪」文件无效**,所以必须显式 `git rm --cached`。
(另:`.gitignore` 里那条 `write\.env` 是写错的 —— gitignore 中 `\` 是转义符,
它实际匹配文件名 `write.env` 而非 `write/.env`;幸好 `write/.gitignore` 里有 `.env*` 兜住了。)
**历史清理(`git filter-repo`)的判断已变**:当初不做的唯一顾虑是「会让备份分叉」——
2026-10-06 GitHub 那份已消失,**这个顾虑没有了**;且离线 bundle 已加密
(见 `架构总览.md` §5.6),不受影响。
所以**现在是彻底抹掉这些文件历史的最佳窗口**。
代价:全部提交 SHA 改变、自建 Gitea 那份需重推。
---
### 4.1 `deploy/Dockerfile`(构建环境)
CNB 默认镜像不含 hugo / upx,写一个专属镜像固定版本:
```dockerfile
FROM node:22-bookworm-slim
# 换国内 apt 源(CNB 只明示加速 Docker/NPM/Maven,apt 默认源在国内节点可能慢)
RUN set -eux; \
for f in /etc/apt/sources.list /etc/apt/sources.list.d/debian.sources; do \
if [ -f "$f" ]; then \
sed -i -e 's|deb.debian.org|mirrors.aliyun.com|g' \
-e 's|security.debian.org|mirrors.aliyun.com|g' "$f"; \
fi; \
done; \
apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl tar zstd python3 xz-utils git \
&& rm -rf /var/lib/apt/lists/*
# Hugo extended 0.128.2(linux/amd64)——★ 随仓库携带,构建时零跨境下载
COPY bin/linux/hugo /usr/local/bin/hugo
RUN chmod +x /usr/local/bin/hugo && hugo version
# 又拍云 upx
RUN set -eux; \
curl -fsSL -o /tmp/upx.tgz \
"https://collection.b0.upaiyun.com/softwares/upx/upx_0.4.9_linux_amd64.tar.gz"; \
tar -xzf /tmp/upx.tgz -C /usr/local/bin upx; \
chmod +x /usr/local/bin/upx; \
rm /tmp/upx.tgz
# EdgeOne CLI
RUN npm i -g edgeone@latest
CMD ["hugo", "version"]
```
> ✅ 已实测(2026-10-04):
> 1. **hugo 二进制改为随仓库携带**(`bin/linux/hugo`,84MB,进 git 后仅 24MB)。实测国内节点直连 GitHub 拉这个 release 要 **127 秒**(本机只要 7 秒),而每次构建都要付这个成本 → 自带更稳。
> 2. 又拍云 `upx` 的下载源是 `collection.b0.upaiyun.com`(国内 CDN),直连稳定,保留在线安装。
> 3. **架构不能混用**:`E:\Hugo\hugo.exe` 是 `windows/amd64`(本地预览用),CNB 构建节点是 `linux/amd64`(`bin/linux/hugo`),两者互不替代。
> 4. ~~若以后主仓改选 Gitee(单仓 500MB),这 84MB 会挤占配额~~ —— **2026-10-06 定案:不走 Gitee**(它卡单文件 50MB / 单仓 500MB 两道门槛)。现行辅仓是**自建 Gitea**(自己的机器,没有配额这回事,见 §4.0),离线备份走 OpenList 上的加密 bundle(`架构总览.md` §5.6)。这条约束自然消失,`bin/linux/hugo` 继续随仓库携带即可。
> 5. **apt 源改阿里云镜像**:CNB 官方只明示加速「Docker / NPM / Maven 镜像」,Debian 默认 apt 源不在承诺范围内,而构建节点在国内 → 换阿里云兜底,避免构建卡在 `apt-get update`(首轮构建时留意这一步耗时)。
### 4.2 `.cnb.yml`(流水线)
```yaml
main:
push:
- name: 构建并发布(境内外双线路)
imports:
# CNB「密钥仓库」(私有;禁止 clone、只能 Web 编辑),存放 10 项凭据。
# 已在 .cnb.yml 里填好:zqlit/blog-secrets,本地草稿见 cnb-secrets.yml(已 gitignore)
- https://cnb.cool/zqlit/blog-secrets/-/blob/main/secrets.yml
runner:
cpus: 4 # 4 核 → 8 GiB
docker:
build: deploy/Dockerfile
stages:
- name: Hugo 构建
script: |
set -euo pipefail
hugo version
python3 scripts/add_draft_to_hidden.py
rm -rf public resources/_gen
hugo --minify --gc
BUILD_HASH=$(find public -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum | awk '{print $1}')
echo "$BUILD_HASH" > public/.build_hash
echo "$BUILD_HASH" > public/build_hash.txt
echo "$BUILD_HASH" > .build_hash.out
echo "✅ 构建完成 hash=${BUILD_HASH:0:12} 文件数=$(find public -type f | wc -l)"
- name: 同步到又拍云(境内源站)
script: |
set -euo pipefail
upx login "$UPYUN_SERVICE" "$UPYUN_OPERATOR" "$UPYUN_PASSWORD"
upx sync public/ / -w 5 --delete
upx put public/.build_hash /__build_hash || true
echo "✅ 又拍云同步完成"
- name: 刷新又拍云 CDN
script: |
set -euo pipefail
# ★ 必须刷固定入口页:只刷首页会让 sitemap.xml / rss.xml /
# archives.html 一直发 CDN 里的旧副本(2026-10-01 实测复现)
{ echo "https://usj.cc/";
for p in sitemap.xml rss.xml archives.html posts.html comment.html links.html; do
echo "https://usj.cc/$p"
done
} > /tmp/purge.txt
upx purge --list /tmp/purge.txt \
|| echo "⚠️ purge 失败,降级不影响源站已更新"
- name: 刷新多吉云 CDN
script: |
set -euo pipefail
python3 - <<'PY'
import hmac, hashlib, json, os, urllib.request
AK, SK = os.environ['DOGE_AK'], os.environ['DOGE_SK']
SITE = 'https://usj.cc'
P = '/cdn/refresh/add.json'
urls = [SITE + '/', SITE + '/sitemap.xml', SITE + '/rss.xml',
SITE + '/archives.html', SITE + '/posts.html']
def refresh(rtype, u, label):
body = json.dumps({'rtype': rtype, 'urls': json.dumps(u)})
sig = hmac.new(SK.encode(), (P + '\n' + body).encode(), hashlib.sha1).hexdigest()
req = urllib.request.Request(
'https://api.dogecloud.com' + P, data=body.encode(),
headers={'Authorization': 'TOKEN %s:%s' % (AK, sig),
'Content-Type': 'application/json'}, method='POST')
with urllib.request.urlopen(req, timeout=30) as r:
print('%s -> %s' % (label, r.read().decode()[:200]))
refresh('path', [SITE + '/'], '目录刷新')
refresh('url', urls, '精确刷新 %d 条' % len(urls))
PY
- name: 部署 EdgeOne(境外线路)
script: |
set -euo pipefail
test -n "$EDGEONE_API_TOKEN" || { echo "缺 EDGEONE_API_TOKEN"; exit 1; }
npx edgeone pages deploy ./public -n hugo-blog -t "$EDGEONE_API_TOKEN" --area overseas
- name: 上报部署状态
script: |
set -euo pipefail
H=$(cat .build_hash.out)
curl -s --max-time 20 -X POST \
"https://api.200181.xyz/api/deploy-status?token=${RSS_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"hash\":\"$H\",\"phase\":\"built\"}" || true
curl -s --max-time 20 -X POST \
"https://api.200181.xyz/api/deploy-notify?token=${RSS_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"hash\":\"$H\"}" || true
```
---
## 五、两个必须复刻的细节(`sync.sh` 里的血泪)
### 5.1 purge 不能只刷首页
`sync.sh` 的注释记录了一个实测坑:
> 历史写法只有 `upx purge "$SITE/"`,即只刷首页 —— `sitemap.xml` / `rss.xml` /
> `archives.html` / `posts.html` 这些「老路径」会一直发 CDN 里的旧副本。
> 2026-10-01 实测复现:中转机产物 sitemap.xml 已含新文章(22812B),
> CDN 上那份却仍是 7/24 的 22592B;手动 purge 后立刻变新。
所以骨架里固定刷「首页 + 6 个入口页」。**进阶做法**(完整复刻 `sync.sh` 的精确刷新):
把文件清单存到又拍云 `/__manifest.txt`,每次 CNB 拉下来与本次产物比对,得出「变化 URL」精确刷新。
好处是新增文章时只刷那几页;代价是多维护一个清单文件。**建议先跑固定入口版,观察 1–2 周再决定要不要加。**
### 5.2 又拍云并发参数
`sync.sh` 用 `-w 10 --strong`。当时失败是因为**跨境**(境外 runner 直传),国内直传理论无此问题。
但建议首次仍保守用 `-w 5`(去掉 `--strong`,它会把请求量放大),实测稳定后再往上调。
---
## 六、砍掉清单
| 砍掉 | 说明 |
|---|---|
| act_runner | 不再需要自建 runner |
| Gitea artifact 传递链 | 含 `upload-artifact@v3/v4` 那串坑 |
| 腾讯云 COS | 中转存储不再需要 |
| 广州中转机 `sync.sh` + 1Panel 定时任务 | **注意:只停任务,机器上还有 mysql/openresty 等 7 个容器** |
| `deploy.yml` 的 8 处 `github.server_url` 补丁分支 | `.cnb.yml` 重新写 |
| 两套冗余通知(TG/飞书/邮件三选一) | 独立小优化 |
**发布链路:7 环节 → 3 环节**(写作 → CNB 构建 → 双线上线)
**自维护构建节点:1 台 → 0 台**
---
## 七、前置动作与待核实项
### 必须先确认(按重要性)
1. **★ EdgeOne 从国内上传 372MB 的耗时**。现在部署是从境外 runner 上传(同区域快),改到 CNB(国内)后是跨境上传到 EdgeOne 国际站。**这是本方案最大的未知数**,必须实测一次。
2. **★ CNB 构建节点能否顺畅拉取 `github.com` 的 hugo release**。不行就换国内镜像源,或把 hugo 二进制预置进 CNB 制品库。
3. **又拍云 v0 接口从 CNB 出口的真实表现**。国内直传预期无碍,但需实测确认(这是整个方案的立论基础,务必第一次就验)。
4. 主仓放哪:
- **CNB 当主仓** → 最简,Gitea 可退役,写作后台改 `origin`
- **Gitea 当主仓 + push mirror 到 CNB** → 写作零改动,代价是多留一台机器
5. 多吉云刷新配额(`sync.sh` 里有 200 条截断逻辑,CNB 版本固定入口页不触及)。
### 建议的验证步骤(不动任何现有东西)
1. 建一个 CNB 测试仓库(或就用一个分支),放最小化 `.cnb.yml` + Dockerfile
2. 跑通「hugo build」→ 确认镜像与构建时长
3. 单独测「`upx sync` 到一个测试 bucket」→ 确认国内直传可行
4. 单独测「`edgeone pages deploy` 到测试项目」→ 拿到跨境上传耗时
5. 四项全绿,才动正式仓库
---
## 八、与之前几份方案的关系
| 方案 | 结论 | 与本方案的关系 |
|---|---|---|
| `代码源与构建平台选型.md` | CNB 首选(100GiB 仓库 + 160 核时) | 本方案是它的落地细化 |
| `Gitee方案评估.md` | 可用,但需瘦身 588MB → 500MB | **用 CNB 就不用瘦身** |
| `EdgeOne双区域方案评估.md` | 建议 `-a global` 或双项目 | 双区域照旧,只是构建方换成 CNB |
| `阿里云ESA评估.md` | 2000 文件上限卡死 | 不适用 |
| `精简方案-只留CF和Hugo.md` | 只留 CF 会丢又拍云+多吉云 | **本方案保留双 CDN,复杂度收益同样大** |
**核心区别**:之前几份在讨论「CDN 选谁」;本方案证明**你不需要换 CDN,只需要换掉「构建 + 搬运」那一层**。而这一层恰恰是复杂度的大头。