Commit Graph
100 Commits
Author SHA1 Message Date
zqlit 3eeafa07b2 fix(证书管家): writeapi.usj.cc 证书纳管(相对软链接);pushall 改为先 rebase 再推
用户:「动手 推送吧」—— 动手处理两个有死线的遗留。

一、writeapi.usj.cc 的证书(12 月会断)→ 已解决

现状复核推翻了上一轮的判断:
  · 上一轮记的「它的 ssl_certificate 引用不在 /www/sites/ 树里」是**错的** ——
    /www/conf.d/writeapi.usj.cc.conf 里明确写着
    ssl_certificate /www/sites/writeapi.usj.cc/ssl/fullchain.pem;
  · 真正原因是**它在 1Panel 面板上根本不是一个网站**(面板共 11 个站点,没有它),
    所以 `findWebsite('writeapi.usj.cc')` 必然抛「找不到网站」→
    加进 one_panel_sites 只会让**每次续期都报错**(而且只警告不阻断,长期没人发现)
  · 实测确认:redeploy 时其余 5 个站点正常,只有它失败

解法:**不写一行代码** —— 它与 artalk.usj.cc 本来就同用一张 *.usj.cc 证书,
让它用**相对软链接跟随 artalk** 即可,随续期自动更新:
  ln -sfn ../../artalk.usj.cc/ssl/fullchain.pem /1panel/1panel/www/sites/writeapi.usj.cc/ssl/fullchain.pem

★★ 踩到的坑:软链接**必须相对路径**
  宿主 /1panel/1panel/www/sites/ ↔ 容器内 /www/sites/(1Panel 的 openresty 跑在容器
  `1Panel-openresty-Kmh2`)。第一版写成宿主绝对路径 → **容器侧断链** →
  nginx -s reload **静默失败**(保留旧配置、服务不中断、握手照旧拿旧证书)→
  表面全绿,真实后果是「等某天 nginx 重启,writeapi 的 HTTPS 直接挂」。
  唯一线索是 worker 进程的启动时间没变 —— **所以验收必须看 worker 是否真的重启**。

验收(全部实测):
  替换前 CN=usj.cc    / LiteSSL RSA CA / 2026-12-07(mtime 10-04 21:50)
  替换后 CN=*.usj.cc  / LiteSSL ECC CA / 2027-01-04
  容器内 head -1 → -----BEGIN CERTIFICATE----- / -----BEGIN PRIVATE KEY-----
  预检 syntax is ok / test is successful
  reload → signal process started;**worker 22:51:39 重启**(证明软链接真被读了)
  握手 SNI=writeapi.usj.cc → *.usj.cc / Jan 4 2027;实访 /health → HTTP 200

新增 deploy/cn-certkeeper/src/dump-cert.mjs:把 KV 里(AES-GCM 加密)的证书导成明文 PEM,
补上「KV ↔ 手工 vhost 磁盘文件」之间原先不存在的桥。只读,用完即删(含明文私钥)。

操作层的两个坑(写进文档):
  · 1Panel 文件 API **拒绝写 .mjs**(返回 500「目标路径不存在」,实为可执行扩展名过滤);
    往新建的 root:root 700 目录写也会失败 → 临时脚本用
    `docker exec -i … sh -c 'cat > 路径' < 本地文件` 送
  · cnrun.py(1Panel 计划任务通道)偶发「退出码 0 但零输出」;
    加 `timeout 30 docker exec …` 包一层即稳定,判断状态优先看容器内直接证据

二、配置回退 + 文档更正

  · certkeeper-config.mjs:把 writeapi 从 one_panel_sites 移除,换成一段注释说明
    「它不是 1Panel 站点,别加进来」+ 软链接做法 + 必须相对的警告;
    线上 config.kv 同步回退(sha256 与改动前完全一致 ccac2530…)
  · 更正「3 个手工 vhost」的说法:vaultwarden **是**正常纳管的站点(面板 #13,
    主域名 vw.usj.cc,别名才是 vaultwarden)→ 手工 vhost 实际只剩 writeapi + dnsapi
  · dnsapi.usj.cc 重新定性为**不做**:它是 cn-dns-helper 的反代入口
    (proxy_pass 127.0.0.1:8018,仅 HTTP),去留应与 cn-dns-helper 一起决定,
    不该单独给一个待退役的服务加 HTTPS

三、pushall 加固:先 fetch + rebase 再推(本次真实撞到的问题)

推 CNB 时被 rejected —— 因为线上写作后台(editor-api)发文章会**直接推 main**,
本机两笔提交与之分叉。这不是异常,是**日常**。原别名直接 push,必然反复撞。

  · 新增 scripts/pushall.sh:fetch → 已在远端之后则 rebase → 推所有远端
    - 冲突时**停在 rebase 中途**交人工(不强推、不丢东西)
    - 工作区不干净时 git 自己会拒绝 rebase(不会吞改动)
    - 任一远端失败**不改判另一个**(主仓失败辅仓照样推),退出码以第一次失败为准
    - ⚠️ 修了自己写的一处疏漏:`if ! cmd; then ec=$?` 的 $? 是**取反后**的结果(0),
      不是 git 的退出码 —— 必须显式 ec=1(靠 set -o pipefail 保证管道退出码不被 sed 掩盖)
  · setup-cnb-remotes.sh 的别名改指向脚本;README 脚本表补这一行

四、文档

  · docs/证书管家.md:速览表「已知遗留」→ 已解决;§9.14 遗留段重写(含纠正自己写错的判据
    「路径像不像不构成判据,要直接查面板站点清单」);**新增 §9.15**
    「手工 vhost 的证书:用相对软链接跟随已纳管站点」(机制 + 路径坑 + 三条验收 + dump-cert 用法)
  · docs/架构精简候选.md:候选 5 标记完成,补上「为什么 A 走不通、B 未采用」;
    执行顺序里划掉它
  · 架构总览.md:§5.7「已知遗留」重写(2 个手工 vhost + vaultwarden 更正);
    §6 待办 #13 改为已解决、#14 标注完成
2026-10-06 22:54:58 +08:00
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
zqlit d3da972c3b feat(editor-api): 线上容器切「CNB 主 + Gitea 备」双推;备份补 fetch 步骤
用户定案:「editor-api 容器 采用 cnb主仓 gitea备份仓 openlist文件备份」。
三层各有各的失效模式,不能互相替代:
  ① 主仓 origin = CNB          —— 源码 + 触发构建,依赖 CNB 平台
  ② 代码辅仓 gitea = 自建 Gitea —— 不受第三方平台规则约束,可 clone / 按提交追溯
  ③ 离线备份 = F50/OpenList     —— 单一加密文件,平台全挂也能恢复

一、线上容器实测切双推(国内机 119.29.215.187)

