Files
blog/blog-admin/README.md
T
zqlit a74b3c7127
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
归档 artalk-cf 评论后端 + rss-robot 到 blog-admin(含技术选型/模块分布 README)
2026-10-04 08:45:40 +08:00

17 KiB
Raw Blame History

artalk-cf —— 跑在 Cloudflare 上的 Artalk 评论后端 + RSS 订阅机器人

自研的 Artalk API v2 兼容服务端,以及合并进来的 RSS 订阅机器人(rss-robot), 一起跑在 Cloudflare Workers + D1 + KV 上,零服务器、零运维、免费额度内。

状态:已上线 → https://api.200181.xyz D1 artalk-cf(78140776-2bfd-439a-8b05-c3eebe5d5b47)| KV RSS_KV(d7f86a0fb43f4450a10b786fc2635128) 管理员账号 admin(后台登录用邮箱+密码,见「部署」)

两个后台都可用:/admin/ 自研管理面板 | /sidebar/ 官方 Artalk 后台 评论前端一行不用改:继续用官方 Artalk 客户端(博客里打包的 2.8.7),换后端只改 hugo.toml 一行 server。


目录

  1. 这个项目是什么
  2. 技术选型
  3. 模块分布
  4. 数据模型
  5. 部署
  6. 接到博客上
  7. 评论数据迁移
  8. 性能与免费额度

这个项目是什么

两件事,各对应一个原本独立的仓库,现在合并进同一个 Worker:

