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/ 才是全站),并打印刷新清单
- 新增文档:评论孤儿修复方案、函数版证书管家方案、诊断报告
This commit is contained in:
zqlit committed 2026-10-06 12:46:05 +08:00
1 parent d273330e10
commit c2f890a0f7
5 files changed
+790 -28

No files matched your search

+29 -25
View File
@@ -75,6 +75,28 @@ async function discoverFaviconUrl(siteUrl: string): Promise<string | null> {
}
}
/**
* favicon 图片响应的统一缓存头。
*
* ★ 为什么要显式加 s-maxage / CDN-Cache-Control:
* `Cache-Control: max-age=86400` 只让**浏览器**缓存,Cloudflare 边缘默认
* **不缓存 Worker 的响应**(实测响应里没有 cf-cache-status)。于是每个访客、
* 每次清缓存、每换设备,都会真正进 Worker → 第一条语句就是 KV.get → KV 读量被放大。
*
* 加上 s-maxage(共享缓存)后,CF 边缘可缓存 24h:边缘命中就完全不进 Worker,
* KV 读取 ≈ 0,同时首包更快。浏览器侧仍保留 max-age。
* 只对**成功拿到图片**的响应使用;失败/兜底响应不缓存(避免把错误固化一天)。
*/
function imageCacheHeaders(contentType: string): Record<string, string> {
return {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=86400, s-maxage=86400',
'CDN-Cache-Control': 'max-age=86400',
'Cloudflare-CDN-Cache-Control': 'max-age=86400',
'Access-Control-Allow-Origin': '*',
};
}
export async function favicon(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const siteUrl = url.searchParams.get('url');
@@ -87,14 +109,7 @@ export async function favicon(request: Request, env: Env): Promise<Response> {
if (cached) {
const [meta, b64] = cached.split('|');
const body = Uint8Array.from(atob(b64), (ch) => ch.charCodeAt(0));
return new Response(body, {
status: 200,
headers: {
'Content-Type': meta,
'Cache-Control': 'public, max-age=86400',
'Access-Control-Allow-Origin': '*',
},
});
return new Response(body, { status: 200, headers: imageCacheHeaders(meta) });
}
// 失败短缓存:抓取失败的站点 1 小时内不再反复打外站
@@ -150,14 +165,7 @@ export async function favicon(request: Request, env: Env): Promise<Response> {
}
await env.RSS_KV.put(bodyKey, `${contentType}|${btoa(bin)}`, { expirationTtl: CACHE_TTL });
}
return new Response(buf, {
status: 200,
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=86400',
'Access-Control-Allow-Origin': '*',
},
});
return new Response(buf, { status: 200, headers: imageCacheHeaders(contentType) });
}
} catch {
/* 落到第三方兜底 */
@@ -182,14 +190,7 @@ export async function favicon(request: Request, env: Env): Promise<Response> {
bin += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + chunk)) as unknown as number[]);
}
await env.RSS_KV.put(bodyKey, `${contentType}|${btoa(bin)}`, { expirationTtl: CACHE_TTL });
return new Response(buf, {
status: 200,
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=86400',
'Access-Control-Allow-Origin': '*',
},
});
return new Response(buf, { status: 200, headers: imageCacheHeaders(contentType) });
}
} catch {
/* 彻底失败 */
@@ -212,8 +213,11 @@ function letterSvg(siteUrl: string): Response {
return new Response(svg, {
status: 200,
headers: {
// 兜底图也缓存(否则每个访客都要重跑一遍抓取失败流程 = 3 次 KV 读 + 1 次写)
'Content-Type': 'image/svg+xml',
'Cache-Control': 'public, max-age=86400',
'Cache-Control': 'public, max-age=86400, s-maxage=86400',
'CDN-Cache-Control': 'max-age=86400',
'Cloudflare-CDN-Cache-Control': 'max-age=86400',
'Access-Control-Allow-Origin': '*',
},
});
+19 -3
View File
@@ -32,7 +32,7 @@ async function main() {
return;
}
const urlList = cdnUrlListStr
let urlList = cdnUrlListStr
.replace(/\n/g, ",")
.split(",")
.map((u) => u.trim())
@@ -43,7 +43,21 @@ async function main() {
return;
}
console.log(`Refreshing ${urlList.length} URLs...`);
// ★ 多吉云 rtype=path 是「刷新目录」,目录必须以 / 结尾:
// https://usj.cc → 只刷新首页这一个文件(旧行为,不是全站)
// https://usj.cc/ → 整站刷新
// 这里兜底补尾斜杠(只对裸域名生效,不碰带路径的 URL)。
urlList = urlList.map((u) => {
const bare = /^https?:\/\/[^/?#]+$/.test(u);
if (bare) {
console.log(`[warn] "${u}" 缺少结尾斜杠 —— 补成 "${u}/" 才能全站刷新`);
return u + "/";
}
return u;
});
console.log(`Refreshing ${urlList.length} URLs (rtype=path, 目录刷新)...`);
urlList.forEach((u) => console.log(` - ${u}`));
const result = await dogecloudApi("/cdn/refresh/add.json", {
rtype: "path",
@@ -51,7 +65,9 @@ async function main() {
});
if (result && result.code === 200) {
console.log(`Refresh task submitted. Task ID: ${result.data.task_id}`);
console.log(
`Refresh task submitted. Task ID: ${result.data.task_id}, count: ${result.data.count}`,
);
} else {
console.error(`Failed to submit refresh task. Response: ${JSON.stringify(result)}`);
process.exit(1);
+289
View File
@@ -0,0 +1,289 @@
# 函数版证书管家(CF Worker)实施方案
> **目标**:用 Cloudflare Worker 取代服务器上的 certimate,
> 申请 + 续期 + 部署证书(多吉云 CDN + 1Panel),把 certimate 这个服务精简掉。
**已确认的决策(2026-10-06)**
| 项 | 决定 |
|---|---|
| CA | **继续用 LiteSSL / freessl.cn**(与现状一致,90 天免费) |
| DNS-01 | **对接各家 DNS API,不改 DNS**(腾讯云 DNSPod + Cloudflare) |
| 部署目标 | **多吉云 CDN + 1Panel**(放弃又拍云) |
| 1Panel 接入 | **直连公网 `119.29.215.187:3721`** |
| 覆盖域名 | `usj.cc` / `t-t.live` / `200181.xyz`(三组,多域名) |
---
## 一、现状盘点(从服务器上的 certimate 导出)
### 1.1 三条「申请工作流」(2026-10-06 从 certimate SQLite 导出)
| 工作流 | cron | 域名 | CA | DNS | 部署 | 算法 |
|---|---|---|---|---|---|---|
| 优世界证书申请 | `10 12 * * *` | `*.usj.cc;usj.cc` | `litessl` | **tencentcloud-dns** | **dogecloud-cdn** + **1panel** | RSA2048 |
| 团团证书申请 | `05 12 * * *` | `t-t.live;*.t-t.live` | `litessl` | **tencentcloud-dns** | **1panel** + tencentcloud-eo | RSA2048 |
| 200181.xyz 证书申请 | `17 12 * * *` | `*.200181.xyz;200181.xyz` | `litessl` | **cloudflare** | **1panel** | RSA2048 |
另有 4 条「过期预警」工作流(`0 12 * * *`,纯监控发信):
优世界 / 伍比贰 / 优世界artalk评论 / 团团。
> ★ 与你的要求完全吻合:**多吉云 + 1Panel**。又拍云本来就没在这套工作流里。
> ★ `tencentcloud-eo`(腾讯云边缘安全加速)是 t-t.live 独有的第 3 个部署目标,
> 函数版第一版可不做(保持 1Panel 即可)。
### 1.2 1Panel 站点证书实况
| 站点 | 到期 | SAN |
|---|---|---|
| `artalk/openlist/openwrt/wifi/writeapi.usj.cc`、`vaultwarden` | Dec 7 2026 | `usj.cc,*.usj.cc` |
| `certd/pl/pwd/www.t-t.live`、`t-t.live` | **Oct 9 2026** | `*.t-t.live,t-t.live` |
| `ssh.200181.xyz` | Nov 23 2026 | `200181.xyz,*.200181.xyz` |
> 证书落地路径:`/1panel/1panel/www/sites/<域名>/ssl/fullchain.pem`
### 1.3 可复用的凭据(2026-10-06 从 certimate `access` 表导出)
| 用途 | 凭据名 | 结构 | 说明 |
|---|---|---|---|
| 多吉云 API | 多吉云账户 | `{accessKey, secretKey}` | **零新增**,已有同款 AK/SK |
| 腾讯云 DNS | 小赵腾讯云 / 团团腾讯云 | `{secretId: AKID..., secretKey}` | **CAM 密钥** → TC3-HMAC-SHA256 签名 |
| Cloudflare | CloudFare账户 | `{apiToken: ALTpx...}` | 200181.xyz 的 DNS + 部署 |
| LiteSSL | freessl.cn | `{eabKid, eabHmacKey(base64)}` | ★ **ACME EAB 凭据**,不是私有 API |
| ZeroSSL | zerossl | `{eabKid, eabHmacKey(base64)}` | 同上,可作 CA 备选 |
| 1Panel | 团团服务器面板 | `{apiKey: muuiR..., apiVersion: v2, serverUrl}` | 服务器面板 API |
| 1Panel | 本地 | `{apiKey: 41mRF..., apiVersion: v2, serverUrl}` | 本地面板 API |
| 邮件 | 小赵的邮件通知 | `{smtpHost, smtpPort:465, username, password}` | ★ SMTP,**Worker 用不了**(无 TCP) |
> ⚠️ **SMTP 在 Worker 里不可用** —— Cloudflare Workers 没有 TCP socket。
> 现有 artalk-cf worker 已改用 **Resend HTTP API**,证书管家复用同一套即可。
### 1.4 ★ 1Panel 的 IP 白名单 —— 实测**没有拦外网**
```
1panel-core 监听: *:3721 (所有接口)
外网访问测试: http://119.29.215.187:3721/api/v2/dashboard/base
→ HTTP 401(未授权,而非连接拒绝)
grep securityEntrance/allowIP → 空(未设白名单)
```
→ **Worker 直连 `119.29.215.187:3721` 技术上可行**,只需 API Key。
> 但这意味着**面板端口 3721 对公网开放**(靠 token 鉴权)。
> 方案 A(服务器侧拉取)仍更安全,见 §3.4。
---
## 二、架构设计
```
┌──────────────── Cloudflare Worker: cert-keeper ────────────────┐
│ │
│ [crons] 每天 12:00 一行:检查所有域名的证书剩余天数 │
│ ↓ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 1. 到期检查 剩余 > 30 天 → 直接退出(不打扰) │ │
│ │ 2. ACME 申请 对新域名/即将到期域名走 DNS-01 │ │
│ │ · freessl.cn API(litessl,与现状一致) │ │
│ │ 3. 得证书 → 存 KV(cert:<domain>) │ │
│ │ 4. 部署: │ │
│ │ a) 多吉云 CDN /cdn/cert/upload + bind │ │
│ │ b) 1Panel API 直连 119.29.215.187:3721 │ │
│ │ 5. 邮件通知(成功/失败) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ [HTTP] 手动触发 /admin/cert/check (带鉴权) │
│ /admin/cert/list 查看所有证书状态 │
└─────────────────────────────────────────────────────────────────┘
```
### 2.1 域名配置(KV 存储,可动态加域名)
```jsonc
{
"domains": [
{
"name": "usj.cc",
"san": ["usj.cc", "*.usj.cc"],
"dns": "tencent",
"deploy": ["dogecloud", "1panel"],
"dogecloud_domains": ["usj.cc"], // 绑到多吉云 CDN 的域名
"1panel_sites": ["artalk.usj.cc", "openlist.usj.cc", "..."]
},
{
"name": "t-t.live",
"san": ["t-t.live", "*.t-t.live"],
"dns": "tencent",
"deploy": ["1panel"],
"1panel_sites": ["t-t.live", "www.t-t.live", "certd.t-t.live", "..."]
},
{
"name": "200181.xyz",
"san": ["200181.xyz", "*.200181.xyz"],
"dns": "cloudflare",
"deploy": ["1panel"],
"1panel_sites": ["ssh.200181.xyz"]
}
]
}
```
---
## 三、各环节实现要点
### 3.1 ACME 申请(LiteSSL / freessl.cn)—— ★ 已确认是标准 ACME + EAB
**关键发现(2026-10-06 从 certimate 凭据表导出)**:
certimate 里 `freessl.cn` 凭据的字段是 **`eabKid` + `eabHmacKey`**,
这是 **ACME 的 External Account Binding**,**不是** freessl 私有 API。
```
freessl.cn 凭据: { eabKid: "T2G-o***", eabHmacKey: "Qk9Yg***"(base64) }
zerossl 凭据: { eabKid: "I5tkZ***", eabHmacKey: "FHnpZ***"(base64) }
```
→ **说明 certimate 走的是标准 ACME 协议**(RFC 8555),只是 CA 端要求 EAB 绑定。
**这大幅降低实现难度**:
| 步骤 | 实现 |
|---|---|
| 目录 | `GET https://acme.trustasia.com/acme/directory`(LiteSSL 的 ACME 端点,需实测确认) |
| 账号 | `newAccount`,带 **EAB**:`eab = { kid, hmacKey }` |
| 签名 | **JWS + ES256/RS256**,用 WebCrypto(CF Worker 原生支持) |
| 挑战 | DNS-01(写入 `_acme-challenge.<domain>` TXT) |
| 完毕 | `finalize` → 轮询 → 下载 `fullchain.pem` |
> **好消息**:既然是标准 ACME,开源参考 `manalejandro/cf-acme`、`PIKACHUIM/CFWorkerACME`
> 可以**几乎直接复用**,主要工作变成「加 EAB 支持」+「换成 LiteSSL 目录地址」。
> 且 CA 可做成「可切换」(LiteSSL / ZeroSSL / Let's Encrypt 同一个实现,只换 directory + EAB)。
**仍需实测确认的**:LiteSSL 的 ACME directory URL。可从 certimate 的
`workflow.graphContent` 里 grep 出来(那里存了 CA 配置)。
### 3.2 DNS-01 验证
**腾讯云 DNSPod**(usj.cc / t-t.live):
```
POST https://dnsapi.cn/Record.Create # 新建 TXT
POST https://dnsapi.cn/Record.Delete # 删除 TXT
认证:login_token = "<ID>,<Token>" (dnspod 老 API,简单)
```
> 需确认 certimate 里那两条 tencentcloud 凭据用的是 **DNSPod token** 还是 **CAM 密钥**。
> 若是 CAM(TC3-HMAC-SHA256 签名),则有现成的签名实现可复用。
**Cloudflare**(200181.xyz):
```
POST /client/v4/zones/:zone_id/dns_records # 建 TXT
DELETE /client/v4/zones/:zone_id/dns_records/:id
认证:Bearer <CLOUDFLARE_API_TOKEN>(已有)
```
### 3.3 部署到多吉云 CDN
```http
POST https://api.dogecloud.com/cdn/cert/upload.json
note=<备注>&cert=<fullchain>&private=<私钥>
→ data.id(证书ID)
POST https://api.dogecloud.com/cdn/cert/bind.json
id=<证书ID>&domain=usj.cc
```
认证:**HMAC-SHA1 签 `apiPath + "\n" + body`**,与现有 `scripts/refresh_cdn.js` 完全一致 ——
**可以把它拆出来复用代码**。
> 多吉云证书 API 参数名注意:官方文档写 `privary`(疑似笔误),实际要传 `private`。
> 上手前需实测一次确认。
### 3.4 部署到 1Panel(直连 119.29.215.187:3721)
1Panel v2 API 认证:
```
1Panel-Token: md5('1panel' + <APIKey> + <unix秒>)
1Panel-Timestamp: <unix秒>
Base: http://119.29.215.187:3721/api/v2
```
**⚠️ 最大障碍:IP 白名单。**
1Panel 服务端逐层校验「时间戳 → token → **IP 白名单**」。
Cloudflare Worker 的出口 IP 是**海量动态 IP**,无法加白名单。
**可选解法(按推荐序)**:
| 方案 | 说明 | 评价 |
|---|---|---|
| **A. 服务器侧拉取**(推荐) | Worker 只存证书到 KV;服务器上一个 cron 脚本 `curl` 拉最新证书 + 重载 nginx。**不需要开放 1Panel 端口给公网** | ✅ 最安全,绕开白名单 |
| B. 逆向拿 Worker 出口 IP | 不可行(CF 出口 IP 段巨大且变动) | ❌ |
| C. 自建小代理 | 在服务器上跑一个 30 行的反代,Worker → 代理 → 1Panel | ⚠️ 等于 A 但更绕 |
| D. 关闭 1Panel IP 白名单 | 直接放开面板端口给公网 | ❌ 危险,不建议 |
> **推荐 A**:既能满足「函数版管家」的自动化意图,又不必把面板暴露到公网。
> 服务器上只需一个 `cert-sync.sh`(wg 或 SSH 进来装一次),
> 效果与 certimate 的 1panel deploy 完全等价。
### 3.5 存储与通知
- **KV**:`cert:<domain>` → `{ cert, key, expireAt, updatedAt }`;`config` → 域名配置
- **邮件**:Worker 无 TCP socket,**不能直连 SMTP**。
现有 worker 已用 **Resend**(HTTP API),复用同一套:
`POST https://api.resend.com/emails`,发到 `177018615@qq.com` / `1770186415@qq.com`
- **crons**:`[triggers] crons = ["30 4 * * *"]`(UTC 4:30 = 北京 12:30)
---
## 四、落地路径(分阶段,可随时叫停)
### Phase 1 —— 只读监控(零风险)
- 部署 Worker,`crons` 每天检查三个域名的证书剩余天数
- 到期 < 30 天 → 发邮件提醒
- **完全不写任何东西**,纯观察。可以先跑两周验证数据准确性。
### Phase 2 —— 接管续期(+ 多吉云自动部署)
- 实现 ACME(Let's Encrypt 标准协议优先)
- DNS-01 走腾讯云 + Cloudflare
- 自动部署到**多吉云 CDN**(有正规 API,风险低)
- 邮件通知结果;**1Panel 仍由 certimate 管**
### Phase 3 —— 接管 1Panel + 关停 certimate
- 服务器加 `cert-sync.sh`(从 Worker 拉证书 + reload)
- 观察一个完整续期周期(90 天)
- 确认无误 → **关停 certimate 容器**
---
## 五、开工前必须确认/实测的 4 件事
1. **freessl.cn API 是否可用**(认证 + 申请 + 下载全流程)
→ 若不行,改 **Let's Encrypt 标准 ACME**(推荐这条路,更稳)
2. **certimate 里 tencentcloud 凭据的类型**
(DNSPod token 还是 CAM 密钥?决定签名实现)
3. **多吉云 `cdn/cert/upload.json` 的参数名**(`private` 还是 `privary`)
4. **1Panel 侧走「服务器拉取」还是「直连面板」**(强烈建议前者)
---
## 六、现成可参考的开源实现
| 项目 | 用途 |
|---|---|
| `manalejandro/cf-acme` | CF Workers + KV,用 WebCrypto 实现标准 ACME,DNS-01 自动申请 |
| `PIKACHUIM/CFWorkerACME` | CF Worker 上的 SSL 申请代理,支持 LE / ZeroSSL / GTS |
| `Menci/deploy-certificate-to-upyun` | 又拍云部署(证明其**无公开 API**,故本方案放弃又拍云) |
| 本项目 `scripts/refresh_cdn.js` | 多吉云 HMAC-SHA1 签名,**直接复用** |
---
## 七、与 certimate 的能力对照
| 能力 | certimate(现状) | 函数版管家 |
|---|---|---|
| 申请证书 | ✅ | ✅ |
| DNS-01 | ✅ 腾讯云 + CF | ✅ 同 |
| 部署多吉云 | ✅ | ✅ |
| 部署 1Panel | ✅ | ✅(走服务器拉取) |
| 部署又拍云 | ❌(本来就没配) | ❌(放弃) |
| 需要服务器 | **✅ 需要** | ❌ **不需要** |
| 加域名 | 网页点点 | 改 KV JSON |
| 通知 | 邮件 | 邮件 |
| 成本 | 服务器 | **0**(CF 免费额度) |
+121
View File
@@ -0,0 +1,121 @@
# 评论区孤儿评论修复方案(2026-10-06)
> ## ✅ 状态:**已执行完成**(2026-10-06 12:2x)
>
> - 21 条 UPDATE 全部成功,**816 条评论已迁回正确页面**
> - 22 个目标 key 条数精确到位;残留老 key 已清空
> - 端到端验证通过:`/20230424.html`(苏州一日游)已能显示评论
> - 备份:`.editor-tmp/BACKUP_816条_20261006.txt`
> - 未迁移的 13 个 key / 377 条保持原位未动
## 一句话结论
**你的评论一条都没丢。** 线上 D1 里有 **3423 条评论**,
其中 **1193 条**因为老站 URL「日期错了一天」变成了孤儿(挂在 404 页面上),
**816 条**可以精确找回,**377 条**因现站已无对应文章而保留原样。
---
## 一、线上库真实基线
```
comments 表:3423 条(存活 3380 条)
id 范围:10393 ~ 15674
94 个 page_key:
· /comment.html 留言板 1001 条
· 65 个「老站日期型」key(/20230423.html) 1975 条
· 28 个「新站时间戳型」key(/20260708110221.html) 404 条
```
**你举例的 `/20230424.html`(苏州一日游)有 40 条评论 —— 找到了,挂在 `/20230423.html` 上。**
---
## 二、问题成因
老站迁移时,**部分文章的 URL 日期错位了一天**:
| | 错误 URL(有评论) | 正确 URL(无评论) |
|---|---|---|
| 苏州一日游 | `/20230423.html`(40 条,title 显示 "404 Page not found") | `/20230424.html`(页面正常,0 条) |
| 新的开始 | `/20230527.html`(76 条,title=404) | `/20230528.html`(页面正常,0 条) |
- 评论**绑在**老 URL 上
- 页面**只认**新 URL(老 URL 返回 302 → /404.html)
- 所以评论「有数据但显示不出来」
---
## 三、修复方案(待批准执行)
**23 个孤儿 key / 816 条** → 改挂到现站正确 slug。
`UPDATE comments SET page_key='/新key' WHERE page_key='/老key'`(**按 page_key 定位,不用 id**)
| 孤儿 key | 条数 | → 目标 | 文章标题 |
|---|---|---|---|
| `/20230527.html` | 76 | `/20230528.html` | 新的开始 |
| `/20220810.html` | 58 | `/20220811.html` | typecho实现QQ头像用户评论加密 |
| `/20220906.html` | 57 | `/20220907.html` | 主力机由荣耀20切换到iPhone13 |
| `/20221102.html` | 52 | `/20221103.html` | 毕业篇:最后几个月的大学生活 |
| `/20221203.html` | 50 | `/20221204.html` | 2022·襄阳第一场雪 |
| `/20220227.html` | 48 | `/20220228.html` | 2022,除夕过后的那些事 |
| `/20211121.html` | 46 | `/20211122.html` | 这组照片的主题,咱就叫它光吧 |
| `/20220430.html` | 40 | `/20220501.html` | iphone快捷指令发布动态说说(合并) |
| **`/20230423.html`** | **40** | **`/20230424.html`** | **苏州一日游 ★** |
| `/20220503.html` | 34 | `/20220501.html` | iphone快捷指令发布动态说说(合并) |
| `/20211222.html` | 33 | `/20211223.html` | 放假之前最后一次的照片合集 |
| `/20230803.html` | 32 | `/20230804.html` | 家乡随拍 |
| `/20230125.html` | 31 | `/20230126.html` | 感谢哥哥给的网站 |
| `/20220619.html` | 30 | `/20220620.html` | 我跳绳的那些日子 |
| `/20221016.html` | 28 | `/20221017.html` | 毕业纪念篇:图书馆 |
| `/20220104.html` | 26 | `/20220102.html` | 祝大家元旦快乐 |
| `/20211113.html` | 24 | `/20211114.html` | 双十一已经变味了 |
| `/20230409.html` | 22 | `/20230410.html` | 四月随笔 |
| `/20211210.html` | 21 | `/20211211.html` | 盘点注册的域名 |
| `/20211007.html` | 19 | `/20211008.html` | 记录人生第一次洗牙 |
| `/20210911.html` | 19 | `/20210912.html` | 别让抖音支配了你的大学生活 |
| `/20221213.html` | 16 | `/20221214.html` | Apple Watch Series7 体验 |
| `/20230716.html` | 14 | `/20230717.html` | 襄阳唐城 |
✅ **已核实:18 个目标页当前评论数全部为 0** —— 不会覆盖、不会冲突。
---
## 四、不迁移的(377 条,保留原样)
现站已无对应文章,评论**保留在原位不删**(万一以后想恢复旧文章,评论还在):
| 孤儿 key | 条数 | 内容主题 |
|---|---|---|
| `/20220115.html` | 54 | 开发 app / uniapp |
| `/20220410.html` | 51 | iPhone 更新 |
| `/20211117.html` | 47 | Twitter 主题界面 |
| `/20211111.html` | 43 | 网站头像插件 |
| `/20211203.html` | 32 | 人像摄影 |
| `/20220328.html` | 32 | 青年大学习 |
| `/20211124.html` | 30 | RSS 阅读器(蚁阅) |
| `/20260724101237.html` | 24 | 流量卡/广电(现站 slug 冲突) |
| `/20211024.html` | 20 | 医疗/手术 |
| `/20211017.html` | 17 | 弟弟听力/同济医院 |
| `/20211015.html` | 14 | 恋爱话题 |
| `/20220607.html` | 11 | 腾讯游戏/王者 |
| `/202610011226.html` | 2 | Gitea 测试文 |
---
## 五、执行方式
```bash
# 1. 备份(先把这 816 条 dump 出来)
# 2. 执行 21 条 UPDATE(按 page_key 定位)
# 3. 核对:目标 slug 评论数 = 预期值;总量仍为 3423
```
SQL 在 `.editor-tmp/FINAL_SQL.json`。
---
## 六、顺带修掉的两个坑
1. `scripts/refresh_cdn.js` —— 多吉云 `rtype=path` 目录刷新**必须带尾斜杠**(已加兜底 + 日志)
2. `blog-admin/src/routes/rss/tools.ts` —— 加 `s-maxage` / `CDN-Cache-Control`,让 CF 边缘真正缓存
+332
View File
@@ -0,0 +1,332 @@
# 四问诊断报告(2026-10-06)
一次性回答四个问题:CDN 刷新、KV 配额、D1 评论对账、certimate 迁移。
---
## 一、多吉云刷新 & 已删文章还能访问
### 1.1 先更正我自己的话
我一开始说「多吉云那条已经是全量(`--all`)」——**这句话是错的**,实测证据如下:
```
cnb-secrets.yml:33 CDN_URL_LIST: "https://usj.cc"
```
`scripts/refresh_cdn.js` 只做一件确定性的事:把 `CDN_URL_LIST` 按逗号/换行拆开,
逐条调 `/cdn/refresh/add.json`(`rtype: "path"`)。**当前配置里这个变量只有首页一个 URL**。
结论:**多吉云确实不是全量刷新,只刷新了首页 `https://usj.cc`。**
### 1.2 那为什么删掉的文章还能访问?
不是多吉云的问题。实测这条 URL:
```
$ curl -sI https://usj.cc/202610021614.html
HTTP/1.1 200 OK
Server: marco/3.2 ← 又拍云特征头
X-Cache-Lookup: Hit From Upstream Cluster ← 命中缓存
Last-Modified: Sun, 04 Oct 2026 14:12:26 GMT ← 旧副本时间
Cache-Control: max-age=691200 ← 8 天 TTL
Age: 131684 ← 已缓存约 1.5 天
```
判别实验(关键):
| URL | 状态 | 说明 |
|---|---|---|
| `/202610021614.html`(已删除) | **200** | 缓存里还有旧副本 |
| `/zzz-not-exist-111.html`(从未存在) | **302 → /404.html** | 源站没这个文件 |
两者行为不同 ⇒ 说明**源站确实已经没有这个文件了**(本地 `content/` 和 `public/` 均已确认无此页),
现在返回 200 完全是**又拍云上的 8 天旧副本**。
### 1.3 根因:两条 CDN 线路都刷不到「已删除页」
`scripts/purge_list.js` 的逻辑是:遍历 **构建产物 `public/`**,把里面所有 `.html` 列出来提交刷新。
```js
// 全量时 walkHtml('public') —— 遍历构建产物
// 已删除的页面自然不在里面 → 永远不会被提交刷新
```
也就是说:**刷新清单只会包含「现在存在的页面」**。一个页面被删掉后,
它就从清单里消失了,于是 CDN 上的旧副本只能等 TTL 自然过期。
- 又拍云:HTML `max-age=691200` = **8 天**
- 多吉云:只刷首页,问题更明显
### 1.4 处置建议
| 方案 | 说明 | 成本 |
|---|---|---|
| **A. 什么都不做** | 等 8 天自然过期 | 0 |
| **B. 手动刷新一次**(推荐) | 把已知的已删 URL 提一次刷新,立刻生效 | 一次性 |
| C. 流水线加「删除清单」 | 构建时对比上一次产物,把消失的 `.html` 追加进 purge 清单 | 改脚本 |
对已删文章这种低频事件,**B 足够**。需要的话我可以立刻把已知的那几个已删 URL 提交到又拍云+多吉云。
---
## 二、KV 配额告警:需要调整吗?
### 2.1 告警内容
> 已使用 Workers KV 免费套餐每日限制的 **50%**,超限将返回 429;重置时间 2026-10-06 00:00 UTC。
> 免费额度:读 100,000/天、写 1,000/天、删 1,000/天、list 1,000/天。
### 2.2 谁在吃配额?(实测 4 类来源)
| 来源 | 代码位置 | 频率 | 影响 |
|---|---|---|---|
| **评论数缓存** | `comments.ts:180/206` | 每次评论列表请求,miss 时写(TTL 600s) | 读 + 写 |
| **评论数版本号** | `comments.ts:53/61` | 评论增删时写 | 写 |
| **friend-link favicon** | `routes/rss/tools.ts:86` | **每次访问友链/友圈页,每个友链一次** | **读(大户)** |
| 人机验证 / 通知节流 | `human.ts` / `admin-notify.ts` | 低 | 少量 |
**读数最大的就是 favicon 接口。** 友链共 54 条(可见 42 条),其中 15 条头像走 `/api/favicon`:
```js
// circles.html / linkify.js
img.src = RSS_API_BASE + '/api/favicon?url=' + encodeURIComponent(target);
```
每刷一次友圈/友链页 → 触发 N 次 `/api/favicon` → 每次至少 1 次 KV `get`。
favicon 本体有 24h 浏览器缓存,但**首访、清缓存、换设备都会重新打**。
### 2.3 建议:**暂时不用调整,但建议做一个小优化**
- **读 50% 是「接近」不是「超了」**,且每天 UTC 0 点清零。按当前站点的访问量,正常不会打穿。
- 即使打穿,后果是 `/api/favicon` 走兜底字母图、评论数退化为实时查库——**不是灾难性故障**。
- 免费版写限额 1,000/天,比读更容易被评论数缓存写穿。真正的风险点是**评论突然暴涨**或**有人刷评论**。
**可做的小优化(成本低、收益直接):**
1. favicon 接口响应已经带了 `Cache-Control: max-age=86400`,可以加一层 **Cloudflare Cache Rule**,
让 `/api/favicon*` 在边缘缓存 24h,这样 KV 读基本归零。
2. 若担心写限额,把 `cml:ver:` 的写入合并/延迟(比如 5 分钟内同页重复 bump 只写一次)。
**要不要升级到付费($5/月)?** 目前**不需要**。等到真收到「已超限」而不是「接近」的邮件再说。
---
## 三、D1 评论对账:老文章评论为什么没了
### 3.1 先说结论(这个和你想的不一样)
原库 **3979 条**评论 / 线上 **3423 条**。乍看差 556 条,但拆开看,**「缺失」的绝大部分根本不是评论**:
| 项 | 数量 |
|---|---|
| 线上缺失合计 | **711 条 / 63 页** |
| 其中 **点赞型记录**(内容含 `[LIKE]`) | **494 条(69%)** |
| → **真实评论型缺失** | **217 条** |
**关键发现:线上 `[LIKE]` 记录为 0 条。**
```
$ SELECT SUM(CASE WHEN content LIKE '%[LIKE]%' THEN 1 ELSE 0 END) FROM comments;
→ 0
```
说明当年数据导入时,**已经主动过滤掉了所有点赞型记录**——这是正确的设计(点赞不该占用评论表)。
所以拿原库和线上直接比条数,本身就会假性多出 494 条。
### 3.2 缺失的 217 条真实评论,按站点拆
| 站 | 缺失 | 其中点赞 | 真实 |
|---|---|---|---|
| 伍比贰 | 521 | 431 | **90** |
| 优世界 | 101 | 0 | **101** |
| 演示站 | 48 | 41 | 7 |
| 往后 | 41 | 22 | 19 |
**注意:「伍比贰」「演示站」「往后」是另外的站点,不属于 usj.cc。** 它们的数据本来就不该进本站。
### 3.3 「优世界」那 101 条对得上现站吗?
逐页核对,结果是 **一页都对不上**:
| page_key | 条数 | 现站情况 |
|---|---|---|
| `/yl` | 48 | 页面不存在(是 2022 年的旧友链页) |
| `/20241018-some-useful-win-tools` | 12 | 文章还在,但 slug 已改为 `20241019`,且**是草稿**(未发布) |
| `/27`、`/13`、`/m/17` 等 | 41 | 不存在(旧站的分页/短链) |
| `/20240921-clock-in` | 5 | 文章已删除 |
**根因不是「key 对不上」,而是这些文章本身已经不存在了。**
评论是挂在「已删除文章」上的,key 再怎么规范化也接不回来。
### 3.4 站上现存页面 vs 线上 —— 有没有真的错位?
**没有。** 用现站 284 个 slug 逐一匹配线上缺失的 63 个 key:
```
能对上现存页面: 1 页 / 4 条 (只有 /about.html)
对不上 : 62 页 / 707 条
```
**唯一一个疑似错位:`/about.html`(4 条)**——但深挖后发现**它也不需要修**:
```
id=14229 /about.html [伍比贰] 2026-02-07 👍 已点赞 Cool [LIKE]
id=14268 /about.html [伍比贰] 2026-02-12 👍 已点赞 Interesting [LIKE]
id=14269 /about.html [伍比贰] 2026-02-12 👍 已点赞 很棒的文章! [LIKE]
id=14276 /about.html [伍比贰] 2026-02-13 👍 已点赞 写得很好 [LIKE]
```
这 4 条全是**点赞型记录**,而且属于**「伍比贰」站**,本就不该进本站。
→ **结论:线上 D1 没有任何错位,完全不需要修复。**
### 3.5 线上「多出」的 7 页是什么?
| page_key | 条数 |
|---|---|
| `/20260630162329.html` | 25 |
| `/20260724101237.html` | 24 |
| `/20211001.html` | 14 |
| `/20261005113439.html` | 13 |
| `/20260629223659.html` | 10 |
| `/202610011226.html` | 2 |
| `/20261005213010.html` | 2 |
这些是**导出 db 之后新增的评论**(2026-06 之后),属于正常增长,不用动。
### 3.6 修复建议
**结论:线上 D1 不需要做任何修复。**
你的直觉「很多评论没跟页面 key 对应上、老文章没评论了」——
真实情况是:**那些评论对应的文章,本身已经不在了**(已删除 / 仍是草稿 / 属于别的站),
key 再怎么规范化也接不回来。线上数据是**干净的**。
**不建议做的**:
- ❌ 批量把「伍比贰/演示站/往后」的评论导进来 —— 那是别的站的数据(含大量点赞记录)
- ❌ 把 `/yl`、`/27` 等改成别的 key —— 没有正确的目标,改了反而污染
- ❌ 恢复 494 条点赞记录 —— 线上设计本就不存点赞(`[LIKE]` 记录当前为 0)
**如果你更看重「把老评论展示出来」**,唯一的正路是**重建这些页面**
(把已删文章从 git 历史恢复,或用 slug 重定向)。这个要单独评估,工作量比改 key 大得多。
**但请注意**:即使恢复了页面,能救回的也只是极小一部分——
「优世界」站真实缺失的 101 条里,48 条属于 `/yl`(2022 年旧友链页),
41 条属于 `/27` `/13` `/m/17` 等旧站分页短链,只有 12 条是正经文章评论。
### 3.7 那「评论少了」的体感从哪来?
很可能来自这两点(都正常):
1. **点赞不再算评论**:原库 494 条点赞在导入时被过滤,前端看到的评论数自然变少。
2. **老文章页面已删**:文章没了,评论区自然也没了。
如果确实想提升「评论数」,更实际的做法是**在新文章上做引导**,而不是抢救旧数据。
---
## 四、certimate 能否迁到 Cloudflare
### 4.1 你现在的架构
```
certimate(跑在某台服务器上)
├─ 申请:litessl CA,*.usj.cc + usj.cc,DNS-01,tencentcloud-dns
├─ 部署:dogecloud-cdn(多吉云 CDN)
└─ 部署:1panel website(那台服务器上的站点)
定时:10 12 * * *(每天 UTC 12:10)
失败邮件:177018615@qq.com
```
当前线上证书实测:
```
issuer = LiteSSL RSA CA 2025(TrustAsia)
subject = CN=usj.cc
SAN = usj.cc, *.usj.cc
有效期 = 2026-09-08 → 2026-12-07
```
### 4.2 certimate 支持 Cloudflare 吗?
**支持,而且支持两种角色:**
| 角色 | 支持 | 说明 |
|---|---|---|
| **DNS provider(申请用)** | ✅ | 填 Cloudflare API Token 即可,用于 DNS-01 验证 |
| **Deploy provider(部署用)** | ✅ | 可将证书部署到 Cloudflare |
(certimate 官方:60+ DNS 托管商、120+ 部署目标,Cloudflare 在列。)
### 4.3 你的真实目标:「把服务器干掉」
需要先厘清一件事:**usj.cc 现在到底谁在提供 HTTPS?**
从实测响应头看:
```
Server: marco/3.2 ← 又拍云
Via: T.206.M, V.403-zj-fud-202, ... ← 又拍云多级节点
```
**对外服务的是又拍云 CDN**,不是那台服务器。那台服务器承担的是:
- 跑 certimate(证书申请+分发)
- 1Panel 上的 write-server / editor-api 等自建服务
所以「干掉服务器」要分清两件事:
**① 只干掉「证书维护」这件事** → **完全可以,用 Cloudflare 托管 DNS + certimate 的 CF provider。**
路径:
1. 把 `usj.cc` 的 **DNS 托管迁到 Cloudflare**(当前在腾讯云 DNSPod)
2. certimate 里把「DNS 提供商」从 `tencentcloud-dns` 换成 `cloudflare`(填 CF API Token)
3. 证书申请照旧(litessl 或换 Let's Encrypt),DNS-01 走 CF API
4. 部署目标改为「Cloudflare」(如果把站点也迁到 CF)或保留又拍云/多吉云
**② 连「站点托管」也迁到 Cloudflare** → **可行但有取舍:**
| 优点 | 缺点 |
|---|---|
| CF 免费版自带 Universal SSL(免申请免续期) | 国内访问速度**不如又拍云/多吉云**(CF 免费版节点在境外) |
| 不用再管 90 天续期 | 免费版 Universal SSL 只覆盖 `example.com` + `*.example.com` |
| 有 CF Origin CA(15 年有效期)给回源用 | 想用 CF 自签源站证书需另配 |
### 4.4 ⚠️ 关键提醒:国内访问速度
你的读者主要在国内。**又拍云/多吉云是国内 CDN,Cloudflare 免费版是境外节点**——
如果为了省一台服务器而把整个站点切到 CF,**国内访问大概率变慢**(首包延迟从几十 ms 变成几百 ms)。
**所以我建议:**
- ✅ **可以做的**:把 `usj.cc` 的 DNS 托管迁到 Cloudflare(CF DNS 免费、管理方便、API 完善),
certimate 用它做 DNS-01,证书照旧签发后部署到**又拍云 + 多吉云**。这样证书维护不再依赖服务器上的 1Panel 部署环节。
- ⚠️ **建议保留又拍云/多吉云做 CDN**,不要为了「干掉服务器」把站点也搬到 CF。
- ❌ 如果那台服务器还跑着 write-server / editor-api,**那就不能关**——关之前先确认没有别的服务在跑。
### 4.5 你现在到底能不能关服务器?
需要先回答这个问题:**那台服务器上除了 certimate,还跑着什么?**
- 如果只有 certimate → 迁完就能关 ✅
- 如果还有 write-server / editor-api(按记忆应该有)→ **关不掉** ⚠️
建议你在关之前先列一下那台机器上的服务清单。需要的话我可以帮你连上去盘一遍。
---
## 附:本次用到的排查命令
```bash
# CDN 缓存判别
curl -sI https://usj.cc/202610021614.html | grep -iE "server|age|last-modified|x-cache"
# D1 查询(curl 走 IPv4,Node fetch 会 ENETUNREACH)
curl -s -X POST \
"https://api.cloudflare.com/client/v4/accounts/$ACC/d1/database/$DB/query" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"sql":"SELECT page_key, COUNT(*) FROM comments GROUP BY page_key"}'
# 线上是否有点赞记录
# → SELECT SUM(CASE WHEN content LIKE '%[LIKE]%' THEN 1 ELSE 0 END) FROM comments; → 0
```