先探测出两个关键事实:
  · 国内机**能**直连自建 Gitea(/api/v1/version → 200,0.38s)
  · 国内机**够不到**家里的 OpenList(192.168.0.1 私有地址,6s 超时)
    → 「文件备份」这一层只能由家庭机发起,国内机做不到(下面的 fetch 修复正因此

改动:
  · /srv/blog 停在 432cf5e3,落后 origin/main 10 个提交且是其后代
    → git merge --ff-only 追平到 448b898e,**未强推**
  · 挂 gitea 远端(凭据编在 URL 里,同本机做法)
  · .env:PUSH_REMOTES=origin → origin,gitea;追加 GITEA_URL/USER/PASS
  · docker-compose.yml 默认值同步为 ${PUSH_REMOTES:-origin,gitea}
  · docker compose up -d --build 重建

验证(全部实测通过):
  · 启动日志 [editor-api] 分支 main 推送远端 origin, gitea
  · curl /health → "remotes":["origin","gitea"]
  · 容器内推 origin ✅ / 推 gitea ✅(推临时分支 → ls-remote 确认 → 删分支,无残留)
  · 三处 main 对齐 448b898e(本地 / origin / gitea);工作区干净

二、备份侧:修掉一个静默漏洞 —— 打包前必须先 fetch

git bundle create --all 取的是**本地已知** ref,其中 refs/remotes/origin/main
停在上次 fetch/pull 的位置。而这份备份跑在**家里**那台机器上,工作区并不会
随写作后台(editor-api)的发布自动更新。所以原先是:后台新发的文章**一篇都不
在备份里**,而备份照样报「成功」—— 失败是静默的。

  · backup-bundle.mjs 打包前插入 git fetch --all --tags --prune
    - 失败**不致命**(离线也得出得来备份)→ 降级为「用本地已有 ref 打包」+ 显著告警
    - 新增 --no-fetch 可跳过;步骤号 1/6..6/6 → 0/7..6/7
    - 新增一行 `快照 origin/main = <sha> <时间> <标题>` —— 恢复时第一件要确认的就是它
  · backup-run.mjs:头部补三层说明;失败邮件的「常见原因」加上 fetch 降级这一条

验证:fetch 3s 拉完 origin+gitea → 打包 3.6s → 加密 1.7s → 上传 19.2s(31.5MB/s)
      → 读回 sha256 一致,快照 = 448b898e

三、bootstrap.sh:修掉另一个静默陷阱

脚本每次都会**重写** .env,而 .env 是 GITEA_PASS 的唯一落点 →
「重跑一次但没现给 GITEA_PASS」会把辅仓推送**静默关掉**(PUSH_REMOTES 降级成
origin),不报错、不提示,直到要恢复时才发现 Gitea 早就没在同步。
  · 改为:命令行没给就从旧 .env 里捡回来(显式传新值仍优先)
  · 并把 GITEA_URL/USER/PASS 也写进 .env(容器不读这几个键,仅作复用锚点)
  · 该逻辑用 4 个用例离线验证(有/无 .env、显式覆盖、.env 里没 PASS),
    并确认 set -euo pipefail 下不会被 [ -n ] && cmd 这类写法误触发退出

四、文档

· 架构总览.md:§1.2 地址地图加 editor-api 行;§2 旅程图补「两条推送入口同一套语义」;
  §5.2 加「线上容器的推送目标」段(含验证方法与 pull 源只有 origin 的铁律);
  §5.6 加「打包前必须 git fetch」+「覆盖范围」表(诚实列出够不到的部分);
  §6 待办 #4/#10 更新、新增 #12
· editor-api/README.md:新增「落盘:写一次,存三处」
· README.md:推送说明补线上后台那一路与 fetch 说明;脚本表更新
2026-10-06 22:27:36 +08:00
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
zqlit 5f27ad321d chore(备份): 异地备份定案为 OpenList 离线 bundle,撤销全部 git 辅仓
用户定案:「辅助仓就用 openlist,其他不再考虑」。
据此把前一轮为 Gitee 铺的路全部收回,git 远端只剩 CNB 一个。

一、git 配置收口
- `pushall` 别名 `origin + gitee` → `!git push origin main`(只推唯一远端)
- 移除 `gitea` remote(自建 23.254.236.47:3001)—— 远端仓库本身没删,
  需要时可 `git remote add` 恢复;改动前配置存
  `.workbuddy-backup/git-remotes.20261006-211516.txt`
- 确认无 gitee 相关 credential 残留

二、发布链路去 Gitee 化(6 处)
- `deploy/editor-api/bootstrap.sh`
    · 删掉 GITEE_URL / GITEE_SSH_KEY 两个变量与「没给私钥就降级」的分支
    · PUSH_REMOTES 默认 → origin
    · 原「配 gitee 辅仓远端」一节改为「清理退役远端」循环(gh/gitee/gitea),
      让从旧部署续用的工作区自动恢复干净
- `docker-compose.editor.yml`、`editor-api/server.mjs` → 默认值 origin
- `editor-api/README.md` → 变量表同步
- `editor-api/Dockerfile` → 注释里的「CNB / GitHub」改「CNB」
- `blog-admin/src/routes/rss/tools.ts` → deploy-notify 的注释里
  「与 Gitea Actions 的构建通知配套」改为中性描述
  (该轮询链路 2026-10-04 起已被 CNB 国内节点直传取代)

三、`scripts/setup-cnb-remotes.sh` 重写为单远端模式
- 去掉 GITEE_URL / GITEE_TOKEN 参数、校验、凭据写入与自检提示
- 新增「清理退役远端」步骤
- 凭据处理改为「已存在空的 credential.helper 就不再添加」,
  不再用 --replace-all —— 本仓另有一个从 `$HOME/.workbuddy/secrets/cnb-token`
  读令牌的自定义 helper,那是有效的,不能被脚本抹掉

四、备份升级为「唯一辅仓」的配置
- 保留份数 3 → 7(一周窗口;每份 605.5 MB ≈ 4.2 GB,F50 有 256 GB)
    · `scripts/backup-task.cmd` 默认参数 --keep 7
    · `scripts/backup-bundle.mjs` 的 KEEP 默认值同步为 7
      (原先写的是 2,一直被命令行参数掩盖着)
- 远端目录 `/本地/备份` → `/本地/备份/blog-bundle`:
  根目录是用户自己在用的(放着 github-zqlit-*、local-repos-* 等手工备份),
  实测发现直接放根下的 bundle 已被清掉 —— 改子目录隔离,避免混放与误删

五、新增 `scripts/backup-run.mjs`:备份的推荐入口 + 失败告警
- 读 `.workbuddy-backup/openlist-backup.env`(只补空缺,环境变量优先)
- 跑 backup-bundle.mjs 并实时透传输出,同时留一份日志尾部
- 退出码非 0 → 经 `scripts/send_mail.js` 发告警邮件(附日志尾部与常见原因);
  成功默认不发,`--notify-success` 才发
- 退出用 `process.exitCode` 而非 `process.exit()`,避免截断未排干的 stdout
- 发信失败不改判备份退出码 —— 通知不该掩盖真正的故障
- 理由:这是当前**唯一**的异地备份,而「每天自动跑」的任务最典型的失败模式
  恰恰是静默的(F50 被带出门、换了网段、OpenList 没起来、口令改过……),
  没有告警就要等到真要用备份那天才发现
- `scripts/backup-task.cmd` 改调它

六、文档
- `架构总览.md`
    · §1.2 地址地图:备份行改指 F50/OpenList;通知行补「兼做备份失败告警」
    · §2 旅程图:双推改单推,并说明备份换了介质
    · §5.1 / §5.2 推送与远端:只剩 origin;补「已移除远端」表与恢复命令;
      GitHub 退役记录保留并补上「CI 定义也已删除」
    · §5.3 由「辅仓选型」改为「异地备份的定案」—— 明确不走 git 远端;
      平台对比数据保留备查,并注明 `bin/linux/hugo` 出库不必再做了
    · §5.6 补「唯一备份」定位、专属子目录、失败告警、keep 7、SMTP 配置键,
      实测数据更新为本次复测值
    · §6 待办:#2 定案、#3 不必做、#4 已移除、#8 已更新、#10 定位升级,
      新增 #11(F50 目录使用约定)
- `CNB构建落地方案.md` §4.0:双远端改单远端,脚本示例去掉 Gitee 参数
- `README.md`:推送说明改单推;脚本表补 backup-run.mjs

实测(2026-10-06,本轮复测):
  bundle 7.4s / AES-256-GCM 加密 1.3s / 上传 19.2s(31.6 MB/s)
  / 读回 sha256 一致 → 端到端 60.6s,退出码 0
  告警邮件链路已实测(发出一封「备份成功」验证信)

★ 一处过程记录,供以后避免重复踩坑:
  中途我把「本机沙箱里 `env -u ... cmd > file` 会让输出整个消失」
  误判成 process.exit 截断 stdout,并据此改了日志实现;
  随后用 `env -u FOO echo hi > file`(同样零输出)证伪 ——
  那是沙箱文件重定向的伪影,与脚本无关。相关改动已回滚,
  只留下本身无害的 process.exitCode 写法。
2026-10-06 21:27:28 +08:00
zqlit af1bbf9c1e chore(远端): 彻底剔除 GitHub —— 删除已退役的 GitHub Actions 定义
用户明确「GitHub 现在整改,不能再作为备份库」,据此把 GitHub 从本项目
所有环节清干净。上一轮(b39dc297)已完成 remote / 推送别名 / 发布链路的
去 GitHub 化,本轮清掉最后一块实体:CI 定义。

- 删除整个 `.github/` 目录(3 个文件、共 1268 行):
    deploy.yml          1169 行  原主构建发布流水线(已由 .cnb.yml 接管)
    aliyun-backup.yml     54 行  备份到阿里云 OSS
    cleanup.yml           45 行  清理 artifacts / cache
  已确认无任何脚本、配置或流水线引用它们(全仓只有 .cnb.yml 的注释提过 deploy.yml)。
  实现完整保留在 git 历史里,需要时可翻。

同步更新引用它们的注释与文档(否则会指向不存在的文件):
- `.cnb.yml` 头部:说明原流水线已删除、要查旧实现请翻 git 历史;
  去掉「见 deploy.yml 第 290 行」这类失效指向
- `CNB构建落地方案.md`:
    §4.0 标题「CNB 主仓 + GitHub 备份」→「+ 辅仓备份」
    §pushurl 缺陷论证里「GitHub 那份备份」→「辅仓那份备份」
    §敏感文件:「已核实 github.com 返回 Page not found」→ 更新为
      账号被标记 + 仓库被 AUP 清空,该威胁面已消失
    §历史清理的判断已变:当初不做的唯一顾虑(备份会分叉)已随 GitHub 消失,
      filter-repo 现在是可行窗口(代价:SHA 全变、自建 Gitea 需重推)
- `架构总览.md`:§5.1 标注当前实际只用 `git push origin main`(gitee remote 未建);
  §6 #1 改写为「GitHub 已彻底退出备份体系」、#5 由「可删」改为「已删除」;
  末尾「已退役参考」改为「已删除,可翻 git 历史」
- `README.md`:脚本表补上 `backup-bundle.mjs` / `backup-task.cmd`;
  `setup-cnb-remotes.sh` 的说明去掉 GitHub
2026-10-06 21:11:48 +08:00
zqlit 8abe93d17a feat(backup): 整仓离线备份上线 —— bundle 加密后传中兴 F50 上的 OpenList
背景:GitHub 被按 AUP 清空后,用户提出自己的中兴 F50(5G CPE + 内置 256GB)
上跑着 OpenList,想用它当第三层备份。实测可行,已落地并跑通。

为什么是 bundle 而不是直接推 git:
  WebDAV 不支持原子的 rename/lock,bare repo 挂上去 push 会让对象写坏 ——
  表面成功、实际随机损坏,可能几个月后才发现。bundle 是单文件顺序写,安全。

为什么加密(用户一度想省掉):
  历史里含 .env、TLS 私钥、GITEA_SECRETS.md。介质是随身设备的内部存储,
  明文 = 把密钥放在一台可能丢失/刷机/送修的机器上。
  OpenList 的登录只保护「访问通道」,不保护「存储介质」——拆机就能读。
  加密成本实测仅 1.9~2.9 秒、体积不变;且 CNB/Gitea 仍是明文副本,
  口令丢失只是少一份备份,不构成单点。

实现(scripts/backup-bundle.mjs,零 npm 依赖):
  - git bundle create --all → AES-256-GCM(Node 内置 crypto)
  - 布局 magic(8)|salt(16)|iv(12)|密文|tag(16),scrypt(N=32768,r=8,p=1) 派生密钥
  - 为什么不用 gpg:本机 gpg 2.4.9 在 Windows 下已损坏(反复 stale lockfile,
    node spawn 直接 EBUSY);换内置 crypto 后零外部依赖且带认证标签
  - 上传后可选 --verify:下载回来比对 sha256,端到端闭环
  - --keep 控制远端保留份数;--decrypt 恢复;--list 盘点;--dry 不上传

定时任务(scripts/backup-task.cmd + Windows 计划任务 Blog-BundleBackup):
  - 每天 03:30 本地时间,默认 --verify --keep 3
  - InteractiveToken + LeastPrivilege、StartWhenAvailable、1h 超时、IgnoreNew 防重入
  - 日志追加到 .workbuddy-backup/logs/backup.log,超 5MB 轮转

★ backup-task.cmd 内容必须全 ASCII:
  cmd.exe 按当前代码页(zh-CN 是 GBK)解析批处理文件,而 node 输出 UTF-8。
  UTF-8 中文注释会吞掉 CR/LF 并把下一行当命令执行 —— 实测踩到(一条 rem 被当命令跑)。
  ASCII 是 UTF-8 子集,纯英文注释与 node 的中文输出混写不会乱。
  同理不能用 %date%(含本地化星期),改用系统时间 API 取 ISO 格式时间。
  日志轮转的 for 语句必须加 if exist 守卫,否则首次运行报「系统找不到指定的路径」。

实测(由计划任务实际拉起,非手工执行):
  bundle 6.8~9.9s(605.5MB)/ 加密 1.9~2.9s / 上传 19.4~20.6s(29.4~31.3 MB/s)
  / 下载回读 sha256 一致,端到端退出码 0
  恢复链路已演练:--decrypt → git bundle verify 报 "records a complete history"
  → 1008 提交完整一致

文档(架构总览.md):
  - §5.3 从「Gitee 两条硬约束」扩写为「辅仓选型」,补入云效 Codeup 基础版对照
    (Git 5GiB + 单文件命令行 200MB)—— 选它则 bin/linux/hugo 不必出库、
    构建链路一行不用改
  - 新增 §5.6 整仓离线备份(介质 / 为什么 bundle / 为什么加密 / 加密格式 / 用法 /
    配置 / 定时任务 / 恢复流程 / 实测数据)
  - §6 待办:#2 改为「辅仓选型未定」并说明 pushall 现状;#3 标注只有选 Gitee 才必须做;
    #9 补记「不改写历史」的唯一障碍已随 GitHub 消失;新增 #10 备份已上线 + 三项安全待办
2026-10-06 21:03:29 +08:00
zqlit b39dc29753 chore(远端): 移除 GitHub,改为「CNB 主仓 + Gitee 辅仓」
起因:GitHub 账号被平台标记,随后仓库被按 AUP 合规条款清空 ——
远端只剩一条孤立提交(无父提交、无内容):

    c21d5669  2026-10-06 20:08:32 +0800  zqlit
    chore: remove repository content (AUP compliance)

同时远端 main 的历史与本地**完全分叉**:两边只共享 2024-07-11 的 Initial commit,
之后每个提交 SHA 都不同(远端那份 989 提交、剥离了 .env/私钥/大二进制)。
所以 `git push gh main` 无法快进,也不可能再当备份用。

改动:

- `git remote remove gh`(改动前配置存 .workbuddy-backup/git-remotes.20261006-201354.txt)
- `pushall` 别名 → `git push origin main; git push gitee main`
- `scripts/setup-cnb-remotes.sh`:参数化第二个远端为 Gitee,
  顺带把「若残留 gh remote 就清掉」做进脚本;Gitee 凭据走 wincred
- `架构总览.md`:§2 发布旅程图、§5.2 远端表、§6 遗留待办全部改指向;
  新增 §5.2 的 GitHub 退役记录(含那条孤立提交的原文)
  与 §5.3「Gitee 辅仓的两条硬约束」实测核算
- `CNB构建落地方案.md`、`README.md`:远端与 pushall 说明同步
- **发布链路一起去 GitHub 化**(这几处原先都写死了 gh):
    · `deploy/editor-api/bootstrap.sh`  GH_URL/GH_SSH_KEY → GITEE_URL/GITEE_SSH_KEY,
      remote `gh` → `gitee`,known_hosts/私钥文件名跟着改;
      保留原设计:**没给私钥就自动降级成只推 origin**,不会让每次发布都报错
    · `docker-compose.editor.yml`   PUSH_REMOTES → origin,gitee
    · `editor-api/server.mjs`       默认值 → 'origin,gitee'
    · `editor-api/README.md`        变量表同步

★ 尚未解决 / 需要决策的两条 Gitee 硬约束(详见 架构总览.md §5.3):

1. **单文件 ≤ 50MB,而 `bin/linux/hugo` 是 83.1MB**
   —— 不管怎么瘦身历史,只要它还跟踪在 HEAD 里,Gitee 一律拒收。
2. 单仓库 ≤ 500MB,而 `.git` 是 620MB
   —— 移出那个 83MB 后 HEAD ≈ 374MB,才有余量。

另:线上 editor-api 容器的 .env 仍是 `PUSH_REMOTES=origin,gh`,
且仓库里还有一个 `gh` remote。因为推送逻辑对辅仓失败只警告、不阻断发布
(`git.mjs`:主仓成即算成),所以**线上发布没有坏**;
但要真正切到 Gitee,需要等 Gitee 仓库与私钥就位后再部署一次。
2026-10-06 20:18:29 +08:00
zqlit 740d77e4cb fix(ssl): 修掉 5 处静默失败,本项目全面接管签发部署,Worker 退回只读
一、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),
保留三条纯监控告警。
2026-10-06 20:08:28 +08:00
zqlit 1427c47dcf feat(ssl): 证书签发链路搬到国内机 Docker,清理 1Panel 过期证书
架构定案(B+C):CF Worker 免费版 CPU 硬顶 10ms(cron 同),
不再购买 Paid($5/月≈¥36),改为——
  B. Worker 留免费版 + 代码优化(把 CPU 压进预算)
  C. 签发+部署整条链路搬国内机 Docker 容器

代码
- acme.ts: 缓存 signingKey 为 Promise,单次签发 importKey 12→1 次
  (实测 importKey 126µs / sign 84µs;一次签发 3.3ms → 1.4ms)