子项目 来源 职责 路径
artalk-cf 自研 Artalk API v2 兼容评论后端(44 路径 / 54 操作已实现 46 个) /api/v2/*
rss-robot 迁移自 EdgeOne 的 api/*.js RSS 订阅抓取 + 友链/友圈 + 公众号内容 /api/*(不含 /api/v2)

两者共存于一个 Worker、一个 D1、一个 KV,互不冲突: Artalk 客户端只打 /api/v2/*,RSS 模块只打 /api/*(非 v2),路径天然隔离。


技术选型

整体判断:为什么是 Cloudflare Workers + D1,而不是自建服务器

考量 结论
成本 免费版够用(Workers 10 万请求/天、D1 读 500 万行/天、存储 5GB),博客评论量级远用不满
运维 无服务器、无 SSH、无进程守护;wrangler deploy 一条命令发布
大陆可用性 见下方「已知取舍」——这是唯一的短板

逐项选型与理由

1. 运行环境:Cloudflare Workers(免费版)

  • 全球边缘函数,fetch 入口 + scheduled 定时入口
  • 致命限制:没有 TCP socket → 无法直连 SMTP 发邮件、无法跑原生 bcrypt,这两点直接决定了后续选型(见下)
  • 单请求 CPU 预算约 10ms(免费版),驱动了「读写分流 + 覆盖索引 + 计数缓存」一系列优化

2. 数据库:D1(Cloudflare 的 SQLite 边缘库)

  • 兼容 SQLite 语法,wrangler d1 execute 直接跑 SQL,批量导入零成本
  • 读写分离:D1 Sessions API + 只读副本(first-primary 写 / first-unconstrained 读)
  • 免费额度:读 500 万行/天、写 10 万行/天。这是本项目最需要盯的资源(2026-10-03 曾被打爆过一次,见性能章节的「打爆复盘」)

3. KV(键值存储):RSS 订阅与验证码通行证

  • RSS 的 feeds_config / 抓取游标 / 失败退避、人机验证的通行证、管理员通知节流,全放 KV
  • 理由:这些是「低频读、高频写、无关系」的数据,放 D1 会白白烧行数额度

4. 语言:TypeScript + Wrangler

  • 与 Workers 官方工具链天然契合;tsc --noEmit 做类型检查
  • 不引入任何运行时依赖(package.json 里只有 @cloudflare/workers-types、typescript、wrangler 三个 devDependencies)
  • 需要原生缺失的东西就自己写:MD5(Gravatar hash)、Markdown 子集解析、PBKDF2(用 WebCrypto)

5. 鉴权:无状态 HMAC-SHA256 token + PBKDF2 密码

  • token 用 TOKEN_SECRET 做 HMAC 签名、7 天过期,无状态不落库
  • 密码 PBKDF2-SHA256 12 万轮(WebCrypto 实现);兼容读取 (md5) 和明文(仅迁移期)
  • 不用 bcrypt 是因为 Workers 没有 Node 的 native 依赖、也不值得为一个算法引进包

6. 邮件:Resend 的 HTTP API(而非 SMTP)

  • Workers 无 TCP,SMTP 连不上;Resend 免费 3000 封/月
  • 全程 ctx.waitUntil() 异步 + try/catch,发信失败绝不影响评论落库

7. 人机验证:自研「一键验证」(PoW + 行为信号),而非 Turnstile/reCAPTCHA

  • 第三方脚本在大陆加载不稳定,失败会卡死评论提交;还会把访客指纹送给第三方
  • 自研版:浏览器静默算 20~80ms 的 PoW(sha256 前缀 4 个 0),命中可疑信号才让访客「点一下」,再可疑才回退图形验证码
  • 挑战无状态(HMAC 签名不写库),通行证放 KV 不占 D1 额度

8. 前端:零重写,直接复用官方产物

  • 客户端 + 官方后台(/sidebar/)是 Artalk v2.8.7 的预构建产物,内置在 public/ 里
  • 自研管理面板 /admin/ 用纯原生 JS + 零依赖(补齐官方后台缺的「待审视图」「关键词搜索」等)

已知取舍(有意为之)

项 取舍 影响
大陆 → CF 边缘 ~170ms 延迟 CF 免费版在大陆无节点,流量被送到 LAX 加 <link rel="preconnect"> 省掉 0.5s TLS 握手,立竿见影
页面标题自动抓取 不做(跨境请求收益低) 标题改为发评论时从 page_title 落库
邮箱验证码登录 / 社交登录 不做 管理员走密码登录,不受影响
url_resolver 必须保持 false 开启会重写 page_key,现有 /20210911.html 全失配

模块分布

artalk-cf/
├── wrangler.toml            ★ Worker 配置:D1/KV/静态资源/域名/cron/变量
├── schema.sql               ★ 建表(10 张表 + 覆盖索引)
├── 部署清单.md              ★ 照着敲的部署步骤(含域名那一坑)
├── .dev.vars.example        本地开发机密模板(复制成 .dev.vars)
│
├── src/                     ===== 服务端(TypeScript)=====
│   ├── index.ts             入口:路由注册 + fetch + scheduled 定时任务
│   ├── router.ts            迷你路由器(路径参数匹配,挂 / 与 /api/v2)
│   ├── rss-api.ts           RSS 模块入口(路由表 + cron 轮转抓取)
│   ├── types.ts             Env / 实体 / 响应类型定义
│   ├── template.generated.ts  设置页 YAML 模板(由官方 conf 生成)
│   │
│   ├── lib/                 ===== 通用库(无 I/O 依赖的纯逻辑)=====
│   │   ├── db.ts            D1 查询封装 + 限流 + 验证码状态
│   │   ├── cook.ts          实体 → API 输出(对应官方 dao/cook.go)
│   │   ├── session.ts       token 签发/校验 + 密码 + 管理员判定
│   │   ├── md5.ts           MD5(Gravatar hash,Workers 没有)
│   │   ├── md.ts            Markdown 子集 → HTML(先转义再解析,防 XSS)
│   │   ├── util.ts          时间 / CORS / 响应 / 校验工具
│   │   ├── errors.ts        ★ 统一错误出口(技术错误翻译成人话 + 机器码)
│   │   ├── human.ts         人机验证:PoW 挑战 + KV 通行证
│   │   ├── mail.ts          邮件发送(Resend HTTP API)
│   │   ├── admin-notify.ts  管理员提醒邮件(带节流)
│   │   └── rss/             ===== RSS 订阅子系统 =====
│   │       ├── cron.ts      每小时轮转抓取(66 源 ÷ 22 = 3 小时一轮)
│   │       ├── fetch.ts     抓取(超时/重试/代理回退/熔断)
│   │       ├── discover.ts  feed 地址自动发现
│   │       ├── parse.ts     RSS/Atom 解析
│   │       ├── greetings.ts 问候语/每日一图
│   │       ├── notify.ts    更新推送(飞书 webhook 等)
│   │       ├── proxy-health.ts  国内代理(SCF)体检
│   │       └── util.ts      RSS 工具
│   │
│   └── routes/              ===== 路由处理器(按业务域分组)=====
│       ├── public.ts        conf / 验证码 / 投票 / pv / setup / healthz
│       ├── comments.ts      评论 CRUD + 列表与树(含人机验证门)
│       ├── user.ts          用户 / 登录 / 合并 / SSO
│       ├── admin.ts         设置 / 站点 / 页面 / 用户 / 消息 / 统计 / 导入导出 / 上传
│       ├── human.ts         人机验证端点(challenge / verify / status)
│       └── rss/             ===== RSS 路由 =====
│           ├── feeds.ts     订阅源管理
│           ├── links.ts     友链/友圈
│           ├── results.ts   抓取结果 / 文章
│           ├── media.ts     随机图 / 评论机器人
│           ├── greeting.ts  问候语
│           ├── misc.ts      鉴权 / 健康检查
│           ├── tools.ts     favicon / 代理 / cron / 部署状态 / 邮件
│           └── wechat.ts    微信公众号内容
│
├── public/                  静态资源(优先级高于 Worker,改这里不用动 Worker 代码)
│   ├── index.html           /           入口页(健康状态 + 两个后台入口)
│   ├── admin/               /admin/     自研管理面板(原生 JS,6 个视图)
│   │   ├── admin.css        设计令牌 + 全部组件样式
│   │   ├── admin.js         仪表盘/评论/页面/用户/设置/状态
│   │   └── fonts/           Press Start 2P + JetBrains Mono
│   ├── sidebar/             /sidebar/   官方 v2.8.7 管理后台(Vue 预构建)
│   ├── dist/                官方 v2.8.7 客户端与插件
│   └── feed.html            /feed/      RSS 订阅源管理页
│
└── tools/                   工具脚本(开发/部署/自检)
    ├── deploy.sh            一键部署
    ├── import_artrans.py    .artrans → D1 SQL(批量迁移)
    ├── import_delta.py      增量导入
    ├── gen_template.py      从官方 conf YAML 生成设置模板常量
    ├── selftest.ts          纯逻辑自检(MD5/时间/Markdown/密码/token/YAML)
    ├── smoke.sh             冒烟测试
    ├── preview-mail.ts      邮件样张预览
    ├── mailtest.ts          发信测试
    └── check-*.mjs / probe-*.mjs / shot-*.mjs / measure-*.mjs
                             无头浏览器(CDP)测量/回归脚本,调试评论前端用

数据流一览

浏览器(博客评论区 / 后台 iframe)
   │  /api/v2/*(评论)/ /api/*(RSS)
   ▼
Worker fetch(src/index.ts)
   ├─ /api/* 且非 /api/v2/*  → rss-api.ts(RSS 子系统)
   └─ 其余 → router.ts 分发到 routes/*
        │
        ├─ 读请求  → D1 只读副本(first-unconstrained)
        ├─ 写请求  → D1 主库(first-primary)
        └─ 通行证  → KV(人机验证 / RSS 游标 / 通知节流)
   │
scheduled(cron)
   ├─ 0 * * * *   → RSS 轮转抓取(每小时 22 源)
   └─ 17 3 * * *  → 评论 GC(限流/验证码清理 + healthz 缓存刷新 + 代理体检)

数据模型

10 张表(schema.sql),时间统一存 unix 毫秒,输出时按 Asia/Shanghai 格式化:

表 用途 关键字段
sites 站点 name(唯一)、urls
pages 页面 key(=page_key 如 /20210911.html)、site_name、pv、vote
users 用户 name+email(唯一)、password(pbkdf2/md5/明文兼容)、徽章、头衔
comments 评论 rid(父节点)、root_id(顶层祖先)、is_pending/pinned/verified、软删除 deleted_at
votes 投票 target_id + user_id + type 唯一
notifies 通知 user_id、comment_id、is_read/is_emailed
settings 键值配置 key(主键)、value(frontend_conf / admin_users 等)
rate_limits 限流 bucket(如 comment:1.2.3.4)、count、expires_at
captcha_passes 验证码通过状态 ip(主键)、expires_at
captcha_challenges 验证码题目 answer_hash(只存哈希)

覆盖索引(性能关键,见下):

  • idx_comments_page_list — 列表 ORDER BY is_pinned DESC, created_at DESC 专用
  • idx_comments_children — 子评论 root_id IN (...) 专用
  • idx_comments_site_list — 站点级列表

root_id 语义与官方一致:顶层为 0,子孙为该顶层评论的 id。列表默认 nested 模式——取一页顶层,再捞回全部子孙交给前端组树。


部署

完整步骤看 部署清单.md(照着敲约 10 分钟)。快速版:

cd blog-admin && npm install
npx wrangler login
npx wrangler d1 create artalk-cf      # 把返回的 database_id 填进 wrangler.toml
npx wrangler secret put TOKEN_SECRET
npx wrangler secret put ADMIN_PASSWORD
npx wrangler d1 execute artalk-cf --remote --file=./schema.sql
npx wrangler deploy
curl -X POST https://<worker>/api/v2/setup

★ 域名这一步有个坑,动手前先看

Cloudflare Worker 的自定义域名要求「域名的 NS 托管在 Cloudflare」。 而 usj.cc 的 NS 在 DNSPod(还挂着又拍云 + 腾讯云 EdgeOne 调度)——千万不要为了评论 API 把整站 NS 搬去 Cloudflare,会弄坏现有国内双 CDN 链路。

本项目用的是 api.200181.xyz(NS 本来就在 Cloudflare):

[[routes]]
pattern = "api.200181.xyz"
custom_domain = true

*.workers.dev 地址在大陆基本不通,只能自测。四种域名方案取舍见 部署清单.md 第 9 步。

机密清单(不写进仓库)

Secret / 变量 用途
TOKEN_SECRET 登录 token 签名
ADMIN_PASSWORD 管理员密码
RESEND_API_KEY 邮件(可选)
MAIL_FROM / MAIL_ADMIN 邮件发件人 / 收件人(可选)

接到博客上

E:\GitHub\blog\hugo.toml:

[params.artalk]
  server = "https://api.200181.xyz"   # ← 只改这一行
  site = "优世界"                      # 必须与 SITE_DEFAULT 一致

客户端调用 ${server}/api/v2/...,pageKey 用 .RelPermalink(/20210911.html),与 D1 的 page_key 完全对应,不需要改任何模板或前端代码。


评论数据迁移

方式一:批量 SQL(推荐)

Worker 逐条 INSERT 会产生几千次 D1 往返,免费版单请求 CPU 只有 10ms 容易超时,所以走 wrangler d1 execute --file:

python tools/import_artrans.py       # 读 .artrans,只导「优世界」,生成 SQL
bash tools/import-out/run.sh --remote

脚本做了:按 site_name 过滤、按「昵称+邮箱」重建用户、保留原始 id 与 rid 关系、rid 回填与 root_id 计算分三趟做(与导入顺序无关)、最后自检打印各表条数。

方式二:后台界面导入

登录 /sidebar/ → 迁移页 → 上传 .artrans(小数据量可用)。

⚠️ 别踩的坑

  • url_resolver 保持 false,否则 page_key 被重写,评论区变空
  • 导出里 created_at 为 Go 零值(0001-01-01)的 6 条会被兜底成当前时间

性能与免费额度

免费额度够不够(3440 条评论 / 138 篇文章)

资源 免费额度 你的用量 结论
Workers 10 万请求/天 评论量级一天最多几千 ✅
D1 存储 5 GB 评论约几 MB ✅
D1 读 500 万行/天 一次列表查询几十行 ✅
Workers CPU 10 ms/请求 列表查询含 markdown 渲染,需观察 ⚠️

分层耗时(中国大陆网络实测)

项 耗时
冷连接 TLS 握手 ~0.51s
连接复用后端到端 ~190ms
1 次 D1 查询 8~20ms

真正的瓶颈是「大陆 → CF 边缘」这段(~170ms),不是数据库。 改善:博客里加 <link rel="preconnect" href="https://api.200181.xyz">,省掉 0.5s TLS 握手,成本最低。

打爆复盘(2026-10-03 D1 额度用尽)

D1 免费版读额度(500 万行/天,UTC 00:00 = 北京 08:00 重置)被一次打爆,实测 525 万行。定位与修复:

  1. 取证:CF GraphQL d1QueriesAdaptiveGroups(维度名是 query)按 sum_rowsRead 排序,锁定 COUNT(*) + LEFT JOIN users 是无索引全表扫
  2. 修复:
    • 三个覆盖索引(idx_comments_page_list / _children / _site_list)
    • 计数 KV 缓存(只在增删/审核时失效)
    • 列表/计数不再无谓 JOIN users
    • 两个 COUNT 合并成一条 COUNT(*) + SUM(CASE ...)
  3. 教训:改动前先本地预览 + 冒烟测试,别拿线上打爆的额度再跑测试

自查方法

打开 https://api.200181.xyz/api/v2/healthz 看三个字段:

  • colo —— 请求落在哪个 CF 机房(决定 RTT)
  • region / primary —— 查询由主库还是副本处理
  • mail —— 邮件是否已配置("on")

安全

  • 登录 token:HMAC-SHA256 签名,无状态、7 天过期;改密码写 token_valid_from 让旧 token 立即失效
  • 密码:PBKDF2-SHA256 12 万轮;兼容 (md5) / 明文(仅迁移)
  • 后台接口全部走 requireAdmin,未登录 403
  • 评论软删除(deleted_at),误删可从 D1 恢复
  • CORS 白名单 + 预检处理;写接口走 D1 计数限流
  • 验证码答案只存哈希;人机验证挑战 HMAC 签名无状态
  • 顶层 fetch 兜底:任何漏网异常也带 CORS + 人话错误,不给读者甩「Failed to fetch」