Commit Graph
26 Commits
Author SHA1 Message Date
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 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 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 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 cfadba6007 fix(后台文案): 删除确认框措辞纠正
旧文案「…不进 git,也不影响版本历史,但前台会立刻少一篇」有两处不准:
- 「前台会立刻少一篇」不成立 —— DELETE 只把目录 move 到服务器回收目录
  (/srv/editor-trash),不 commit 不 push,线上要等下次「发布」才少一篇;
- 「不影响版本历史」也不严谨 —— 下次发布时这次删除会作为 D 一起进历史。

新文案:「文件会移到服务器上的回收目录(在仓库外,不进 git):
后台立刻少一篇,线上要等你下次点「发布」后才会少。确定删除?」
2026-10-05 22:53:30 +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 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 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 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 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 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 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 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 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 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