Skip to main content

架构概览

  • 入口: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 依次执行:
  1. ✅ Checkout + 安装 Node 24
  2. ✅ 安装 Wrangler:默认 wrangler@3.90.0;若配置了 BLOG_RATE_LIMIT_BINDING,改为 wrangler@^4.36.0(该绑定要求 4.36+)
  3. ✅ 运行三套测试(smoke-test.js 158 例 / gb-verify.js 18 例 / search-verify.js 25 例,失败即中止不部署)
  4. ✅ 校验必要 Secrets 齐全(BLOG_KV_ID / BLOG_D1_ID / CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID)
  5. ✅ 生成 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} 缺失时替换为空串(安全降级,不留字面量占位符)
  6. ✅ 执行 D1 迁移(migrations/*.sql 按文件名升序,schema_migrations 记账表 + 列预检 + 报错兜底三层幂等,见迁移脚本)
  7. ✅ 部署 Worker(wrangler deploy --config wrangler.workers.ci.toml)
  8. ✅ 写入运行时 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 对 KV 的 id 不支持 {env.} 内插,所以 CI 用脚本显式替换占位符。本地调试时也要先手动把 {env.BLOG_KV_ID} / {env.BLOG_D1_ID} 换成真实值,否则配置无法解析。

可选:边缘限流绑定

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)。
/api/music* 只在 Workers 可用:它只在 worker.js 里实现,functions/ 下没有对应文件。所以 Pages 部署下音乐功能不可用(接口返回 JSON 404)。

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 预签名 PUT URL,浏览器直传 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 允许才读得到响应体)
删除:Worker 侧签名 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 自定义域才能接管流量。
R2 桶 CORS 是上传失败的头号原因。浏览器直传要求 R2 允许你的站点跨域 PUT,配置至少包含:AllowedOrigins = 你的站点来源(example.com 与 www.example.com 都写)、AllowedMethods 含 PUT、AllowedHeaders 含 Content-Type。两个桶都要各配一遍。详见 CLOUDFLARE_SETUP_GUIDE.md 第 6.3 节。

定时任务与备份

wrangler.workers.toml 声明两条 Cron: 备份写入 R2_BACKUP_BUCKET,保留最近 30 份;后台「备份与恢复」支持下载、删除、恢复与恢复前安全快照。健康检查页会验证 D1 各表、KV、R2 媒体 / 备份桶、AI 与邮件绑定。

加固后台登录入口

内置的三层登录限流默认生效,也可以在边缘再加一道门(对访客无感): 登录限流的详细阈值、冷却与应急通道见 安全设计,逐步操作见 CLOUDFLARE_SETUP_GUIDE.md 第 9 节。

延伸阅读