- deployer.ts: 新增 DogeCloudDeployer.ping();修正 cert_id → id 的注释
- dnsprovider.ts: 新增 RemoteDns(把 DNS-01 写 TXT 委托给国内机 cn-dns-helper)
- tools/certkeeper-config.mjs: 域名配置抽为唯一事实源(两个消费方共用)
- tools/export-certkeeper-data.mjs: 导出国内机数据目录

新增部署单元
- deploy/cn-certkeeper: 签发+部署容器(只绑 127.0.0.1:8019,compose 管理)
  含 FileKV(文件系统版 KVNamespace)、带鉴权 HTTP、每日 4:10 续期
- deploy/cn-dns-helper: DNS-01 写 TXT 助手(只绑 127.0.0.1:8018)

文档
- 函数版证书管家-方案.md 新增第九章:B+C 定案、实测 CPU 数据、
  容器验收记录、1Panel 过期证书清理记录、t-t.live 两套管理冲突
- 标注旧 8.3 节「免费版跑不了签发」为未实测误判

一并纳入:.gitignore 忽略 deploy/cn-certkeeper/lib/(tsc 编译产物)
2026-10-06 18:28:55 +08:00
zqlit a3bc10c68c docs(ssl): 方案文档补充第八章 —— 签发/续期落地实现与上线记录
记录本轮实测结论:免费版 [limits] 会拒绝部署(code 100328)、
多吉云 bind 参数名 id、1Panel v2 与 SSL 大写字段、LiteSSL /v2 目录、
CSR extensionRequest 三层 SEQ 等易误判的坑;含上线 version_id 与
403-vs-404 路由存在性验证方法。
2026-10-06 17:07:29 +08:00
zqlit 8f2eee1a38 fix(ssl): 注释掉 [limits] cpu_ms —— 免费版会直接拒绝部署
2026-10-06 实测 wrangler deploy 报错:
  X [ERROR] A request to the Cloudflare API (.../versions) failed.
    CPU limits are not supported for the Free plan. [code: 100328]

注意是「整个部署失败」而非「忽略该字段」,所以 Free plan 下必须注释。
同时记下影响:免费版 CPU 硬顶 10ms,ACME 签发(ECDSA 签名 + 手写 DER CSR)
跑不起来 —— 签发/续期链路需要 Workers Paid;探针/自检/查询不受影响。

升级 Paid 后放开注释即可(cron 默认 30s,Paid 可到 5min)。
2026-10-06 17:06:51 +08:00
zqlit 43dbc8b75f feat(ssl): ACME 自动签发与自动续期,实现证书全生命周期闭环
证书管家此前只做「探针」(查剩余天数),现补齐签发+部署两个环节,
参照 certimate(MIT)的 DNS-01 流程自行实现,不再依赖闭源 certd。

新增(纯 WebCrypto,零 npm 依赖):
- lib/acme.ts        ACME v2 客户端:ES256 JWS(原始 r||s)、RFC7638
                     thumbprint、EAB、badNonce 重试、DNS-01、手写 DER CSR
- lib/dnsprovider.ts DNS-01 适配:DNSPod(TC3-HMAC-SHA256)、Cloudflare
- lib/deployer.ts    部署适配:多吉云 CDN、1Panel 站点(幂等换证书)
- lib/certissue.ts   编排:探针判剩余天数 → 注册/复用账户 → 签发 → 落库
                     → 逐目标部署;RENEW_BEFORE_DAYS=30
- routes/ssl.ts      新增 POST /ssl/issue、GET /ssl/renew-check、
                     POST /ssl/selfcheck(环境自检,只读不签发)
- index.ts + cron    每日 04:10 自动续期检查;cpu_ms 提到 60s

与 certimate 的差异:certimate 每个 workflow 每天无条件重跑,
这里改为先探针查剩余天数、低于阈值才签,省 CA 限速额度。

实测修正(易误判,勿回退):
- 多吉云 bind 参数是 {id, domain},非 {cert_id}(用假 id 对照实验确认:
  cert_id 回「域名不存在」= 参数被无视)
- 多吉云上传私钥字段是 private;列域名用 /cdn/domain/list.json
- 1Panel 必须用 /api/v2/(v1 返回 HTTP 200 但正文是 HTML 停用页)
- 1Panel HTTPS 配置字段是 SSL(大写),写错会导致每次续期都重绑
- LiteSSL ACME 目录须带 /v2:acme.trustasia.com/acme/v2/directory

测试:selftest-acme 16/16(CSR 过 openssl 验签、JWS 过 Node crypto 验签)、
selftest-deploy 18/18、selftest:ssl 117/117、UI 全过、tsc 干净
2026-10-06 16:59:37 +08:00
zqlit 432cf5e398 feat: 证书探测改走国内机真 Node —— Workers 拿不到证书正文的兜底方案
根因:Workers 上 cloudflare:sockets 没有 getPeerCertificate,
node:tls 的同名方法是桩函数(调用即抛 not implemented)。

- editor-api 新增 /ssl-probe 本地端点(真 Node,读对端证书正文):
  不外发、不落盘;ssl 白名单放行,admin/ssl 可用,editor/匿名拒绝
- certprobe 改两级:首选国内机(带 X-Editor-Token),失败自动降级本地握手
- certProbe/certCheck 接线 EDITOR_API_BASE + EDITOR_TOKEN,撤掉 ?debug 诊断
- wrangler.toml 显式开 nodejs_compat(compat date 早于默认启用阈值)
- 手写 node:tls 最小类型声明(保持零依赖)
- seed-ssl-config.mjs 净化:真实凭据移到 secrets-backup/certkeeper-seeds.json,
  TOKEN_SECRET 改从 .dev.vars 读;脚本本体不含任何凭据
- role-perm 新增第 9 节 12 项(59/59),UI 探测 37 项全过
2026-10-06 15:55:22 +08:00
zqlit a40a92f526 feat: SSL 证书管家 —— 后台面板 + 专属角色 + 国内机放行
集成在 api.200181.xyz(同一个 Worker),国内机 writeapi.usj.cc 同步可用。

新增第三档后台角色 'ssl':
- 只拿证书管家钥匙,看不到评论/文章/用户等模块
- 判定正着枚举放行(canManageSSL = admin|ssl),不用排除法,
  免得以后新增角色静默获得私钥权限
- 鉴权只认 Bearer 会话,绝不走 isAdminRequest —— 后者有 Artalk
  老客户端的 query 兜底,一旦进来的就是 TLS 私钥

数据(复用 RSS_KV,前缀 certkeeper:):
- 凭据一条一键,避开 KV 读-改-写无事务导致的并发丢数据
- 私钥/AK-SK 一律 AES-GCM 密文(TOKEN_SECRET 经 PBKDF2 派生)
- 列表接口只回显前 4 后 4 位,明文不进内存
- 配置读失败抛错而非返回空,避免一次保存覆盖线上配置

只读监控:
- probeTls 走 cloudflare:sockets 拿证书正文,实现到期分级
  与「库里记录 vs 线上实测」对比(match/mismatch/live-only/unreachable)
- 到期提醒邮件(HTML 已转义)

国内机 editor-api:
- identify 识别 ssl 角色;业务分支前白名单,ssl 只能碰 /health、
  /admin/session、/admin/logout 与 /api/v2/ssl*
- /api/v2/ssl* 反代放行 admin + ssl(RSS 仍是 admin-only)
- posts.mjs 越权兜底方向修正:owns() 从「非 editor 即放行」改为
  「只有 admin 不受限」,漏进来的 ssl 被当受限编辑而非管理员全放行
- createPost 显式拒绝非写作角色

验证:typecheck ✓ / selftest:ssl 117 项 ✓ / 浏览器 37 项 ✓
2026-10-06 14:19:01 +08:00
zqlit ca2fb7163d perf: 开启 Workers Cache 缓存 favicon,并给动态接口兜底 no-store
背景:favicon 此前只有 Cache-Control: max-age(仅浏览器缓存),
CF 边缘不缓存 Worker 响应 → 每次请求都进 Worker → 第一句就是 KV.get。
友圈页按友链数放大,KV 读量被显著放大。

★ 关键纠错:zone 级 Cache Rules 对 Worker 响应**完全无效**。
原因是 Worker 位于 zone 缓存之前("Workers sits in front of cache"),
且 Worker 直接生成响应时不发 fetch 子请求,cache status 只能是 none/unknown。
查证 CF 官方文档后确认,正确方案是 [cache] enabled = true(Workers Cache,
2026-07 上线,需 wrangler >= 4.69)。

改动:
- wrangler.toml: 新增 [cache] enabled = true;compatibility_date 提到 2026-07-24
- package.json: wrangler ^3.99 → ^4.147,workers-types ^4 → ^5(peer 要求)
- index.ts: 抽出 dispatchRequest;新增 ensureCachePolicy 兜底
  ★ 这一步是必须的:Workers Cache 遵循 RFC 9111 含**启发式缓存**,
    没带 Cache-Control 的响应也可能被缓存。实测 /api/v2/comments 开启后
    立刻被缓存(HIT, age:29)→ 用户发的新评论看不见。
    现在白名单式兜底:已显式声明可缓存的(favicon 的 s-maxage)保留,
    其余一律补 no-store。

验证(中转机实测,已清空 zone 的无效 cache rule):
- /api/favicon      → MISS → HIT,age 递增(Worker 不再执行,KV 读归零)
- /api/v2/comments  → BYPASS ×3,cache-control: no-store
- /api/v2/conf      → BYPASS,no-store
- /api/v2/healthz   → BYPASS,no-store
2026-10-06 13:28:56 +08:00
zqlit d2f1d176af docs: 修正证书实况表(实测校准)
- t-t.live 组:文档记的 Oct 9 是旧快照,实测线上已续期为
  Let's Encrypt Sep 28 → Dec 27(三个子域共用)
- usj.cc 组:确认 Dec 7 2026(LiteSSL / TrustAsia)
- 补充签发方列与探测方法说明
2026-10-06 13:05:55 +08:00
zqlit 42f28f0a39 content: 恢复《流量卡还真得选御三家😂》文章
该文被 4215e081 删除,但线上评论库(24 条)与 pages 记录均未动,
仅缺文章文件。补回文章后 slug=20260724101237 与 page_key 精确对应,
评论自动挂上,无需任何数据迁移。

从 4215e081^ 恢复整个目录(index.md + 2 张配图)。
2026-10-06 13:03:43 +08:00
zqlit c2f890a0f7 feat: favicon 边缘缓存 + CDN 整站刷新兜底 + 评论孤儿修复归档
- blog-admin tools.ts: favicon 响应加 s-maxage/CDN-Cache-Control,
  让 CF 边缘可缓存,边缘命中即不进 Worker,KV 读取量大幅下降
