740d77e4cb3ae5e55428fbebaecd8dcbe4b64362
一、1Panel 部署器三个缺陷(其中两个此前完全不可见)
1. `POST /websites/{id}/https` 的字段名是 `websiteSSLId`,不是 `sslId`。
发 `sslId` 会被 Go 静默忽略成零值 0,于是
`websiteSSLRepo.GetFirst(WithByID(0))` → 返回
`HTTP 200 + code 500「服务错误: record not found」`,
报错文案落在 DB 层,完全指不到参数名 —— 整个 t-t.live 部署被这条卡住。
对照实验:`sslId=13 → 500` / `websiteSSLId=13 → 200 code=200`。
2. 换证书内容的接口选错。`POST /websites/ssl/update` 的结构体
`WebsiteSSLUpdate` **根本没有** `certificate` / `privateKey` 字段,
传了被丢弃、且 `domains` 只从 `otherDomains` 取(不传就清空),
还会顺带把 `autoRenew` 置 false —— 而它**照样返回 200 success**。
实测:原样 update 后 5 个站点的 `ssl/*.pem` mtime+md5 一个都没变。
正确接口是 `POST /websites/ssl/upload` + `sslID > 0`:取记录 → 覆盖
→ 重算 ExpireDate/domains → `UpdateSSLConfig()` → 重新物化站点文件。
副作用:`Upload()` 把 `primaryDomain` 重算成证书第一个 SAN
(#11 因此从 `usj.cc` 漂成 `*.usj.cc`)→ 必须补 `domains` 兜底匹配,
否则每次续期都新建一条重复记录。
3. `deploy()` 幂等捷径漏了「记录被换过」这一维:`sslId` 没变但内容变了时
会跳过绑定,站点文件就停留在旧证书。改为引入 `replaced` 标志强制重绑。
二、另外两处静默失败
4. `parsePemInfo()` 对**完整链**返回 `{}`:旧实现把 PEM 各段 base64 拼接后
一次 `atob`,中间段尾部的 `=` 填充导致抛错,整函数返回空 →
`rec.expireAt` 退化成「签发时刻 + 90 天」。而完整链恰恰是部署器最常
拿到的形态。改为只解析第一段(叶证书)。
顺带新增 `derLen()` / `parseSanFromDer()`,精确定位 SAN 扩展
OID `2.5.29.17` 再读 `[2] dNSName`,替掉原来的字节扫描启发式。
这条同时是「多吉云复用失效」的根因:判据缺到期时间,只看域名集合
就永远认为已覆盖 → 续期静默空转。修好后判据带上 `notAfter` 比对(1 天容差)。
5. DNS-01 挑战通知会撞 `400 authorization must be pending`(200181.xyz 连中两次)。
这是**竞态**不是逻辑错:「先读状态再 POST」挡不住毫秒级窗口。
已在 POST 侧做幂等容错(只认这一句),最终由 `pollAuthz` 定论。
三、Worker 退回只读
- `crons` 去掉 `10 4 * * *`,`index.ts` 里 renew 分支整体删除
- `POST /ssl/issue` 改 **501 硬拒绝**(而不是静默降级),响应给出国内机命令
- 签发 + 部署整条链路跑在国内机容器 `cn-certkeeper`
四、顺带修掉的两个「配置被悄悄抹掉」
- `configSave()` 不再丢掉表单不管理的 `probe_connect` / `probe_sni`。
之前管理员在面板改任何一项,这两个字段就会被清空,
后果是挂在 CDN 后的 t-t.live 探针退回公网、被误判成「还剩 80 多天」,
源站证书到期也不续。现在保存时从旧配置带过来。
五、新增 4 个常驻运维工具(deploy/cn-certkeeper/src/)
- `renew-one.mjs` 只对单个域名签发+部署(原 `/renew` 无域名过滤,会全量重签)
- `redeploy.mjs` 复用已签好的证书只重跑部署(不碰 ACME,不白烧配额)
- `rollback-dogecloud.mjs` 应急把 CDN 域名绑回指定证书 id
- `txt-inspect.mjs` `_acme-challenge` 下的 TXT 残留盘点/清理
- `selfcheck-acme.mjs` CA 层诊断:只读目录 + 复用账户,不签发不部署
六、测试与文档
- 自测新增 [11] 节 8 项 PEM 解析回归(样本是 openssl 现场生成、内联写死的
叶+中间证书,两段都以 `=` 结尾,正是 bug 现场),含精确值断言:
> 125 项通过,0 失败
- `npm run typecheck` 零错误
- 方案文档:§9.11 由「待验证」改为定案(`ssl/update` 不物化、
`ssl/upload+sslID` 才物化);新增 §9.12「本轮又修掉的 5 个静默失败」、
§9.13「本轮最终状态」、§9.14「certimate 工作流清查」
线上验收:三个域名(t-t.live / usj.cc / 200181.xyz)线上证书均为
LiteSSL ECC、2027-01-04 到期、daysLeft=90、needRenew=false;
1Panel 证书库 5 条精简为 3 条且全部在用;
certimate 停掉全部「会签发并部署」的工作流(团团 / 优世界 / 200181.xyz),
保留三条纯监控告警。
优世界博客(usj.cc)
Hugo 静态博客 + 自研评论后端 + 写作后台。 构建与发布跑在腾讯云 CNB(国内节点),单次发布约 3.5 分钟,境内/境外两条线路一次推完。
架构全貌、迁移前后对比、运维要点 →
架构总览.md
一、项目构成
| 子系统 | 位置 | 技术栈 |
|---|---|---|
| 内容 | content/、themes/Ying/ |
Hugo 0.128.2 extended + Ying 主题 |
| 评论后端 | blog-admin/ |
artalk-cf:Cloudflare Workers + D1 + KV(api.200181.xyz) |
| 写作后台 | write-server/(线上 post.usj.cc)、write/(本地 Windows) |
Next.js |
| 发布 | .cnb.yml、deploy/ |
CNB 流水线 + Dockerfile |
二、目录结构
blog/
├── .cnb.yml # ★ CNB 流水线(push + 每日定时)
├── deploy/Dockerfile # 构建镜像(hugo 二进制由 bin/linux/hugo 提供)
├── bin/linux/hugo # Hugo extended 0.128.2(linux/amd64,供 CNB 构建用)
├── content/
│ ├── posts/<年>/<日期>-<标题>/ # 文章(Page Bundle,index.md + 图片)
│ ├── about.md / links.md / circles.md / archives.md
├── themes/Ying/ # 主题(layout / assets / data)
├── static/ # 原样复制进产物(emotion 表情、image、js …)
├── blog-admin/ # 评论后端(artalk-cf)
├── write-server/ # 线上写作后台
├── write/ # 本地写作前端
├── scripts/ # 各类工具脚本(见第六节)
├── hugo.toml # Hugo 主配置
└── 架构总览.md # ★ 架构文档
三、内容写作
文章结构
每篇文章是一个 Page Bundle:
content/posts/2024/2024-05-01-文章标题/
├── index.md # 正文
└── 配图.jpg # 同目录图片(可用相对路径引用)
URL 规则
由 front matter 的 slug 决定(hugo.toml 里 permalinks.post = "/:slug"):
---
title: "我的文章"
date: 2024-05-01
slug: "my-post"
---
生成 https://usj.cc/my-post.html(uglyURLs,带 .html)。
隐藏文章
在 front matter 加 status: hidden。构建前 scripts/add_draft_to_hidden.py 会把它转成
draft: true,不出现在列表里,但直达链接仍可访问。
本地预览
hugo server -D # 含草稿
四、发布流程
写作(write-server / write/)
│ git pushall
▼
CNB 主仓 zqlit/blog ──触发──► CNB 流水线(国内节点,约 3.5 分钟)
├─ Hugo 构建
├─ 同步到又拍云(境内源站)
├─ 刷新又拍云 CDN
├─ 刷新多吉云 CDN
├─ 部署 EdgeOne Pages(境外线路)
└─ 邮件通知
- 推送:
git pushall=git push origin main; git push gh main(origin= CNB 主仓,gh= GitHub 备份) - 触发:推送到
main;另有每日0 9 * * *(北京时间)定时构建 - 密钥:全部来自 CNB 密钥仓库
zqlit/blog-secrets,经.cnb.yml的imports注入, 仓库里没有任何明文密钥 - 改动
main即自动上线,本地无需构建
五、三个子系统
内容系统
Hugo + Ying 主题。hugo.toml 控站点信息、永久链接、Artalk 地址、弹幕等。
评论系统(blog-admin/)
自研的 Artalk v2 兼容服务端,跑在 Cloudflare Workers + D1(SQLite)+ KV:
- 前端用官方 Artalk 客户端(
themes/Ying/assets/js/libs/Artalk.js,本地打包,非 CDN) - 后端 API 基址
https://api.200181.xyz(评论/api/v2/*与 RSS 订阅/api/*同一 Worker) - 部署:
cd blog-admin && npm run deploy(详细步骤见blog-admin/README.md、部署清单.md)
写作后台
write-server/:线上版(Next.js),部署在独立主机write/:本地 Windows 版
六、常用脚本(scripts/)
| 脚本 | 用途 | 在哪跑 |
|---|---|---|
add_draft_to_hidden.py |
构建前把 status: hidden 转成草稿 |
CI |
refresh_cdn.js |
刷新多吉云 CDN(零依赖) | CI |
send_mail.js |
构建结果邮件通知(零依赖 SMTP) | CI |
setup-cnb-remotes.sh |
切换/重建 git 远端(CNB 主仓 + GitHub 备份) | 本机 |
optimize_images.js |
图片批量压缩优化 | 本机 |
generate_circle_data.js |
抓友链 RSS 生成朋友圈数据 | 本机 |
update_link_lite_json.ps1 |
友链 links.yaml → JSON |
本机 |
add_ancient_chars.py / check_ancient_chars.py / merge_chars.py |
字体生僻字增补与校验 | 本机 |
cleanup_duplicates.js / migrate_slugs.js |
一次性维护脚本 | 本机 |
deploy_*.sh(又拍云 / EdgeOne / 定时)是本机手动部署的旧入口,日常已不需要—— 推送main由 CNB 自动完成。
七、相关文档
| 文档 | 内容 |
|---|---|
架构总览.md |
当前架构全貌(子系统、发布链路、迁移前后对比、运维要点) |
CNB构建落地方案.md |
迁 CNB 的实施方案与实测数据 |
代码源与构建平台选型.md |
平台对比(CNB / Gitee / GitLab / EdgeOne / 阿里云 ESA) |
砍COS改造步骤.md |
腾讯云 COS 下线记录 |
blog-admin/README.md |
评论后端完整说明 |
blog-admin/部署清单.md |
评论后端部署步骤 |
Languages
TypeScript
42%
JavaScript
32.2%
CSS
13.8%
HTML
6.2%
Shell
3.4%
Other
2.4%