架构概览
- 入口:
worker.js负责路由分发、SPA 回退、静态资源缓存头与安全响应头 - API 实现:
functions/_lib/api-core.js(文章 / 评论 / 统计 / 认证 / 设置 / RSS / Sitemap / Webmention / 预览 / 错误日志)、functions/_lib/{analytics,backup,subscribe,relations,search,popular,og,media,music}.js与functions/api/ai/*.js(Workers AI) - Pages 兜底:
functions/api/[[path]].js让 Pages 侧未知/api/*也返回 JSON 404,绝不回退 HTML - 兼容性日期:
2025-02-01
前提条件
- 注册 Cloudflare 账号
- 安装 Wrangler CLI:
npm install -g wrangler
步骤一:创建资源
登录
创建 D1 数据库
database_id(UUID 格式,带横线)。注意它不是数据库名,也不是 KV 的 id。
创建 KV 命名空间
id(32 位十六进制)。
KV 不是文章存储,而是限流与去重设施(评论 5 条/分钟、点赞 10 次/分钟、浏览 30 次/分钟、点赞 30 天去重、浏览 1 小时去重、AI 用量与缓存)。未绑定时这些限制会静默失效(日志里有 warning)。
步骤二:配置 GitHub Secrets
在仓库 Settings → Secrets and variables → Actions → Secrets 中添加(不要加到 Variables,工作流只读 Secrets):
逐项的获取位置与格式说明见 GitHub Secrets 配置,完整的新手教程见 CLOUDFLARE_SETUP_GUIDE.md。
步骤三:部署
推送到main 分支(或手动 Run workflow),GitHub Actions 依次执行:
- ✅ Checkout + 安装 Node 24
- ✅ 安装 Wrangler:默认
wrangler@3.90.0;若配置了BLOG_RATE_LIMIT_BINDING,改为wrangler@^4.36.0(该绑定要求 4.36+) - ✅ 运行三套测试(
smoke-test.js158 例 /gb-verify.js18 例 /search-verify.js25 例,失败即中止不部署) - ✅ 校验必要 Secrets 齐全(
BLOG_KV_ID/BLOG_D1_ID/CLOUDFLARE_API_TOKEN/CLOUDFLARE_ACCOUNT_ID) - ✅ 生成
wrangler.workers.ci.toml:先校验 ID 格式(BLOG_KV_ID必须是 32 位十六进制、BLOG_D1_ID必须是 UUID),再用脚本显式替换{env.BLOG_KV_ID}与{env.BLOG_D1_ID};{env.SITE_URL}/{env.CF_ZONE_ID}缺失时替换为空串(安全降级,不留字面量占位符) - ✅ 执行 D1 迁移(
migrations/*.sql按文件名升序,schema_migrations记账表 + 列预检 + 报错兜底三层幂等,见迁移脚本) - ✅ 部署 Worker(
wrangler deploy --config wrangler.workers.ci.toml) - ✅ 写入运行时 Secret:
BLOG_ADMIN_SETUP_KEY、BLOG_WRITE_TOKEN、CF_API_TOKEN、R2、Resend、BLOG_ADMIN_EMAIL、COMMENT_BLOCKLIST、BLOG_PREVIEW_SECRET、BLOG_AI_ENABLED/BLOG_AI_PUBLIC(所有可选值都是非空才写入)
手动触发时工作流会额外跑一步
wrangler whoami,便于定位凭证问题。改完任何 Secret 都必须重新运行一次部署,GitHub Secret 的修改不会自动同步到已部署的 Worker。步骤四:首次设置密码
访问https://你的域名/admin:
- 配置了
BLOG_ADMIN_SETUP_KEY(推荐):页面点「首次部署?使用安装密钥初始化」,填新密码(至少 8 位)+ 安装密钥。在初始化之前,登录接口一律返回 403,别人抢不走管理员账号 - 未配置:首次登录会自动生成随机默认密码(格式
xxxx-xxxx,标记must_change)并直接签发会话,登录后请立刻改密。存在先到先得竞态,全新部署建议配置安装密钥
Wrangler 配置
Workers (wrangler.workers.toml)
可选:边缘限流绑定
wrangler.workers.toml 里没有启用 [[ratelimits]](默认关闭)。该绑定需要 wrangler ≥ 4.36,且并非所有账户都能部署,所以采用「按需注入」:
计数器按 Cloudflare 数据中心本地缓存、最终一致,定位是「防滥用」而非精确计费。被拦下的请求不查库也不写库。
静态资源缓存策略
Worker 对静态响应统一设置Cache-Control:
API 侧另有自己的策略:只读接口
public, s-maxage=60, stale-while-revalidate=300(RSS / Sitemap 为 s-maxage=300),写接口与评论列表一律 no-store。
发布即生效的闭环:只读接口带
Cache-Tag(posts / feed / sitemap / post:<id> / stats:<id>),文章增删改后 Worker 会调 Cloudflare Cache Purge API 按标签清除。需要同时配置 CF_ZONE_ID([vars])与 CF_API_TOKEN(工作流自动写入,取 CLOUDFLARE_API_TOKEN 的值,Token 需含 Zone·Cache Purge 权限)。Pages (wrangler.toml)
[ai] / [assets] / main 都是 Workers 专属配置,Pages 配置里没有;functions/api/[[path]].js 则是 Pages 侧的兜底,让未知 /api/* 也返回 JSON 404(public/_redirects 的 SPA 回退本来会把它们兜成 HTML 200)。Workers AI(推理)
AI 功能由 Cloudflare Workers AI 提供(默认模型@cf/meta/llama-3.2-3b-instruct),按 Neurons 计费,每天约 10,000 Neurons 免费额度(约数千次摘要级调用):
- 绑定点:
wrangler.workers.toml中的[ai] binding = "AI"(部署时自动创建;不要在控制台手动重复绑定) - 关闭开关:环境变量
BLOG_AI_ENABLED设为0/false/off可整体禁用;BLOG_AI_PUBLIC设为同样值可禁止匿名访客生成摘要(登录后仍可用),防止被任意脚本刷走免费额度 - 这两个变量由部署工作流在非空时自动写入 Worker;也可以手动执行
npx wrangler secret put BLOG_AI_ENABLED --name kejiland(BLOG_AI_PUBLIC同理) - 优雅降级:未绑定 AI、未配置 D1 或开关关闭时,
/api/ai/*返回 404,前端aiProbe自动隐藏所有 AI 入口,博客其余功能零影响 - 前端记忆:探测「可用」缓存 10 分钟、「不可用」仅 30 秒,AI 上线 / 修复后刷新页面即恢复
R2(音乐音频 + 媒体图片存储)
音乐音频与媒体图片本体都存放在 R2 对象存储,元数据分别在 D1(music / media 表)。通过 S3 兼容 API 接入(非 Wrangler R2 绑定):
上传(预签名直传)
- Worker 签发 SigV4 预签名
PUTURL,浏览器直传 R2(文件不经过 Worker,规避 Worker 请求体上限,R2 egress 免费) - 签名头是
content-type;host:Content-Type参与签名,浏览器必须原样发送;因此 R2 桶 CORS 的AllowedHeaders必须包含Content-Type - 签名路径按 RFC 3986 逐段编码(
!'()*也转义),最终 URL 与签名用路径完全一致 - 使用
UNSIGNED-PAYLOAD,预签名有效期 3600 秒 - 不签
Content-Length:浏览器对该头的处理存在差异,纳入签名会导致 403 且响应不带 CORS 头 - 失败时前端会暴露真实失败阶段:预检被拦时是
HTTP 0:预检被拦截 / CORS 或网络中断;R2 返回错误时会从 XML 响应体提取<Code>/<Message>拼在HTTP <状态码>后面(跨域下只有桶的 CORS 允许才读得到响应体)
DELETE(签名头 host;x-amz-content-sha256;x-amz-date + UNSIGNED-PAYLOAD)成功后再删 D1 行,两者保持一致。
音乐(音频)
- 目标桶:
musicStorage()=R2_BUCKET与R2_PUBLIC_BASE都非空 → 音乐桶;否则回退R2_MEDIA_BUCKET+R2_MEDIA_PUBLIC_BASE。修复后音乐优先写回音乐桶(此前会误写媒体桶) - 格式白名单:mp3 / m4a / ogg / oga / wav / aac / opus / flac,单文件 ≤ 30MB
- 读取:R2 桶绑定自定义域名(如
music.example.com),前台播放器直接拉流 - 环境变量:
R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY/R2_ENDPOINT/R2_BUCKET/R2_PUBLIC_BASE
媒体图片(独立桶)
- 目标桶:固定
R2_MEDIA_BUCKET(图片与音乐不同桶) - 格式白名单:png / jpg / jpeg / webp / gif / svg / avif / bmp / ico,单文件 ≤ 10MB
- 删除:url 为本站
/media/前缀且 origin 命中R2_MEDIA_PUBLIC_BASE时先删 R2 对象再删 D1 元数据 - 环境变量:
R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY/R2_ENDPOINT/R2_MEDIA_BUCKET/R2_MEDIA_PUBLIC_BASE - 私有备份桶:
R2_BACKUP_BUCKET,保存自动 / 手动备份 JSON,不配置时备份与恢复停用
降级与配置细节
- 未配置 R2 凭据时读取列表仍可用(D1),上传返回
503,其余功能不受影响 - 音乐与图片共用同一对凭据,所以这对凭据必须同时有权限访问两个桶
- 工作流按桶分组写入:
R2_BUCKET控制音乐桶,R2_MEDIA_BUCKET控制媒体桶,R2_BACKUP_BUCKET控制私有备份桶。三者都要求R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY/R2_ENDPOINT存在;只配媒体桶时图片可用,不要求填写音乐桶
⚠️ 自定义域优先级:同主机名下,Worker 自定义域绑定会覆盖 R2 自定义域。若 music.example.com 曾被绑定到某 Worker,需先在 Workers 控制台删除该绑定,R2 自定义域才能接管流量。
定时任务与备份
wrangler.workers.toml 声明两条 Cron:
备份写入
R2_BACKUP_BUCKET,保留最近 30 份;后台「备份与恢复」支持下载、删除、恢复与恢复前安全快照。健康检查页会验证 D1 各表、KV、R2 媒体 / 备份桶、AI 与邮件绑定。
加固后台登录入口
内置的三层登录限流默认生效,也可以在边缘再加一道门(对访客无感):
登录限流的详细阈值、冷却与应急通道见 安全设计,逐步操作见 CLOUDFLARE_SETUP_GUIDE.md 第 9 节。
延伸阅读
- Cloudflare 配置完全指南(新手版) —— 从注册账号到 R2 CORS、绑定域名、首次初始化管理员,每一步都写了在控制台点哪里
- GitHub Secrets 与 R2 配置指南 —— 只聚焦 Secrets 与 R2 令牌:每个值从哪来、怎么填、两个桶用一个还是两个 Token
- 安全设计 · 本地静态部署 · KV → D1 迁移
