Files
blog/docs/archive/平台与选型/砍COS改造步骤.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

220 lines
9.4 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-06)。它记录的是**当时的评估与方案**,其中的结论可能已被后续决策推翻。
> **请勿据此判断当前架构** —— 现行架构唯一事实源是 [`架构总览.md`](../../../架构总览.md)。
# 砍掉腾讯云 COS 中转层 —— 完整改造步骤
> 目标:把「build → COS → 中转机 → 又拍云 → 多吉云」这条绕路,
> 改成「build → Gitea artifact →(中转机拉取)→ 又拍云 → 多吉云」,
> 砍掉腾讯云 COS 这一层,省下 COS 存储/流量账单。
> 状态(2026-10-04 更新):
> - ✅ `deploy.yml` 已改完并**已推送**
> - ✅ 中转机 `sync.sh.new`(artifact 版)已上传到 `/opt/upyun-sync/sync.sh.new`,**尚未生效**
> - ✅ Gitea 只读 token 已写入中转机 `/opt/upyun-sync/.gitea_token`(600 root:root)
> - ❌ 链路**尚未跑通**:run #55 倒在 v4 不支持,run #56 倒在 v3 的 finalize 500
> - ⏸️ 最新修复(单文件 tar,commit `1f568bbf`)**本地已提交、待推送**
> - ⚠️ `deploy.yml` 与 `sync.sh` 必须一起生效,否则国内线路停更(见下)
---
## 零、实测踩到的两个坑(都已修,记录备用)
### 坑 1:`upload-artifact@v4` 在 Gitea 上直接失败(run #55)
```
::error::@actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+
are not currently supported on GHES.
```
Gitea 被 v4 识别为 GHES,但它没实现新版 artifact API → **降级 v3**(commit `ed38f938`)。
### 坑 2:v3 传「目录」时 Gitea finalize 返回 500(run #56,耗时 30 分钟)
v3 收目录是**逐文件上传**:3216 个 blob / 376MB 全部传完(日志可见 `Processed file #3216`),
但最后的 finalize 调用被 Gitea 拒绝:
```
Finalize artifact upload - Attempt 1..5 of 5 failed with error: Request timeout
::error::Finalize artifact upload failed: Artifact service responded with 500
```
**结论:Gitea 扛不住「文件数多」的 artifact。** 改法(commit `1f568bbf`):
build 侧先把 `public/` 打成**单个** `public.tar.zst`,只上传这 1 个文件。
副作用是好的——产物结构从此完全确定,不再有「解出来会不会多一层 `public/`」的歧义。
---
## 一、为什么不能只推一半(重要)
deploy.yml 改完后,build 产物**不再上传 COS**,只进 Gitea artifact。
而国内中转机的 sync.sh 如果**还从 COS 拉**,就会:
- build 传 artifact → COS 没有新产物
- 中转机从 COS 拉 → 拿到的是旧产物 / 拉不到
- **国内线路(又拍云 + 多吉云)停更**
所以 `deploy.yml` 和 `sync.sh` 必须**同步改、同步推**,不能只推 deploy.yml。
---
## 二、已完成的改动(deploy.yml)
文件:`.github/workflows/deploy.yml`
### build job
- ❌ 删掉:「Setup Python」「Install/Configure coscmd」「Get last build hash from COS」「Check if upload is needed」「Upload to Tencent COS」「Upload hash to COS」
- ✅ 新增:「Upload build artifact (public/)」→ `actions/upload-artifact@v4`,name=`hugo-public`,path=`public/`,retention-days=7
- 保留:「Report deploy status (built)」上报 hash 到 `api.200181.xyz`
### deploy-edgeone job
- ❌ 删掉:「Check if deployment is needed」(last_hash 判断)、「Setup Python」「Install/Configure coscmd」「Download files from COS」
- ✅ 改成:`actions/download-artifact@v4`(name=`hugo-public`,path=`public/`)→ 直接 `npx edgeone pages deploy ./public`
- `deployed` 恒为 true(push 才触发,本就该部署)
### 清理
- 删掉所有 `download_duration` / `last_hash` / `need_upload` / `coscmd` 引用(通知、表格里的「下载 COS」等)
---
## 三、剩下的改动:sync.sh(国内中转机)
文件:`/opt/upyun-sync/sync.sh`(国内机 119.29.215.187,通过 1Panel API 或 SSH 操作)
### 要改的核心逻辑
原流程(第 1-3 步):
```bash
# 1. 取远端 hash:从 COS 读 /__build_hash(cos_sign.py 签名)
# 2. 比对本地 hash,无变化退出
# 3. 从 COS 下载 public.tar.zst
```
新流程:
```bash
# 1. 调 Gitea API 列出最新 artifact
# GET https://gitea.usj.cc/api/v1/repos/zqlit/blog/actions/artifacts?name=hugo-public
# (Header: Authorization: token <GITEA_TOKEN>)
# 2. 拿最新一个 artifact 的 id,下载 zip
# GET .../actions/artifacts/{id}/zip (302 重定向,curl -L 跟随)
# 3. 解压 zip → 读 public/.build_hash → 比对本地 hash
# 有变化才继续走「upx sync 又拍云 → purge → 刷多吉云」
```
### 需要的准备
1. **Gitea 只读 token**:
- Gitea 后台(gitea.usj.cc)→ 头像 → 设置 → 应用 → 生成令牌
- 名称随意(如 `upyun-sync`),勾选 `read:repository` 即可(只读够用)
- 生成后把 token 存到中转机 `/opt/upyun-sync/.gitea_token`(chmod 600)
2. 中转机访问 Gitea 的网络:已实测 0.56s,稳定,无问题。
### sync.sh 改动示例(第 1-3 步替换成如下)
```bash
# ---------- 取最新构建(从 Gitea artifact,替代原 COS)----------
GITEA="https://gitea.usj.cc"
REPO="zqlit/blog"
TOKEN=$(cat "$ROOT/.gitea_token" 2>/dev/null)
# 1. 列出 hugo-public artifact,拿最新 id + created_at
ART_LIST=$(curl -fsS --max-time 40 -H "Authorization: token $TOKEN" \
"$GITEA/api/v1/repos/$REPO/actions/artifacts?name=hugo-public" 2>/dev/null)
ART_ID=$(echo "$ART_LIST" | python3 -c "import sys,json; a=json.load(sys.stdin); print(a[-1]['id'] if a else '')")
[ -n "$ART_ID" ] || { say "❌ 没有找到 hugo-public artifact"; exit 1; }
# 2. 下载 artifact zip
rm -f "$ROOT/artifact.zip"
curl -fSL --max-time 600 -H "Authorization: token $TOKEN" \
"$GITEA/api/v1/repos/$REPO/actions/artifacts/$ART_ID/zip" \
-o "$ROOT/artifact.zip" || { say "❌ 下载 artifact 失败"; exit 1; }
# 3. 解压(artifact zip 里是 public/ 的内容)
rm -rf "$ROOT/public.new"; mkdir -p "$ROOT/public.new"
unzip -q "$ROOT/artifact.zip" -d "$ROOT/public.new" || { say "❌ 解压失败"; exit 1; }
# 4. 读 hash 比对(.build_hash 在 zip 里 public/.build_hash)
REMOTE=$(cat "$ROOT/public.new/.build_hash" 2>/dev/null | tr -d ' \r\n')
LOCAL=$(cat "$STAMP" 2>/dev/null | tr -d ' \r\n')
if [ "$REMOTE" = "$LOCAL" ] && [ -f "$PUBLIC/index.html" ]; then
say "无变化 ${REMOTE:0:12}…"; exit 0
fi
say "发现新构建: ${LOCAL:0:12}… -> ${REMOTE:0:12}…"
# 后续:原子切换 public.new → public,然后照旧 upx sync / purge / 刷多吉云
# (这部分逻辑不变,只是"下载产物"和"取 hash"的来源从 COS 换成了 artifact)
```
> ⚠️ 注意:artifact 解压出来的目录结构取决于 `upload-artifact` 的 path。
> deploy.yml 里 `path: public/`,所以 zip 里是 `public/...`,解压后要取 `public.new/public/`
> 或调整 path。实测一次 build 看 zip 结构最稳。
---
## 四、执行顺序(照着做)
### 第 1 步:生成 Gitea token
Gitea 后台 → 设置 → 应用 → 生成令牌 → 只读 → 复制 token
### 第 2 步:token 放到中转机
```bash
# 通过 1Panel API 或 SSH(国内机)
echo "你的token" > /opt/upyun-sync/.gitea_token
chmod 600 /opt/upyun-sync/.gitea_token
```
### 第 3 步:改 sync.sh
按上面「三」的示例,把「取 hash + 下载」两段从 COS 换成 Gitea artifact。
### 第 4 步:推 deploy.yml
```bash
cd /e/GitHub/blog
git add .github/workflows/deploy.yml
git commit -m "chore: 砍掉 COS 中转,build 产物改用 Gitea artifact 传递"
git push gitea main
```
### 第 5 步:验证
1. 看 Gitea Actions 这次 build 是否成功(重点看 `upload-artifact` 有没有报错——这是 Gitea 28 兼容性风险点)
2. 看中转机 sync.log 有没有「✅ 同步完成」
3. 打开 `https://api.200181.xyz/api/deploy-status` 看国内线路是否 synced
4. 访问 usj.cc 看国内是否更新
### 第 6 步:确认稳定后,清理 COS
- 删 Gitea secrets 里的 COS_SECRET_ID / COS_SECRET_KEY / COS_BUCKET / COS_REGION(先留着观察几天再删)
- 腾讯云 COS 桶 `hugo-1303964578` 可以清空或删桶(确认不再被引用后)
- 中转机的 `cos_sign.py` 可以删
---
## 五、风险点与回退
| 风险 | 说明 | 对策 |
|---|---|---|
| `upload-artifact@v4` 在 Gitea 28 兼容性 | v4 是新版 action,Gitea 兼容层可能只支持 v3 | 若报错,降级到 `actions/upload-artifact@v3` |
| artifact 保留期 7 天 | 若长时间不部署,artifact 过期 | retention-days 设大,或确认 deploy-status 兜底 |
| zip 目录结构 | `path: public/` 解压后是 `public/...` 还是直接文件 | 实测一次 build 看 zip 结构再定解压路径 |
| 回退 | 万一 artifact 链路不通 | 原 deploy.yml 备份在 `.workbuddy-backup/deploy.yml.20261004-085652`,`git revert` 即可回 COS |
---
## 六、原始备份位置
- deploy.yml 原始版本:`E:/GitHub/blog/.workbuddy-backup/deploy.yml.20261004-085652`
- 已改版本:`E:/GitHub/blog/.github/workflows/deploy.yml`(未推送)
---
## 七、关键信息速查
| 项 | 值 |
|---|---|
| 境外 Gitea | `23.254.236.47:3001`(域名 gitea.usj.cc),SSH root / `5I3fXqbV5Aco9aW9K9` |
| Gitea 数据 | docker `gitea/gitea:latest`,`/opt/gitea/data`,SQLite |
| 国内中转机 | `119.29.215.187:3721`(1Panel),又拍云同步任务 ID=15 |
| 中转机 sync.sh | `/opt/upyun-sync/sync.sh` |
| Gitea artifact API | `GET /api/v1/repos/zqlit/blog/actions/artifacts` 和 `/{id}/zip` |
| 又拍云 | bucket=`imzql`,operator=`1770186415` |
| 多吉云 | AK=`4fa23701981899fb`(sync.sh 内已硬编码) |