- scripts/refresh_cdn.js: 裸域名自动补尾斜杠(多吉云 rtype=path 是目录刷新,
  https://usj.cc 只刷首页,https://usj.cc/ 才是全站),并打印刷新清单
- 新增文档:评论孤儿修复方案、函数版证书管家方案、诊断报告
2026-10-06 12:46:05 +08:00
zqlit d273330e10 merge: 合入编辑器发布的两笔提交(2026-10-05 22:00~22:06 测试文章增删) 2026-10-05 22:54:09 +08:00
zqlit cfadba6007 fix(后台文案): 删除确认框措辞纠正
旧文案「…不进 git,也不影响版本历史,但前台会立刻少一篇」有两处不准:
- 「前台会立刻少一篇」不成立 —— DELETE 只把目录 move 到服务器回收目录
  (/srv/editor-trash),不 commit 不 push,线上要等下次「发布」才少一篇;
- 「不影响版本历史」也不严谨 —— 下次发布时这次删除会作为 D 一起进历史。

新文案:「文件会移到服务器上的回收目录(在仓库外,不进 git):
后台立刻少一篇,线上要等你下次点「发布」后才会少。确定删除?」
2026-10-05 22:53:30 +08:00
zqlit 5e9d328841 fix(排序): 同日文章 date 补全时分秒 + EdgeOne 部署重试增强
现象:小乖同一天发的第二篇排到了第一篇后面。

根因:front matter 的 date 只写到年月日(写入侧只收得到日期),
同一天多篇的时间戳完全并列 → 排序退化成比标题,前台/后台都错。

改动:
- 数据(6 篇):用目录名/slug 里的时间戳把 date 补全为
  'YYYY-MM-DDTHH:MM:SS+08:00',slug 即写作时刻。每篇仅 1 行 diff。
  2026-10-05 两篇(必须)、2026-06-02 / 06-03 四篇(同一 bug 的历史遗留)。
- 写入侧根治
  - editor-api/src/posts.mjs:新建时「只到天」补北京时间此刻;
    保存时「同日保真」——没换日子则原有时分秒一字符不动。
  - write-server/src/lib/posts.ts:parseDate 按 Asia/Shanghai 归日,
    列表同天排序不再受 UTC 时区漂移影响。
- 回归测试:新增 editor-api/test/date-time.mjs(8 项,自带一次性临时仓库)。
- .cnb.yml:EdgeOne stage 重试 3 次/20s → 5 次/30s。
  构建节点到 api.edgeone.ai 偶发 15s 超时会被 CLI 误报
  「Invalid EDGEONE_PAGES_API_TOKEN」,纯网络抖动,令牌没坏。
- .gitignore:忽略 *.zip 与 .edgeone/(误留的 blog.zip 达 361MB)。
2026-10-05 21:54:03 +08:00
zqlit 6b8fba827b fix(评论): 徽章排序兜底 —— 系统徽章(博主/小编)恒定排在最前
现象:普通用户的 badge_name 徽章(如「小编」)会排在
友链/友圈/官职之后,与「博主」的位置不一致(管理员路径下系统徽章
在 .atk-badge-wrap 里、会被提前,普通用户路径则可能被追加到末尾)。

修法:拆掉 .atk-badge-wrap 后做一次确定性重排,按
系统徽章 > 置顶 > 头衔 > 友链 > 友圈 > 官职 的优先级依序 append 回
昵称(DocumentFragment 移动保序)。不管 Artalk 把系统徽章渲染在哪
都会归位,以后新徽章只要补进 PRIO 数组即可。

本地验证:CDP 注入 fetch 拦截给小十的评论加 badge_name=小编,
完整复现截图场景 → 顺序 小编 友链 友圈 司户参军 ✅
2026-10-05 18:21:00 +08:00
zqlit c1f9a0c6f0 fix(ci): 全量刷新判定改为「非纯内容改动即全量」+ 刷新分批(又拍云 50/次)
上一版用「themes/layouts/assets 等 dirs 白名单」判全局,实测被绕过:
build 2 的 diff 只有 scripts/purge_list.js(主题改动在上一个 commit),
判成非全局 → 只刷了 10 条固定入口页 → 文章页仍引用已被 --delete 删掉的
旧 page-only.min.1e47477b…(靠 CDN 缓存才 200,缓存一过期即 404)。

改法:
- 判定反转:diff 只含 content/** 才算「纯内容改动」(走精细刷新),
  其它一律全量刷 public 下所有 .html。
- 拿不到基线时:push 事件按全量(宁多刷不漏刷),定时任务只刷固定入口页。
- 输出一行 stderr 诊断(base/变更数/纯内容/全量/清单条数)便于事后核对。
- .cnb.yml:清单按 50 条/批 + 批间 6s 调用 upx purge(又拍云限制
  单次 ≤50、每分钟 ≤600),并逐批打日志;不再一次性提交 1300+ 条。
2026-10-05 17:57:51 +08:00
zqlit 5a50df6e23 fix(ci): 主题/layouts/assets 等全局改动时,purge 清单追加全站 HTML
问题:模板用 resources.Fingerprint 生成带 hash 的 JS/CSS 文件名,而 upx sync
带 --delete(远端不存在的文件会被删)。只改主题、不动 content/*.md 时,
purge_list 只输出固定入口页 + content 页 → 文章页的 HTML 仍留在 CDN(TTL 8 天),
里面引用的是**已被删掉的旧指纹 JS** → 评论区/交互断链(实测文章页仍引用
page-only.min.1e47477b…,该文件本轮已不在源站,只是还在 CDN 缓存里)。

修法:diff 命中 themes/ layouts/ assets/ static/ data/ archetypes/ 或
hugo.toml/config.* 时,walk public/ 把所有 .html 加入刷新清单
(本机验证 1336/1336 覆盖)。
2026-10-05 17:39:59 +08:00
zqlit 79c9f2371c feat(评论): 最近表情改为分组 tab + 提交后面板收起 + 小编徽章配色 + 走心评论 20 条
- emoticon-recent.js: 「最近使用」从面板顶部独立栏重构为分组切换条最左侧的
  同构 tab(data-index="recent"),无记录时整个 tab 隐藏;复用 Artalk 原生
  .atk-grp[data-type=image] 网格样式,零自定义布局。
- artalk.js:
  · 新建评论成功(res.ok)后,对当前激活的插件按钮派发 click(),收起仍展开的
    表情面板(Artalk 上游只 reset 输入内容、不关面板),内部 openedPlug 状态同步复位。
  · 徽章处理末尾把徽章文字镜像到 data-badge 属性,供 CSS 按名字配色。
- artalk.html: 新增 .atk-badge[data-badge="小编"] 玫红实底规则(含暗色模式),
  特异性高于博主规则,两者互不覆盖。
- featured-comments.html: 走心评论 limit 8 → 20(后端上限)。
- main.css: 移除旧 .atk-recent-emoticons 相关样式。
2026-10-05 17:34:18 +08:00
zqlit 1b2e3d46f8 fix(友链): 修复申请「通过不了/删不掉」;feat(评论): 通知按文章作者分派
## 友链自助申请(通过不了 / 删除不掉)
根因:admin.js 全局事件委托把 data-id 一律 parseInt,
而友链申请 id 是字符串(lamus5uptzcdam)→ NaN → JSON 成 {"id":null}
→ 后端 String(null||'') 为空 → 400 id and action required,查看/通过/拒绝全失效。
引入于 a74b3c71(10-04 归档 rss-robot 时;后台只有友链申请用字符串 id)。

- admin.js:纯数字才转数字,其余原样(评论/用户/站点按钮请求完全不变)
- admin.js:待审卡片加「删除」按钮
- links.ts:新增 action:'delete'(不受「已处理过」限制,处理过的也能删)
- links.ts:approve 改为先写友链再改状态 + URL 去重(重试幂等)
- links.ts:已是目标状态时返回 {ok:true,already:true},不再 409
  —— 否则网络抖动后重试会让用户以为「怎么点都通不过」

## 评论通知按文章作者分派
编辑角色(女朋友)写的文章,评论通知改发给她,不再打扰博主。
不需要「文章→作者」同步表:Hugo 模板把 author_id 注入 artalkConfig,
artalk.js 已有的 fetch 拦截器把它塞进评论提交体,Worker 直接读。

- artalk.html:artalkConfig 增 pageAuthorId(老文章 0)
- artalk.js:提交评论时带上 author_id(读 window.artalkConfig,防 PJAX 过期)
- comments.ts:createComment 解析 author_id 并传给通知
- mail.ts:收件人改为「这篇文章的主人」——作者非管理员且开了邮件则发给他,
  否则回退博主;已被回复通知过 / 作者本人评论自己的文章都不重复发
- mail.ts:增一行收件人决策日志,便于线上排查

验证:Worker tsc 通过;Hugo 构建通过;artalk.js 语法通过;
友链本地跑通(通过 200 → 再通过 already:true → 友链仅 1 条 → 删除 200 → 再删 404)。
2026-10-05 16:44:15 +08:00
zqlit 68416366eb fix(editor-api): 身份判定改为会话优先,防止配置漂移静默提权
identify() 原来「令牌优先」:令牌对得上就用 X-Editor-* 头的身份,没有头时默认 admin。
国内线路的 nginx 目前不注入令牌,但一旦配置回滚到「注入令牌」的旧版本,编辑的请求
会命中令牌分支 → 静默获得管理员权限(看到全部文章、能改别人的、能全量发布)。

改为「会话(真实的人)优先于共享令牌(服务身份)」,把边界写死在代码里而不是靠配置。
新增断言:编辑会话 + 管理员令牌头 → 仍按编辑身份。role-perm.mjs 47/47
2026-10-05 14:41:02 +08:00
zqlit d899cd793b feat(编辑角色): editor 只能写并发布自己的文章
- D1 users 加 role 列(''/editor/admin),与 is_admin 成对写入
- Worker routes/editor.ts 放行 admin+editor,反代注入 X-Editor-Uid/User/Role(昵称 encodeURIComponent)
- editor-api 引入 identify():身份取自注入头或本机会话;文章归属记 frontmatter author_id
- 编辑发布改精确 pathspec(只提交自己文章目录),管理员保持全量;空 pathspec 显式拦截
- 国内机直连登录改为转发 CF /user/access_token 校验,CF 不可达回退本机管理员
- handoff 签名覆盖身份(ts/uid/name/role),防编辑一键跳转变管理员
- admin.js:编辑只渲染「文章编辑」tab、作者框只读;用户管理加角色下拉 + 新建用户
2026-10-05 14:34:56 +08:00
zqlit 66c27c7b62 fix(ci): CDN 刷新清单改为「固定入口页 + 本次改动过的内容页」
又拍云对 HTML 的 max-age 是 8 天,而流水线只刷固定白名单(/ sitemap rss
archives posts comment links)。不在清单里的页面改动后,源站已更新、CDN 仍发
旧副本——本次 about.html 就卡住了(源站 47935B 含新板块,CDN 仍是 41454B)。

- 新增 scripts/purge_list.js:读 git diff 找出改动的 content/*.md,按
  slug/url/目录名/文件名推输出页,并校验 public/ 下确实存在才加入
- .cnb.yml 刷新步骤改用它,脚本失败时兜底只刷首页
- git diff 必须带 -z / core.quotepath=false,否则非 ASCII 路径会被转义引号包裹,
  校验必然失败(本次踩过)
2026-10-05 12:19:28 +08:00
zqlit 1b71fba413 feat: 走心评论(精选评论)+ 移动端评论框底栏布局修复
- 底栏:≤480px 压缩按钮内边距,SE 等小屏单行放下;换行时按钮组右对齐
- Worker:comments 加 is_featured;PUT /comments/:id 支持切换;新增公开接口
  GET /api/v2/featured/comments(排除悄悄话/待审/已删,服务端渲染 markdown)
- 前端:评论区注入「精选」按钮(回复右侧,仅管理员可见)
- 关于页:走心评论板块(标题寄语通栏 + 卡片瀑布流),排在热力图上方
2026-10-05 12:08:33 +08:00
zqlit 3d7d7bd396 fix(theme): pageTitle 加 safeJS —— 修 Go 模板 JS 上下文二次转义导致标题天生带引号
在 <script> 里 {{ .Title | jsonify }} 会被 Go 模板按 JS 上下文再转义一层:
标题「留言」渲染成 pageTitle: "\"留言\"",前端每次上报 page_title 都自带
一对引号 —— 评论入库、邮件标题因此出现《"留言"》,历史上 pages 表一堆标题带
引号(17 行)也是这个根因。jsonify 后用 safeJS 标明这就是最终 JS 字面量,
`</` 仍按惯例转义兜底;已验证中文/emoji 标题字面量均合法
2026-10-05 11:01:20 +08:00
zqlit f7e0596218 fix(comments): 悄悄话允许管理员发表 + 邮件标题优先用实时标题;返回顶部改 UIkit 同款动画
- comments.ts: 去掉旧匿名语义遗留的 !admin 前置条件 —— 博主(登录态)发的悄悄话
  以前被忽略标志、入库 is_anonymous=0 变成公开评论
- 邮件标题改用本次请求带来的 page_title(前端 .Title 总是最新),D1 pages.title
  仅兜底:存量脏数据 /comment.html 曾存成字面量 "留言",邮件渲染出《"留言"》
  (线上已清洗 17 行带引号标题 + 3 篇被污染成「留言」的文章页标题)
- 返回顶部:CSS 挂类+scrollTo 同帧执行会被浏览器按旧计算样式瞬移("一下子就上去"),
  改 UIkit scrollIntoView 同款 rAF + 余弦 ease-in-out + 40*dist^0.375 时长(上限 1.2s),
  滚轮/触摸打断、reduced-motion 瞬移;main.css 移除 .css-smooth-scroll
2026-10-05 10:52:13 +08:00
zqlit 4fa592c11c feat(comments): 匿名评论升级为「悄悄话」——只藏内容不藏身份;fix: PJAX 瞬断自动重试
- 悄悄话:头像/昵称/头衔/等级/IP归属照常显示,仅内容对非管理员请求剥空
  (F12/网络面板拿不到原文),博主后台/登录态/邮件看全文
- 开关挪进底栏 .atk-bottom-left、表情/预览按钮右边,移动端不挤;
  回复悄悄话勾不勾随自己(去掉自动勾选)
- 前端识别:服务端剥空后 .atk-content 为空即悄悄话(空评论会被后端拒绝、
  纯表情评论有 img 不误判),无需解析 API
- 邮件页面标题书名号《》保留(按用户要求)
- PJAX 偶发「连不上服务器」:GET /api/v2 网络错误或 5xx 静默重试 2 次
  (700ms/1800ms 退避),POST 不重试,被 abort 的请求不续命
2026-10-05 10:05:21 +08:00
zqlit 215929a67f feat(comments): 匿名评论 + 表情最近使用;fix: 自动填充聚焦提示 & 邮件模板回退原版 & 返回顶部改 CSS 动画
- 匿名评论:comments.is_anonymous 列;对外昵称/头像/网址/徽标/等级/IP归属全部脱敏,
  真实身份留 users 表(回复通知照发、autofill 不受污染);邮件昵称也用「匿名」防泄露
- 表情包最近使用:面板顶部「最近」栏,localStorage 记录 8 个,零侵入复用 Artalk 插入逻辑
- 匿名开关插在 textarea 与底栏之间独立一行,不挤移动端底栏
- 自动填充:提示改 focusin 触发(原 input+sessionStorage 一次性门导致永不触发);
  已填昵称/网址时不覆盖并弹提示
- 邮件模板 UI 回退原版深色气泡设计,保留表情内联尺寸/主题文案等逻辑修复
- 返回顶部:rAF 自绘改 CSS scroll-behavior(滚动期临时挂类),修移动端掉帧
2026-10-05 09:02:23 +08:00
zqlit 090262d278 fix(autofill): 编辑器容器选择器改为 .atk-main-editor,线上 Artalk DOM 里没有 .atk-editor,委托匹配不到导致功能空转 2026-10-05 07:46:08 +08:00
zqlit bcfa229105 邮件模板微信化 + 评论老友自动填充 + CF 后台国内线路一键跳
- 邮件模板重做成微信聊天式:灰底、自己评论绿色气泡靠右、对方白色气泡靠左、
  40px 圆角方头像(weavatar,邮箱 md5 不外泄)、时间居中、#07C160 主按钮;
  布局全嵌套表格(Outlook 稳),表情内联 max-height:28px(修掉「表情太大」)、
  配图限宽 100%;管理员主题从「新的评论待审」改成「您有一个新的评论」
- 老友自动填充:新增 /comments/lookup(只回昵称+网址,绝不含邮箱;IP 限速
  30 次/10 分钟)+ 主题 comment-autofill 模块(填昵称弹提示、填邮箱自动带出
  上次的昵称/网址,事件委托不怕 Artalk 重建编辑器)
- CF 后台新增「国内线路」一键跳:/editor/handoff 用共享令牌对时间戳 HMAC
  签名 → editor-api /admin/handoff 验签发会话 Cookie(2 分钟有效、防重放),
  晚高峰丢包时直达国内机后台免重新登录
2026-10-05 07:36:26 +08:00
zqlit 18a4882577 登录页去掉订阅口令:订阅接口认后台会话 + editor-api 反代
- Worker 侧 requireAuth 新增两条通道:① Authorization 管理员会话(CF 版后台
  登录后订阅模块免口令);② X-Editor-Token 共享令牌(服务端到服务端)。
  原 ?token=/Cookie 通道保留,RSS 阅读器与友圈页不受影响
- editor-api 新增订阅反代(/api/feeds|links|link-apply):国内机后台的
  订阅源/友链由本机带共享令牌去 CF 取,前端不再持有任何口令;GET 幂等重试
  抗跨境抖动
- 登录页只留账号+密码(CF 版与国内机版统一);国内机后台侧栏新增订阅中心
  入口;RSS_TOKEN / LS_RSS 前端残留全部清理
2026-10-04 23:57:02 +08:00
zqlit a11631faa6 fix(editor): 编辑器接口只认真实会话,封掉 name/email 匿名兜底越权(P0)
isAdminRequest 的 query 兜底会把「读文章源码/改文件/删文章/触发 git 发布」
裸露给任何拼参数的人。改为 requireAdminSession:只认 Bearer 会话,
已登录 + is_admin(或邮箱命中配置管理员)才放行。线上 Worker 已部署此版本。
2026-10-04 23:40:49 +08:00
zqlit a4758dbbfd 同步编辑器里已删除的两篇测试文章(工作区与远端保持一致) 2026-10-04 23:40:31 +08:00
zqlit 9f586d625b 后台登录改造:自建登录页 + Cookie 会话,弃用 basic auth 弹窗
- editor-api 新增 /admin/login|session|logout:HttpOnly Cookie 会话(落盘持久化)、
  登录失败限速(5 次锁 5 分钟)、鉴权改为「X-Editor-Token 或 会话」双通道
- 登录页重做:品牌标识、图标输入框、密码可见切换、柔光背景,深浅色适配
- 前端以 /admin/session 探测部署模式:国内机走本机会话,CF 版走原令牌流程
- nginx:/admin/ 与 /api/v2/editor/ 去掉 basic auth 与令牌注入,登录凭据只存 Cookie
- bootstrap.sh 同步 ADMIN_USER/ADMIN_PASS,保持一键迁移能力
2026-10-04 23:40:23 +08:00
zqlit 96751b8143 fix(editor): 删文章 EXDEV 修正 + 订阅口令不再阻断登录
1) 删除文章报 EXDEV(cross-device link not permitted)
   · 根因一:容器内 /blog 与 /app/trash 是两个独立 bind mount,
     rename(2) 跨挂载点必然 EXDEV —— 哪怕宿主机上它们同在一块盘。
   · 根因二:退化成 fs.cpSync 后,Windows 上遇到**中文目录名**会把
     Node 进程直接干崩(exit 127,不抛异常,stdout 缓冲丢失)。
     而文章目录名几乎必然含中文(<日期>-<中文标题>-<slug>)。
   · 修法:改用 readdirSync/copyFileSync 自己递归复制(宽字符路径,中文 OK);
     bootstrap.sh / compose 改成挂 BLOG_DIR 的父目录,让仓库与回收站
     落在**同一个挂载点**内,正常情况下 rename 直接成功、不再复制。

2) 登录页「订阅口令」填错会阻断整个登录
   · 它只是可选订阅模块的口令,不该 throw 掉登录流程。
   · rssApi() 无条件 searchParams.set('token', RSS_TOKEN),会用浏览器里
     记住的旧口令覆盖调用方刚传的值 → 正确口令也验不过。
   · 现在:填错只 toast 提示 + 清掉失效旧口令;rssApi 尊重调用方传的值。

3) wrangler.toml:EDITOR_API_BASE 改指国内机新域名 writeapi.usj.cc
   (原指向即将到期的境外机 post.usj.cc,且那台从未部署过 editor-api)

验证:api-e2e 44/44;front matter 往返 130/130;保存往返 130/130;
中文路径删除实测通过(目录 + 中文子目录 + 图片全部移入回收站,进程不崩)。
2026-10-04 22:11:02 +08:00
zqlit e8d13b9a6e fix(docker): apk 默认走阿里云镜像(国内构建实测官方源单包 30~40s) 2026-10-04 21:33:39 +08:00
zqlit c217d20c30 feat(editor): 在线编辑文章(Worker 前端 + 轻量 docker 后端)
后端(新增 editor-api/,零 npm 依赖,只用 node 内置模块):
- 只做文章相关:列表/读取/新建/保存/删除/图片上传/git 状态·发布·同步
- front matter 往返保真:未改动的块按字节照抄,CRLF/块标量/引号写法都不动
- 列表用目录名当 id,slug 撞名不再静默改错文件(返回 409 列候选)
- 鉴权只有一条路:X-Editor-Token(只存在 Worker 侧,浏览器拿不到)
- 端口只绑 127.0.0.1,由宿主机 nginx 反代出去

部署(新增 deploy/editor-api/bootstrap.sh):
- 一条命令在新机器上完成 克隆仓库→写 .env→起容器→健康检查
- 状态全在两个目录(/srv/blog 仓库工作区 + /srv/editor-api 配置),
  迁移 = 复制目录或在新机重跑本脚本,容器本身无状态

Cloudflare Worker 侧(blog-admin):
- src/routes/editor.ts:鉴权 + 反代,浏览器只跟 Worker 说话
- 管理面板新增「文章编辑」:两栏布局 + 快捷插入面板(13 项短代码,
  与 write-server 的 ShortcutPanel 一致)+ 底部 草稿/保存/发布
- 系统设置改为 schema 驱动的表单,且**以运行时实际生效的配置为准**
  (frontend_conf/captcha/moderator/ip_region/site_default + KV human_check),
  修复「表单显示一套、评论系统跑另一套」的脱节问题

验证:tsc 0 错;front matter 往返 130/130;保存往返 130/130;
API e2e 44/44;无头 Chrome UI e2e 14/14
2026-10-04 21:16:06 +08:00
zqlit 48a2fd697f ci: EdgeOne 境外部署加重试(构建节点到 api.edgeone.ai 偶发 15s 超时会被误报成令牌无效) 2026-10-04 18:45:01 +08:00
zqlit 437cd339ad fix: 管理员编辑跳过邮箱校验 + 返回顶部 rAF 丝滑滚动 + admin.js 正则笔误 + 页面标题污染
- artalk.js: beforeSubmit 在编辑态跳过昵称/邮箱校验(管理员改评论不再被拦)
- floating-tools.js: 返回顶部/去评论改 rAF + easeInOutCubic 自绘,原生 smooth 不可靠
- admin.js: 修 5 处 //s //n 正则笔误(整脚本 SyntaxError 致后台全灰),需 wrangler deploy
- artalk.html: 显式传 pageTitle(不再读 document.title,PJAX 竞态致文章页标题被记成「留言」)
- artalk.js: conf 缓存合并前剔除 pageKey/pageTitle/pageThumb,杜绝跨页串值
2026-10-04 18:30:30 +08:00
zqlit e8c991274a fix(comments): 额度故障态提示去冗余 + 修「误恢复」bug + 本地复现开关
服务端 D1 额度用尽时同屏会出现三层重复提示,且故障态会被定期误解除。

- 提示从三层收敛为两层:横幅只留一句极简状态(⏸ 评论暂停中,预计明早 8:00 恢复),
  卡片是唯一详细说明并紧贴编辑器;删掉「点击编辑器再弹一遍 toast」
- 修 bug:恢复判定原来过于宽松,任何 conf/comments/captcha/votes/pages 请求成功都算恢复;
  但 /captcha/status 走 KV 不读 D1,60s 探针恒成功 → 故障态每 60s 被误撤,编辑器看似正常
  实则列表还挂着。现在恢复只认评论列表本身成功,探针改用 GET /comments?limit=1(真读 D1)
- 故障态不再灰化编辑器(opacity+grayscale 观感很差),只禁交互,原因由横幅说明
- 服务端 errors.ts 文案去掉与卡片标题重复的「评论服务暂时不可用」前缀
- 新增本地调试开关:仅 localhost 下 ?atk_svc=<code> 强制进入故障态,便于排查,生产不生效

改动文件:themes/Ying/assets/js/modules/artalk.js、blog-admin/src/lib/errors.ts
(errors.ts 需下次部署 Worker 才在线上生效,前端已做旧文案兼容清洗)
2026-10-04 18:04:47 +08:00
zqlit 571d552c9d fix(build): 消除构建期远程拉取隐患 + 评论提交反馈 + 友链卡片移动端
构建隐患(本次丢构建的根因)
- footer.html 用 resources.GetRemote 在每个页面渲染时拉 api/links 与 api/feeds,
  构建节点连不上 Cloudflare 时重试撞穿 Hugo partial 30s 超时 → 整站构建失败
- 改为「流水线 curl 快照 + 模板读本地文件」:
  · 新增 scripts/fetch_snapshots.sh,一次拉齐 conf / friend_links / friend_feeds
    (--max-time 30 --retry 2,JSON 校验,失败一律删文件且恒 exit 0)
  · .cnb.yml Hugo 构建 stage 改为调用该脚本
  · footer.html 改读 site.Data.friend_links / site.Data.friend_feeds,缺失渲染空值
  · .gitignore 忽略 /data/friend_links.json 与 /data/friend_feeds.json
- 降级实测:删掉快照后构建仍 EXIT=0,产物 friendLinks 为空数组

评论提交反馈(artalk.js 新增 __atkSubmit 模块)
- 校验:config.beforeSubmit 拦下 内容空 / 昵称空 / 邮箱空 / 邮箱格式错(网址不校验)
- 发送中:editor-submit 常驻 loading,超 7s 补「网络较慢」
- 成功:editor-submitted 按 normal/reply/edit 分文案,is_pending 提示等待审核
- 失败:拦 POST /api/v2/comments,403/429/4xx/5xx/网络异常全部翻译成人话
- 失败时输入内容保留,并清掉 Artalk 自带的重复小字通知
- 无头浏览器实测 19/19 通过(POST 走 mock,未向线上写数据)

友链申请卡片移动端
- max-width:640px 下隐藏 .atk-fl-nick(昵称 + 博主徽章),桌面端不变

改动:6 files, +282 / -61
2026-10-04 17:38:04 +08:00
zqlit 836743b16d ci: 重跑部署(上次构建因构建节点连不上 api.200181.xyz 超时失败) 2026-10-04 17:02:45 +08:00
zqlit e0c5ac5739 feat(comments): 人机验证改点按式 + 分割线可配置 + 投票契约修复
- 人机验证抽成 human.js 模块,改成 Cloudflare Turnstile 风格的「点一下验证」,
  控件移到「评论一下」发送按钮左侧(不再用图形验证码那一套)
- 留言页 / 文章页两段内联脚本(友链弹窗、人机验证)抽成模块随 page-only.js 加载。
  原先内联 <script> 写在 #pjax-container 外,PJAX 只换容器内容不重新执行它,
  从首页 PJAX 进留言页会出现「申请友链卡片点了没反应」「没有人机验证控件」
- 友链申请弹窗 #flApplyBox 隐藏滚动条(内容仍可滚,避开细滚动条 + 毛玻璃的割裂感)
- 内容分割线抽成 partial divider.html,hugo.toml [params.divider] 可切
  icon(原生 uk-divider-icon)/ image(本地图片),当前用 icon
- 投票契约修复(后端 blog-admin):Artalk 客户端把方向编在 target_name 后缀
  (comment_up / comment_down / page_up / page_down)、路径只有两段,
  而原先只注册了三段 :choice 路由 → 点赞/踩一路 404、界面点了没反应。
  补 POST /votes/:target_name/:target_id,新增 parseVoteTarget() 同时兼容两套契约
  (名字里与路径里方向矛盾时判非法,不静默取其一),错误文案改用实体名
- 无身份点赞不再静默失败:artalk.js 加 __atkVoteGuard,
  未填昵称/邮箱时拦截并提示「填写昵称和邮箱后才能点赞」,
  同时把光标送到昵称框(多个同名输入框逐个试,以 focus 真生效为判据)
2026-10-04 16:26:28 +08:00
zqlit 50a906e07c perf(comments): 评论加载提速 + 骨架屏 + 友链弹窗 pjax 修复 + toast 统一 Message.js
优化:
- initArtalk 加节点级幂等守卫(mount.__atkInited,失败回滚),
  消除 artalk.html/mypjax 多入口导致的 4× comments + 4× pv 重复请求
- fetch 包装层对无 body 的 GET/HEAD 去掉多余 content-type,
  消掉 Artalk 对 GET /comments 的 CORS 预检(按完整 URL 缓存,
  每篇文章都重付一次 RTT)。实测评论列表 +1150~1565ms → +626ms

修复:
- 评论列表骨架屏改注入 .atk-list-body(原骨架在 #Comments 内,
  被 Artalk.init 清空),list-loaded/故障/12s 兜底移除
- 友链申请弹窗抽成 modules/friendlink.js 进 page-only bundle
  (原内联脚本在 #pjax-container 外,pjax 后 flApplyOpenModal 未定义)
- toast 统一走本地化 Message.js/Qmsg 适配层(保留 window.Toast/showToast 旧 API)

文档:
- README / 架构总览 更新;新增 评论加载优化方案.md
2026-10-04 15:00:52 +08:00
zqlit 9e972f2b61 ci: 修 CNB docker build 上下文(by 必须列出 bin/linux/hugo)
首跑失败在镜像构建:
  COPY bin/linux/hugo /usr/local/bin/hugo
  ERROR: failed to compute cache key ... "bin/linux/hugo": not found

根因(CNB 官方文档原文):docker build 的构建上下文里「只包含 Dockerfile
和 by 声明的文件」,未出现在 by 列表中的文件在构建时一律视为不存在。
原配置 by:[package.json] 既漏了 hugo 二进制,又列了个 Dockerfile 根本
没 COPY 的 package.json。

改为 by: [bin/linux/hugo] / versionBy: [bin/linux/hugo]。
2026-10-04 13:07:31 +08:00
zqlit c68d60f34c ci: 触发 CNB 首次流水线(验证密钥仓库 + 构建链路)
密钥仓库 zqlit/blog-secrets 已建好,重跑一次确认整条链路:
Hugo 构建 → 又拍云 sync/purge → 多吉云刷新 → EdgeOne 部署 → 邮件通知。
2026-10-04 13:04:53 +08:00
zqlit 03a066296a docs: CNB构建落地方案 填实密钥仓库地址(zqlit/blog-secrets) 2026-10-04 12:56:53 +08:00
zqlit bcc2236bde ci: 迁移到 CNB 流水线(.cnb.yml)+ 零依赖邮件通知
- .cnb.yml:6 个 stage 顺序执行(Hugo 构建 → 又拍云 sync → 又拍云 purge →
  多吉云刷新 → EdgeOne 部署 → 上报状态);push 与每日 09:00 定时任务
  共用同一组 stage(YAML 锚点),失败通知走 failStages
- 又拍云保持「非阻断但如实上报」:allowFailure 沿用原版 continue-on-error
  语义,国内线路直传失败不拖垮境外线路
- scripts/send_mail.js:零依赖 SMTP 发信(node net/tls,465 隐式 TLS +
  587 STARTTLS),变量名沿用 MAIL_USERNAME / MAIL_PASSWORD
- 密钥经 imports 从密钥仓库 zqlit/blog-secrets 注入(共 10 项)
- .gitignore:补 cnb-secrets.yml 本地粘贴草稿(含真实令牌,不入库)

原 GitHub Actions 里的 artifact 传递 / COS 中转 / 广州机 sync.sh 轮询
整条链在此不再需要 —— 其唯一根因是 runner 在境外,CNB 节点在国内。
2026-10-04 12:56:05 +08:00
zqlit e0218b42bb docs: 迁 CNB 前的架构决策文档 + 构建骨架
- 决策文档:架构总览 / 代码源与构建平台选型 / CNB构建落地方案 /
  EdgeOne双区域 / Gitee / 阿里云ESA / 精简方案
- CNB 构建骨架:deploy/Dockerfile、bin/linux/hugo
  (84MB,linux/amd64 extended 0.128.2,供 CNB 容器 COPY 用)
- scripts/setup-cnb-remotes.sh:双远端切换(CNB 主仓 + GitHub 备份)
- .gitignore 补 .workbuddy/(工作数据不入库)
2026-10-04 12:40:17 +08:00
zqlit 1f568bbfd1 fix: artifact 改为单文件 tar 传递(v3 逐文件上传时 Gitea finalize 500)
实测 run #56:upload-artifact@v3 传 public/ 目录时会逐文件上传 3216 个
blob(376MB),文件全部传完后 finalize 调用被 Gitea 拒绝:
  Finalize artifact upload - Attempt 1..5 of 5 failed: Request timeout
  ::error::Finalize artifact upload failed: Artifact service responded with 500
整轮 build 白跑约 30 分钟(v3 已越过 v4 的 'not supported on GHES' 报错点,
这是另一个独立问题)。

改法:build 侧先把 public/ 打成单个 public.tar.zst(无 zstd 时退 gzip),
只上传这 1 个文件 -> 上传是 1 次 PUT、finalize 常数级开销。
deploy-edgeone 侧下载后解包,产物结构因此完全确定(不再有「多一层
public/」的歧义),原 Normalize 步骤随之删除。

配套:中转机 sync.sh.new 增加「artifact 内 tar 包二次解包」(已上传服务器,
未生效)。
2026-10-04 11:08:40 +08:00
zqlit ed38f9384b fix: upload/download-artifact 降级 v4 -> v3(v4 在 Gitea 上不被支持)
Deploy to Production / probe-network (push) Skipped
Deploy to Production / pre-check (push) Successful in 1m9s
Deploy to Production / build (push) Failing after 39m20s
Deploy to Production / deploy-edgeone (push) Skipped
Deploy to Production / finalize (push) Skipped
Deploy to Production / notify-failure (push) Successful in 5s
实测 run #55:build job 在「Upload build artifact (public/)」失败:
  ::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(走旧 API)。

v3 无 include-hidden-files 选项、隐藏文件不保证进包,故:
- build job 额外写非隐藏的 public/build_hash.txt 兜底
- 中转机 sync.sh 优先读 .build_hash,读不到退 build_hash.txt,最后才本地复算
2026-10-04 10:33:55 +08:00
zqlit fffdbe6865 fix: artifact 传递补两个坑(隐藏文件 + 产物层数)
Deploy to Production / probe-network (push) Skipped
Deploy to Production / pre-check (push) Successful in 54s
Deploy to Production / build (push) Failing after 3m53s
Deploy to Production / deploy-edgeone (push) Skipped
Deploy to Production / finalize (push) Skipped
Deploy to Production / notify-failure (push) Successful in 4s
- upload-artifact@v4 默认排除隐藏文件,public/.build_hash 会丢
  → 开 include-hidden-files: true,否则中转机读不到 hash 无法判断同步
- download 后产物可能多一层 public/ → EdgeOne 部署前统一拍平并校验 index.html

配合中转机 sync.sh 改造(COS → Gitea artifact),两者需同时生效。
2026-10-04 10:06:27 +08:00
zqlit 4ede54b96d fix: 修复 .env.example 被根 .gitignore 的 .env.* 规则误伤
Deploy to Production / probe-network (push) Skipped
Deploy to Production / pre-check (push) Successful in 50s
Deploy to Production / build (push) Failing after 4m16s
Deploy to Production / deploy-edgeone (push) Skipped
Deploy to Production / finalize (push) Skipped
Deploy to Production / notify-failure (push) Successful in 6s
2026-10-04 09:53:20 +08:00
zqlit 2388694e8a chore: 砍COS改造(deploy.yml改artifact传递+gitea-backup可移植部署包+改造步骤文档) 2026-10-04 09:52:50 +08:00
zqlit a74b3c7127 归档 artalk-cf 评论后端 + rss-robot 到 blog-admin(含技术选型/模块分布 README)
Deploy to Production / pre-check (push) Successful in 58s
Deploy to Production / build (push) Successful in 4m3s
Deploy to Production / deploy-edgeone (push) Successful in 3m48s
Deploy to Production / finalize (push) Successful in 26s
Deploy to Production / notify-failure (push) Skipped
2026-10-04 08:45:40 +08:00
zqlit 855a001046 编辑器禁用态:置灰但内容完整(补发送字样/占位符/表情预览图标),svc-down 隐藏 Powered By 版权
Deploy to Production / pre-check (push) Successful in 35s
Deploy to Production / build (push) Successful in 3m40s
Deploy to Production / deploy-edgeone (push) Successful in 3m35s
Deploy to Production / finalize (push) Successful in 22s
Deploy to Production / notify-failure (push) Skipped
2026-10-04 00:28:12 +08:00
zqlit 298f10e6bc 评论错误卡片:额度文案双保险(中文特征词兜底+连接异常优先判)、底部边距加大与footer分隔线隔开(桌面30px/移动24px)
Deploy to Production / pre-check (push) Successful in 1m19s
Deploy to Production / build (push) Successful in 4m3s
Deploy to Production / deploy-edgeone (push) Successful in 3m55s
Deploy to Production / finalize (push) Successful in 28s
Deploy to Production / notify-failure (push) Skipped
2026-10-03 21:01:58 +08:00
zqlit bff48517c0 评论验证码取不到时降级为可重试提示(不留破图)
Deploy to Production / pre-check (push) Successful in 59s
Deploy to Production / build (push) Successful in 4m42s
Deploy to Production / deploy-edgeone (push) Successful in 4m0s
Deploy to Production / finalize (push) Successful in 31s
Deploy to Production / notify-failure (push) Skipped
2026-10-03 20:37:46 +08:00
zqlit 8535f759f4 错误卡片重设计:居中布局+去技术详情(读者无需看),图标放大、重试改胶囊按钮
Deploy to Production / pre-check (push) Successful in 54s
Deploy to Production / build (push) Canceled after 5s
Deploy to Production / deploy-edgeone (push) Canceled after 0s
Deploy to Production / finalize (push) Canceled after 0s
Deploy to Production / notify-failure (push) Canceled after 0s
2026-10-03 20:07:05 +08:00
zqlit afa1346a0f 评论加载失败改为清晰提示:接管 Artalk 错误层,按服务端错误码给读者人话文案+可展开技术详情
Deploy to Production / pre-check (push) Successful in 1m0s
Deploy to Production / build (push) Successful in 4m18s
Deploy to Production / deploy-edgeone (push) Successful in 3m55s
Deploy to Production / finalize (push) Successful in 24s
Deploy to Production / notify-failure (push) Skipped
2026-10-03 19:49:10 +08:00
zqlit 45afb53580 评论人机验证(自研一键验证):静默PoW+行为信号,必要时点一下/图形验证码兜底
Deploy to Production / pre-check (push) Successful in 49s
Deploy to Production / build (push) Successful in 4m39s
Deploy to Production / deploy-edgeone (push) Successful in 3m42s
Deploy to Production / finalize (push) Successful in 33s
Deploy to Production / notify-failure (push) Skipped
2026-10-03 19:30:07 +08:00
zqlit ae93bc7d08 友圈卡片背景头像效果恢复:背景图/头像统一用自建favicon绝对地址+失败兜底,缓存key升v2
Deploy to Production / pre-check (push) Successful in 1m47s
Deploy to Production / build (push) Successful in 4m23s
Deploy to Production / deploy-edgeone (push) Successful in 3m57s
Deploy to Production / finalize (push) Successful in 28s
Deploy to Production / notify-failure (push) Skipped
2026-10-03 17:39:06 +08:00
zqlit 899dcfd006 友圈头像不再兜底第三方默认头像:改用站内/api/favicon(绝对地址),兜底也从cravatar换成自建服务
Deploy to Production / pre-check (push) Successful in 48s
Deploy to Production / build (push) Canceled after 2m55s
Deploy to Production / deploy-edgeone (push) Canceled after 0s
Deploy to Production / finalize (push) Canceled after 0s
Deploy to Production / notify-failure (push) Canceled after 0s
2026-10-03 17:35:32 +08:00
zqlit 5858188628 友链卡片移动端适配:≤640px隐藏头像、收紧间距内边距、微调字号字距
Deploy to Production / pre-check (push) Successful in 47s
Deploy to Production / build (push) Successful in 4m17s
Deploy to Production / deploy-edgeone (push) Canceled after 37s
Deploy to Production / finalize (push) Canceled after 0s
Deploy to Production / notify-failure (push) Canceled after 0s
2026-10-03 17:29:20 +08:00
zqlit 63fd5641d5 友链卡片只转换顶层评论(回复里的同串URL不再被隐藏)
Deploy to Production / pre-check (push) Successful in 53s
Deploy to Production / build (push) Canceled after 3m23s
Deploy to Production / deploy-edgeone (push) Canceled after 0s
Deploy to Production / finalize (push) Canceled after 0s
Deploy to Production / notify-failure (push) Canceled after 0s
2026-10-03 17:23:46 +08:00
zqlit 627b6e6512 徽章样式补充卡片上下文(.atk-fl-card):卡片已移出.atk-comment导致博主徽章露出Artalk内联蓝底
Deploy to Production / pre-check (push) Canceled after 22s
Deploy to Production / build (push) Canceled after 0s
Deploy to Production / deploy-edgeone (push) Canceled after 0s
Deploy to Production / finalize (push) Canceled after 0s
Deploy to Production / notify-failure (push) Canceled after 0s
2026-10-03 17:22:58 +08:00
zqlit c952620fd6 友链卡片固定在评论列表上方(不再被新评论挤到中间):整条评论用class隐藏,卡片插入atk-list-body首位
Deploy to Production / pre-check (push) Successful in 53s
Deploy to Production / build (push) Canceled after 1m2s
Deploy to Production / deploy-edgeone (push) Canceled after 0s
Deploy to Production / finalize (push) Canceled after 0s
Deploy to Production / notify-failure (push) Canceled after 0s
2026-10-03 17:20:04 +08:00
zqlit ab94c25273 修复发评论后卡片被挤下去:原评论内容改用class+!important隐藏(Artalk重渲染会抹掉内联display),转换逻辑改幂等自愈
Deploy to Production / pre-check (push) Successful in 50s
Deploy to Production / build (push) Successful in 4m30s
Deploy to Production / deploy-edgeone (push) Canceled after 2m21s
Deploy to Production / finalize (push) Canceled after 0s
Deploy to Production / notify-failure (push) Canceled after 0s
2026-10-03 17:13:06 +08:00
zqlit 0f4761803c 友链申请表单新增可选图标链接字段(留空由/api/favicon自动抓取站点图标)
Deploy to Production / pre-check (push) Canceled after 6m47s
Deploy to Production / build (push) Canceled after 0s
Deploy to Production / deploy-edgeone (push) Canceled after 0s
Deploy to Production / finalize (push) Canceled after 0s
Deploy to Production / notify-failure (push) Canceled after 0s
2026-10-03 17:05:57 +08:00
zqlit 98126cafe6 评论徽章DOM/尺寸统一(拆badge-wrap+单一类名)、修复main.css!important劫持徽章尺寸、友链卡片挂到comment级与输入框对齐、卡片动效+打字机、申请弹窗按钮错位与滚动条修复
Deploy to Production / pre-check (push) Successful in 58s
Deploy to Production / build (push) Successful in 3m56s
Deploy to Production / deploy-edgeone (push) Canceled after 1m23s
Deploy to Production / finalize (push) Canceled after 0s
Deploy to Production / notify-failure (push) Canceled after 0s
2026-10-03 16:59:34 +08:00
zqlit 87c47d13c9 徽章样式定稿:博主浅底灰黑描边(回线上版)、置顶浅底红字描边;恢复Artalk默认徽章右间距与CN地域徽标原样
Deploy to Production / pre-check (push) Successful in 44s
Deploy to Production / build (push) Successful in 4m40s
Deploy to Production / deploy-edgeone (push) Successful in 3m48s
Deploy to Production / finalize (push) Successful in 24s
Deploy to Production / notify-failure (push) Skipped
2026-10-03 16:12:23 +08:00
zqlit 36e74d46f4 友链申请入口改URL评论触发+卡片原生风格化; 博主/头衔徽章改浅底描边风格与友链友圈官职统一
Deploy to Production / pre-check (push) Successful in 39s
Deploy to Production / build (push) Successful in 4m55s
Deploy to Production / deploy-edgeone (push) Successful in 4m1s
Deploy to Production / finalize (push) Successful in 25s
Deploy to Production / notify-failure (push) Skipped
2026-10-03 15:16:00 +08:00
zqlit e84de9033d 友链申请卡片重设计(头像+昵称+徽章+主题色) + 弹窗横向滚动条/显隐bug修复
Deploy to Production / pre-check (push) Successful in 1m9s
Deploy to Production / build (push) Successful in 4m9s
Deploy to Production / deploy-edgeone (push) Successful in 3m46s
Deploy to Production / finalize (push) Successful in 28s
Deploy to Production / notify-failure (push) Skipped
2026-10-03 14:28:50 +08:00
QunLIn 79c1e51d5c 评论徽章全展示按优先级排序 + 新增金色头衔徽章
Deploy to Production / pre-check (push) Successful in 1m12s
Deploy to Production / build (push) Successful in 2m52s
Deploy to Production / deploy-edgeone (push) Successful in 3m27s
Deploy to Production / finalize (push) Successful in 30s
Deploy to Production / notify-failure (push) Skipped
- 徽章不再互斥:满足条件全部展示,排序 = 系统徽章(博主等)
  > 头衔(新增)> 友链 > 友圈 > 官职
- 头衔徽章:后端 users 表新增 title_name 字段,管理面板用户编辑
  可设置,金色 UI 与官职一致,优先级仅次于系统徽章
- 官方 badge-wrap 挪进昵称元素内,保证与主题徽章的排序正确
2026-10-03 13:37:22 +08:00
QunLIn da1a77a313 友链申请卡片改挂置顶评论 + 弹窗挂 body 加毛玻璃
Deploy to Production / pre-check (push) Successful in 28s
Deploy to Production / build (push) Successful in 2m41s
Deploy to Production / deploy-edgeone (push) Successful in 3m47s
Deploy to Production / finalize (push) Successful in 27s
Deploy to Production / notify-failure (push) Skipped
- 置顶评论卡片化:管理员置顶且内容含「友链自助申请」的评论,
  整条解析成申请入口卡片(隐藏昵称/页脚,渐变虚线卡片,点击打开
  申请弹窗),其余置顶评论不受影响
- 弹窗 DOM 动态挂 document.body:评论容器带 transform 动画会把
  position:fixed 劫持成相对定位,导致遮罩/表单跟着容器变形——
  挂 body 彻底规避
- 遮罩加 backdrop-filter 毛玻璃(GPU 合成,不影响页面性能)
2026-10-03 13:08:14 +08:00
QunLIn 5fcbd2a7b6 字体切全量、评论徽章优先级、友链自助申请卡片、友圈头像兜底
Deploy to Production / pre-check (push) Successful in 1m27s
Deploy to Production / build (push) Successful in 4m49s
Deploy to Production / deploy-edgeone (push) Successful in 3m50s
Deploy to Production / finalize (push) Successful in 21s
Deploy to Production / notify-failure (push) Skipped
- 字体:zql 从 subset(776K) 切换到全量(1.2MB woff2),subset 覆盖
  不足导致大量字回退系统字体,视觉不一致;全量一次加载彻底解决
- 评论徽章统一 comment-rendered 注入,优先级互斥:友链 > 友圈 >
  官职(后端 rank 字段),官方系统徽章(博主)独立显示
- 留言页新增友链自助申请入口卡片 + 弹窗表单(名称/链接/订阅/邮箱
  必填),提交进 api.200181.xyz 待审队列,审核结果邮件通知申请人
- 友圈头像:favicon 抓取失败时兜底第三方 favicon 服务
2026-10-03 12:13:45 +08:00
QunLIn e12da91314 评论后端统一到 api.200181.xyz(与 RSS 同一 Worker)
Deploy to Production / pre-check (push) Successful in 1m10s
Deploy to Production / build (push) Successful in 4m44s
Deploy to Production / deploy-edgeone (push) Successful in 3m39s
Deploy to Production / finalize (push) Successful in 35s
Deploy to Production / notify-failure (push) Skipped
评论与订阅已合并为同一个 Cloudflare Worker(artalk-cf,
同时服务 api.200181.xyz 与 artalk.200181.xyz)。为收敛到
单一域名/单一面板,评论入口统一切到 api.200181.xyz。

- hugo.toml params.artalk.server → api.200181.xyz
- write-server ARTALK_SERVER 默认值同步
- 注意:post.usj.cc 服务器 .env 里若有 ARTALK_SERVER 覆盖项需同步改
2026-10-03 10:57:33 +08:00
QunLIn e7d94c11fd CI 构建完成后上报部署状态到 api.200181.xyz
Deploy to Production / pre-check (push) Successful in 41s
Deploy to Production / build (push) Successful in 4m52s
Deploy to Production / deploy-edgeone (push) Successful in 3m48s
Deploy to Production / finalize (push) Successful in 29s
Deploy to Production / notify-failure (push) Skipped
中转机 sync.sh 同步完成后会上报 phase=synced,CI 在 COS 上传后
上报 phase=built。打开 https://api.200181.xyz/api/deploy-status
即可查看国内线路是否同步完成(state: synced/syncing)。
需在 Gitea 仓库 Secrets 配置 RSS_API_TOKEN(未配置则跳过上报)。
2026-10-03 10:40:22 +08:00
QunLIn f032310188 永久跳过海外直传 UpYun,国内线路全走中转机
Deploy to Production / pre-check (push) Successful in 37s
Deploy to Production / build (push) Successful in 3m56s
Deploy to Production / deploy-edgeone (push) Successful in 3m51s
Deploy to Production / finalize (push) Successful in 24s
Deploy to Production / notify-failure (push) Skipped
海外 runner 对 UpYun v0 接口无论什么参数都是 70 秒左右 EOF
(-w 10/--strong 与 -w 3 均验证),3 轮重试全败,每次部署白耗
5 分钟(build #25 日志实锤)。

直传步骤改为 if: false 永久跳过(代码保留,将来 runner 挪国内
删掉该条件即可恢复)。国内线路由腾讯云广州中转机兜底:每分钟
比对 COS build hash → 下载产物 → upx sync → 又拍云 purge →
多吉云刷新(sync.sh 自带,CI 侧不再重复刷)。
2026-10-03 10:23:43 +08:00
QunLIn cc85ace7c2 官职徽章改回主题渲染,数据由后端 rank 字段下发
Deploy to Production / pre-check (push) Successful in 1m16s
Deploy to Production / build (push) Successful in 10m24s
Deploy to Production / deploy-edgeone (push) Successful in 3m52s
Deploy to Production / finalize (push) Successful in 28s
Deploy to Production / notify-failure (push) Skipped
上一版把九品官阶塞进官方 badge_name/badge_color 槽——但博主
徽章是独一无二的,官职应该接在它后面而不是挤占同一个槽位。
后端现下发独立字段 rank_name/rank_title/rank_count/rank_color/
rank_bg(配色沿用主题原版彩字+半透明底),本文件在
comment-rendered 事件里渲染官职徽章。
2026-10-03 10:01:52 +08:00
QunLIn 20869a3c45 RSS API 地址全部收敛到 hugo.toml [params.rssapi],切到 api.200181.xyz
Deploy to Production / pre-check (push) Successful in 2m0s
Deploy to Production / build (push) Successful in 9m9s
Deploy to Production / deploy-edgeone (push) Successful in 4m3s
Deploy to Production / finalize (push) Successful in 29s
Deploy to Production / notify-failure (push) Skipped
- 新增 [params.rssapi] base 配置,主题 7 处硬编码改为读配置:
  head(preconnect+window.rssApiBase)、footer(构建时拉 links/feeds)、
  render-link(favicon)、circles/links 页 fetch、linkify.js(读 window.rssApiBase)
- scripts/fetch_greetings.js 支持 RSS_API_BASE 环境变量,默认新地址
- scfapi.usj.cc(国内代理)保持不变
2026-10-03 01:19:02 +08:00
QunLIn be5abce16a 评论等级改为后端计算(artalk-cf 下发官方 badge)
主题原来的九品官阶用 localStorage 累计——只统计当前浏览器
渲染过的评论,换设备/清缓存就失真,别人看到的也不准。
现在后端按全站真实评论数算好,随评论 API 下发
badge_name/badge_color,官方前端原生渲染,跨设备一致。
本文件移除本地注入逻辑,友链/友圈徽章保留。
2026-10-03 01:11:43 +08:00
QunLIn 87cfa01b3d write-server 切到自建后端:评论 artalk.200181.xyz + RSS api.200181.xyz
- ARTALK_SERVER: artalk.usj.cc(朋友机器)→ artalk.200181.xyz(自建 CF Workers)
- RSS_API_BASE: api.usj.cc(EdgeOne)→ api.200181.xyz(迁到 CF Workers 的 rss-robot-cf)
- SCF_PROXY 不变(scfapi.usj.cc 是国内代理,继续复用)
2026-10-02 22:37:54 +08:00
QunLIn f486a46b6d 修 UpYun 直传:降并发去 --strong、加三轮重试;UpYun 未成功不再刷国内 CDN
Deploy to Production / pre-check (push) Successful in 1m8s
Deploy to Production / build (push) Canceled after 28m0s
Deploy to Production / deploy-edgeone (push) Canceled after 0s
Deploy to Production / finalize (push) Skipped
Deploy to Production / notify-failure (push) Skipped
- upx sync 从 -w 10 --delete --strong 改为 -w 3 --delete + 三轮重试(单次 25 分钟)。
  本runner 在海外,UpYun v0 接口对并发敏感,-w 10 + --strong 会让一批目录的
  mkdir 全部 EOF(2026-10-02 连续两次失败,第二次直接撞 480s 超时)。
  upx 0.4.9 只有 -w/--delete/--strong 三个参数,没有 --resume/--checksum,
  所以只能靠降并发 + 整体重试。
- 多吉云 CDN 刷新改为仅在 UpYun 成功时执行:EdgeOne 是境外线路,
  它成功不代表国内源站已更新,之前会刷出一份"刷新成功但内容没变"的假象。
2026-10-02 20:39:00 +08:00
QunLIn 09f9e96248 评论后端切到 Cloudflare Workers 自建版
Deploy to Production / pre-check (push) Successful in 1m47s
Deploy to Production / build (push) Successful in 13m55s
Deploy to Production / deploy-edgeone (push) Successful in 4m14s
Deploy to Production / finalize (push) Canceled after 3s
Deploy to Production / notify-failure (push) Canceled after 0s
artalk.usj.cc(朋友机器)→ artalk.200181.xyz
数据已用增量导入同步到 artalk-cf 的 D1。
2026-10-02 19:55:29 +08:00
zqlit 4215e08113 删除一篇文章
Deploy to Production / pre-check (push) Successful in 2m32s
Deploy to Production / build (push) Successful in 9m30s
Deploy to Production / deploy-edgeone (push) Successful in 4m5s
Deploy to Production / finalize (push) Successful in 29s
Deploy to Production / notify-failure (push) Skipped
2026-10-02 18:04:21 +08:00
zqlit 04d0274d9f 修改文章
Deploy to Production / pre-check (push) Successful in 1m17s
Deploy to Production / build (push) Successful in 9m27s
Deploy to Production / deploy-edgeone (push) Successful in 4m43s
Deploy to Production / finalize (push) Successful in 33s
Deploy to Production / notify-failure (push) Skipped
2026-10-02 16:44:28 +08:00
zqlit 34c8164d95 merge: 合并 Gitea CI 修复(三态上报) 与内容发布(测试部署)
Deploy to Production / pre-check (push) Successful in 1m25s
Deploy to Production / build (push) Successful in 9m7s
Deploy to Production / deploy-edgeone (push) Successful in 4m20s
Deploy to Production / finalize (push) Successful in 31s
Deploy to Production / notify-failure (push) Skipped
- 80ea1ff5 只推过 Gitea,16c5ce33 只推过 GitHub,两条线在 95a8455c 分叉
- 写作后台 origin 的 fetch 只指向 GitHub,看不到 Gitea 上的提交,导致 push Gitea 被拒
- 合并后两边内容一致,此后任何提交都同时推两个远端
2026-10-01 20:53:26 +08:00
zqlit 80ea1ff5b3 fix(ci): 删除空壳 deploy-upyun job,UpYun 直传改为如实上报三态
Deploy to Production / pre-check (push) Successful in 2m6s
Deploy to Production / build (push) Successful in 4m18s
Deploy to Production / deploy-edgeone (push) Successful in 7s
Deploy to Production / finalize (push) Successful in 16s
Deploy to Production / notify-failure (push) Skipped
deploy-upyun 是零上传动作的空壳 job,却无条件打印「UpYun 已同步最新内容」,其输出被 Telegram / 飞书 / 邮件通知当成 UpYun 部署成功的依据;真正的上传步骤标了 continue-on-error,失败也不影响 job 颜色。三层叠加,导致 7/25 起国内线路停更两个多月而通知始终显示成功。

改动:删除 deploy-upyun(finalize / notify-failure 的 needs 同步清理);Upload to UpYun 在退出时写 GITHUB_OUTPUT,如实上报 success / failure / skipped 三态与真实耗时(不用 steps.outcome,只依赖最通用的 output 传递);通知与 Step Summary 改为展示三态真实结果,移除「文件已在 Build 阶段同步」这类未经验证的说法。

又拍云直传失败仍不阻断流水线 —— 国内线路由腾讯云广州的 1Panel 计划任务「又拍云同步(国内中转)」每分钟兜底增量推送。
2026-10-01 16:47:05 +08:00
zqlit e1f0b49754 fix(ci): 修复提交信息含 ${{ }} 字样导致 pre-check 崩溃
Deploy to Production / pre-check (push) Successful in 2m35s
Deploy to Production / build (push) Successful in 7m53s
Deploy to Production / deploy-edgeone (push) Successful in 3m52s
Deploy to Production / deploy-upyun (push) Successful in 5s
Deploy to Production / finalize (push) Successful in 30s
Deploy to Production / notify-failure (push) Skipped
根因:pre-check 的 Validate commit 步骤把 commit message 直接插值进 shell 脚本:

    COMMIT_MESSAGE="${{ github.event.head_commit.message }}"

多行提交信息会撑破脚本;若正文里出现 ${{ ... }} 之类字样(例如上一次提交
正文中的 ${{ gitea.* }}),bash 会把它当变量展开并报 "bad substitution",
exit 1 → pre-check failure → build 被 skip。

实证(Gitea task 35 日志):
    /var/run/act/workflow/commit-check: line 20: perf(ci): 兼容 GitHub Actions...
    : bad substitution
    ❌ Failure - Main Validate commit (only for push events)

修复:改用 step 级 env: 传值(COMMIT_AUTHOR / COMMIT_MESSAGE),
commit message 不再进入脚本文本。GitHub Actions 与 Gitea Actions
行为一致,两边都安全,同时消除命令注入面。

本次提交正文刻意保留了 ${{ gitea.* }} 字样,作为该修复的回归验证。
2026-10-01 12:37:22 +08:00
zqlit 92326fff38 perf(ci): 兼容 GitHub Actions 并大幅缩短构建耗时
Deploy to Production / pre-check (push) Failing after 2m54s
Deploy to Production / build (push) Skipped
Deploy to Production / deploy-edgeone (push) Skipped
Deploy to Production / deploy-upyun (push) Skipped
Deploy to Production / finalize (push) Skipped
Deploy to Production / notify-failure (push) Successful in 6s
兼容性(迁移回 GitHub Actions 必需):
- 移除 5 处 ${{ gitea.* }} 专有上下文(Gitea 之外会求值失败),
  改为按 github.server_url 判断的 RUN_URL,GitHub / Gitea 通用
- Telegram / 飞书 / 邮件里的运行详情链接两边都正确

耗时优化:
- build: checkout 加 filter=blob:none(partial clone),
  保留完整提交历史供 Hugo 的 :git lastmod 使用,
  但不拉历史版本的文件内容(多为图片),省约 200MB 传输
- deploy-edgeone: 移除 checkout(原先约 81 秒),内联原 deploy_edgeone.sh
- finalize: 移除 checkout(约 145 秒)与 npm ci(约 66 秒),
  内联零依赖的 refresh_cdn.js(只用 node 内置 crypto 与 fetch)
- finalize: setup-node 不再启用 npm 缓存(本 job 已不安装依赖)

UpYun 相关按用户要求保持原样不动。
2026-10-01 12:22:41 +08:00
zqlit ad118b470d perf(ci): Gitea 环境跳过无效缓存,消除每次约 9 分钟空等
Deploy to Production / pre-check (push) Successful in 54s
Deploy to Production / build (push) Successful in 9m8s
Deploy to Production / deploy-edgeone (push) Successful in 5m29s
Deploy to Production / deploy-upyun (push) Successful in 6s
Deploy to Production / finalize (push) Successful in 4m17s
Deploy to Production / notify-failure (push) Skipped
根因:/opt/gitea/runner/config.yaml 中 container.network=gitea_default
(job 容器落在 172.20.x),但 runner 容器自身只挂在 bridge 网络
(172.17.0.2),其提供的缓存服务 172.17.0.2:44869 跨 bridge 网段不可达。

实测证据:
- runner 容器内 wget http://127.0.0.1:44869/ -> HTTP 404(服务活着)
- gitea 容器 wget http://172.17.0.2:44869/  -> download timed out
- 构建日志:Setup Node restore 死等 278s(01:07:17 -> 01:11:55 ETIMEDOUT)
            Post Setup Node save 死等 288s(01:14:54 -> 01:19:42 ETIMEDOUT)
            Cache Hugo resources restore 亦超时
合计每次构建约 10 分钟纯浪费,且缓存从未真正命中或保存成功过。

改动:
- Setup Node 的 cache 改为按 github.server_url 条件启用;Gitea 上
  表达式求值为空串,setup-node 自动跳过缓存
- Cache Hugo resources 步骤加 if 条件,Gitea 上不执行(避免 restore+save 双超时)
- npm ci 增加 --no-audit --no-fund
- step summary 在 Gitea 上如实显示"已跳过",不再误报"未命中"

无副作用:npm ci 实测 68s 本就是在无缓存状态下跑的(restore 从未成功),
去掉缓存不会使其变慢;GitHub 侧行为完全不变。
2026-10-01 11:42:43 +08:00
zqlit 6838934fa5 fix(ci): 修复定时构建「无新提交则跳过」逻辑失效
Deploy to Production / pre-check (push) Successful in 42s
Deploy to Production / build (push) Canceled after 11m47s
Deploy to Production / deploy-edgeone (push) Canceled after 0s
Deploy to Production / deploy-upyun (push) Canceled after 0s
Deploy to Production / finalize (push) Canceled after 0s
Deploy to Production / notify-failure (push) Canceled after 0s
根因:pre-check job 的 outputs.can_deploy 绑定在 steps.repo-check 上,
而该步骤是无条件写 true;真正写 can_deploy=false 的是 steps.cron-check,
它写进自己步骤的 outputs,从未被 job 级 output 采用。
于是 build 的 if: needs.pre-check.outputs.can_deploy == 'true' 恒为真。

证据(run 8, event=schedule):
- 日志 cron-check 已打印「距离上次提交: 9 小时 / 超过 1 小时没有新提交,跳过定时构建」
- 但随后 build 的 if 表达式求值
  'steps.repo-check.outputs.can_deploy' 结果为 true,build 照常执行

后果:每天北京 09:00 无条件全量构建(build ~17min + finalize ~12.5min),
2 核服务器被白占约 32 分钟/天。线上不受影响(build hash 未变,
COS/EdgeOne/UpYun 上传与部署均正确 skip)。

改动:
- 合并 repo-check 与 cron-check 为单一步骤 decide,统一计算并输出 can_deploy
- pre-check.outputs.can_deploy 改指向 steps.decide.outputs.can_deploy
- 去掉原 cron-check 的 exit 0(避免步骤提前中断导致 output 丢失)
2026-10-01 11:30:15 +08:00