博客评论系统迁移记:Valine → Waline → Twikoo(附评论系统对比与 Cloudflare Workers + D1 部署教程)
注: 本文由AI生成,ednovas编辑并审核发布。
博客评论系统迁移记:Valine → Waline → Twikoo
这个博客从 2021 年开通评论区,五年多里换了三套评论系统:最早是 Valine + LeanCloud;今年 1 月 LeanCloud 发公告宣布停服,4 月我换成了 Waline(Vercel + Neon);结果没过几个月 Vercel 账号被封,评论区整个挂掉,而我至少一个多月都没发现。这次干脆把评论后端搬到了 Cloudflare Workers + D1,换成 Twikoo,并把五年来的评论一条不落地迁了过来。
本文分三部分:
- 📜 迁移历程(第一章):每一代评论系统用了多久、怎么出的事、数据是怎么救回来的
- ⚖️ 评论系统横向对比(第二章):Valine / Waline / Twikoo / Artalk / Giscus / Utterances / Gitalk / Disqus / Remark42 / Cusdis
- 🛠️ 部署教程(第三~六章):Twikoo + Cloudflare Workers + D1 从零部署、接入 Butterfly、导入旧评论
TL;DR
- 现在的方案:Twikoo 2.0.15 + Cloudflare Workers + D1,后端域名
comment.ednovas.xyz,前端用 Hexo + Butterfly 3.6.0 自带的 Twikoo 支持- 迁移结果:536 条评论全部上线,其中 530 条是 Valine 时代的老评论,另外 6 条是 Waline 时期的新评论,差点就丢了
- 成本:0 元(不算域名本身)。全部在 Cloudflare 免费额度内,DNS 本来就托管在 Cloudflare
- 最大的教训:免费 PaaS 账号说没就没。评论后端最好放在自己本来就离不开的平台上,并且定期导出
一、迁移历程
1.1 时间线
| 时间 | 事件 |
|---|---|
| 2021-02-23 | 收到第一条 Valine 评论(Valine + LeanCloud 时代开始) |
| 2026-01-12 | LeanCloud 发布停服公告,即日起停止新用户注册和新建应用(已有应用继续运行) |
| 2026-04-07 | Valine 时代的最后一条评论,累计 530 条,分布在 112 个页面 |
| 2026-04-13 | 迁到 Waline:Vercel 部署服务端,Neon PostgreSQL 存数据,530 条全部导入 |
| 2026-04-18 → 2026-06-02 | Waline 期间新增 6 条评论(5 条正常、1 条垃圾) |
| 最晚 2026-09-10 | Vercel 账号已被封,评论接口返回 451,全站评论区静默失效 |
| 2026-10-19 | 发现问题,从 LeanCloud 和 Neon 找回全部数据,迁到 Twikoo + Workers + D1,536 条上线 |
| 2027-01-12 | LeanCloud 正式停止全部对外服务(还没到,但已经确定) |
1.2 第一代:Valine + LeanCloud(2021-02 → 2026-04)
Butterfly 内置 Valine,在 LeanCloud 建个应用、把 AppID / AppKey 填进主题配置就能用,没有任何后端需要维护。五年多下来攒了 530 条评论。
它的问题是完全依赖 LeanCloud。LeanCloud 的官方停服公告写得很清楚:
- 2026-01-12 起:停止新用户注册和新建应用,已有应用可以继续运行
- 2027-01-12:正式停止全部对外服务,包括应用访问、数据读写、API 调用和控制台
- 停服后用户数据会被永久销毁或匿名化,无法恢复
Valine 本身也基本停更了,最后一个版本 v1.5.3 发布于 2024-12,邮件提醒和验证码从 v1.4.0 起就已废弃。
还有一个隐私问题:Valine 是前端直连 LeanCloud 的,AppKey 就写在页面里,任何拿到这个公开 key 的人都能通过 REST API 读出整张评论表,包括评论者的邮箱和 IP。所以迁走之后一定要在 LeanCloud 控制台把 Comment 表的数据删掉(或直接删除应用),别等到 2027 年停服。
⚠️ 还在用 Valine 的朋友,务必在 2027-01-12 之前把数据导出来迁走。
1.3 第二代:Waline + Vercel + Neon(2026-04 → 最晚 2026-09-10)
2026-04-13,我在 AI 编程助手的帮助下迁到了 Waline:
- 🚀 服务端:Vercel 一键部署
- 🗄️ 数据库:在 Vercel 的 Storage(Marketplace)集成里创建的 Neon PostgreSQL
- 🌐 域名:
comment.ednovas.xyz,CNAME 到 Vercel - 📦 数据:从 LeanCloud 导出 JSON,转成 Waline
wl_comment表的 INSERT 语句导入 Neon
用起来一切正常,4 月到 6 月间还陆续收到了 6 条新评论。然后我就把这事忘了。
1.4 事故:Vercel 账号被封,评论区静默挂掉
某天开始(具体哪天我也不知道),comment.ednovas.xyz 变成了这样:
状态码:451 Unavailable For Legal Reasons |
前端的表现是:每篇文章下的评论区都一直加载不出来,也没有任何显眼的报错,所以我至少一个多月都没发现。Wayback Machine 上这个接口最早的快照(2026-09-10)就已经是 451 了。
被封的原因 Vercel 没有给任何解释,而且是账号级别的封禁。查了一下 Vercel 的文档,DEPLOYMENT_DISABLED 对应的是 402(超出额度 / 需要付费),而我拿到的是 451(因法律原因屏蔽),文档里没有这种组合的说明,原因至今不明。我猜可能和同一个 Vercel 账号下的其他项目有关,但这只是猜测。
1.5 找回现场:后端到底部署在哪?
半年没碰,我已经忘了评论后端放在哪个平台、数据库是什么。排查过程:
# 1. 看评论域名指向哪里 |
知道是 Vercel 之后,我在 AI 助手的本地会话记录里翻到了当初迁移时留下的笔记,这才想起数据库是 Neon。
1.6 抢救数据:LeanCloud + Neon 两头找
LeanCloud 这边:530 条 Valine 评论都还在,用应用的公开 key 通过 REST API 就能读出来。最新一条是 2026-04-07,说明 Waline 上线后从没往 LeanCloud 写过东西。
Neon 这边:虽然 Vercel 账号被封了,但通过 Vercel 创建的 Neon 项目,用同一个账号还能登录 console.neon.tech。我用 Neon CLI(neon login、neon connection-string)拿到连接串,再用 node-pg 以只读方式导出了 Waline 的所有表:
wl_comment共 536 行 = 导入的 530 条 + Waline 期间新写入的 6 条(2026-04-18 → 2026-06-02,5 条正常、1 条垃圾)- 也就是说,一条都没丢
导出时踩了两个小坑:
- 列名是小写的:Waline 建表时用的是不带引号的标识符,PostgreSQL 会把它转成小写,所以列名是
createdat,不是createdAt - 时间差了几个小时:列类型是
timestamp without time zone,存的是 UTC 时间,但 node-pg 会把它当成本地时间解析,读出来就偏了几个小时。解决办法是在 SQL 里直接转成文本
-- ❌ 报错:column "createdAt" does not exist |
导出后我把前 530 行和 LeanCloud 的数据逐条比对:昵称、网址一致,时间差都在 0.5 秒的舍入误差以内。
💡 说实话这次纯属运气:LeanCloud 还没停服,Neon 控制台也还能登录。只要其中一个没了,就会丢一批评论。
1.7 第三代:Twikoo + Cloudflare Workers + D1(2026-10-19 起)
吸取教训,这次的原则是:评论后端要放在我本来就离不开的平台上。博客的 DNS 本来就在 Cloudflare,Twikoo 2.x 又正好有官方的 Cloudflare Workers + D1 适配器,于是就选了它。具体理由见第三章,部署过程见第四章。
二、评论系统横向对比
先说明一下:我自己真正用过的只有 Valine、Waline、Twikoo 三个,其他的信息来自各项目的仓库、npm 和官方文档。下表的版本号和维护状态都是 2026-10-19 核查的;「国内访问」一栏是按部署方式给出的经验判断,实际效果还要看线路。
2.1 基本信息
| 系统 | 最新版本 / 状态 | 数据存储 | 免费部署方式 | 评论需登录 | Butterfly 3.6.0 内置 |
|---|---|---|---|---|---|
| Valine | v1.5.3(2024-12),停更 | LeanCloud(前端直连) | LeanCloud(已停止新注册) | 否 | ✅ |
| Waline | 服务端 1.43.4 / 客户端 3.16.1,活跃 | Node 服务端 + MySQL / PostgreSQL / SQLite / MongoDB 等 | Vercel + Neon、Netlify、Zeabur 等 | 否(可强制登录) | ✅ |
| Twikoo | 2.0.15,活跃 | 云函数 + CloudBase / MongoDB / D1 等 | Workers + D1、Vercel / Netlify + MongoDB Atlas、Docker | 否 | ✅ |
| Artalk | v2.10.0,活跃 | Go 单体服务 + SQLite / MySQL / PostgreSQL / SQL Server | 无,需要自己的服务器 | 否(可开社交登录) | ❌ |
| Giscus | 托管服务,仍在维护 | GitHub Discussions | giscus.app 免费托管 | 是(GitHub) | ❌ |
| Utterances | 2022 年后停更 | GitHub Issues | utteranc.es 免费托管 | 是(GitHub) | ✅ |
| Gitalk | 1.8.1,1.x 已 EOL;2.0 仍是 beta | GitHub Issues(2.0 改为 Discussions) | 纯前端 + CORS 代理 | 是(GitHub) | ✅ |
| Disqus | 商业 SaaS | Disqus 服务器 | 官方托管(免费版带广告) | 可游客评论 | ✅ |
| Remark42 | v1.18.0,活跃 | Go 单体服务 + 内嵌 BoltDB | 无,需要自己的服务器 | 默认需登录,可开匿名 | ❌ |
| Cusdis | 已停止维护,仓库已归档 | — | — | — | ❌ |
2.2 功能对比
| 系统 | 国内访问(经验) | 管理后台 | 通知 | 反垃圾 | 可导入 Valine |
|---|---|---|---|---|---|
| Valine | 停服后不可用 | 无(只能用 LeanCloud 控制台) | 邮件提醒 1.4 起已废弃 | 基本没有 | — |
| Waline | 看部署,vercel.app 必须换自定义域名 |
内置 /ui 后台 |
邮件、微信、QQ、Telegram、PushPlus、Discord、飞书 | Akismet(默认开)、审核模式、IP 限频、reCAPTCHA v3 / Turnstile | ✅ 官方迁移工具 |
| Twikoo | CloudBase 最快(需付费);Workers 必须绑自定义域名 | 内嵌管理面板 | 邮件 + Server 酱 / Qmsg / Pushoo(IM 只通知博主) | Akismet、腾讯云内容安全、人工审核、IP 限频、Turnstile(CF 版没有 Akismet 和腾讯云) | ✅ 后台导入 |
| Artalk | 取决于你的服务器 | 独立控制中心,支持多站点 | 邮件 + Telegram / 飞书 / 钉钉 / Bark / Slack / Webhook 等 | Akismet、腾讯云 / 阿里云审核、多种验证码、频率限制 | ✅(经 Artrans 转换) |
| Giscus | 依赖 GitHub,时好时坏 | 在 GitHub 上管理 | GitHub 通知 | 靠 GitHub 的锁定 / 隐藏 / 屏蔽 | ❌ |
| Utterances | 同 Giscus | 同 Giscus | 同 Giscus | 同 Giscus | ❌ |
| Gitalk | 同 Giscus | 同 Giscus | 同 Giscus | 同 Giscus | ❌ |
| Disqus | 被墙,DisqusJS + 反代只能只读展示 | Disqus 后台 | 邮件 | 内置审核过滤 | ❌ |
| Remark42 | 取决于服务器;Google 等登录方式国内用不了 | 在评论区内联管理 | Telegram / Slack / Webhook / 邮件 | 主要靠登录门槛和封禁用户 | ❌ |
(Butterfly 3.6.0 还内置了来必力 Livere 和 Facebook Comments,这次没有展开。)
2.3 逐个点评
Valine:不建议新用,在用的尽快迁出。 没有自己的后端,生死全看 LeanCloud。2027-01-12 停服后就彻底不能用了,而且公开 key 可以直接读出评论者的邮箱和 IP。Waline 官方迁移工具、Twikoo 后台导入、Artalk 的 Artrans 都能接 Valine 的数据。
Waline:功能最全的 Valine 继任者。 相当于「带后端的 Valine」:内置管理后台、用户系统(可选第三方登录)、浏览量统计、文章反应,通知渠道也最多。缺点是最常见的 Vercel 部署方案里,*.vercel.app 在国内访问很差,必须绑自定义域名;而且免费 PaaS 账号的风险我已经亲身体验过了。另外要注意,Butterfly 3.6.0 用的是 Waline v1 的 new Waline() 写法,CDN 地址没带版本号,jsDelivr 实际返回的是 1.6.0(2022 年的版本),想用 3.x 客户端得升级主题或者自己改模板。
Twikoo:国内友好、部署方式最多(本站现用)。 2.x 改成了 pnpm monorepo,支持腾讯云 CloudBase、Vercel、Netlify、Cloudflare Workers、Docker 等一堆平台。内嵌管理面板、QQ 号自动补全昵称和头像、贴图、KaTeX、代码高亮都有。不过平台方面有些变化:CloudBase 现在需要付费套餐,Hugging Face 新建 Docker Space 也要付费了。Cloudflare 版的限制见第三章。
Artalk:有服务器就很香。 Go 写的单体服务,Docker 一条命令就能跑,前端很轻。独立控制中心、多站点、社交登录、多种验证码、多种审核服务都有,是这几家里最「全家桶」的。代价是必须有自己的服务器,没有 Serverless 免费方案。
Giscus:读者都有 GitHub 账号的话很合适。 基于 GitHub Discussions,无广告、无追踪、零运维。但评论必须登录 GitHub,对非技术读者门槛高,国内访问 GitHub 也时好时坏。
Utterances:能用,但已基本停更。 基于 GitHub Issues,最后一次代码提交是 2022 年。新站直接选 Giscus 就好,思路一样、还在维护。
Gitalk:1.x 已 EOL。 2026-10-16 发布的 v1.8.1 是 1.x 的最终版本,之后不再修 bug、不出安全更新。1.x 的老问题是 clientSecret 写在前端、申请的权限是 public_repo(相当于能写评论者所有公开仓库),换 token 还依赖第三方 CORS 代理。2.0 改用 GitHub App + Discussions,但还是 beta。另外 v1.8.1 只发在了 GitHub Release 上,npm 上的 gitalk 仍停在 1.8.0,Butterfly 3.6.0 用 gitalk@latest 加载的就是它。
Disqus:老牌 SaaS,国内基本不可用。 免费版带广告,去广告需要付费订阅。国内只能靠 DisqusJS 只读展示。不用运维、可以游客评论,但广告、追踪、数据都在别人手里。
Remark42:注重隐私的自托管方案,偏英文社区。 Go 单文件或 Docker 部署,数据是一个 BoltDB 文件,自带导出和自动备份。默认需要社交或邮箱登录,国内读者能用的登录方式有限。v1.18.0 修了两个 token 相关的安全问题,自托管的话建议尽快升级。
Cusdis:已停止维护。 2026-07 README 声明 deprecated,仓库已归档,2026-10-19 访问官网返回 Cloudflare 521,不要再选。
2.4 怎么选
| 你的情况 | 推荐 | 注意 |
|---|---|---|
| 域名在 Cloudflare、不想要服务器、读者在国内、需要匿名评论 | Twikoo + Workers + D1(本站方案) | 一定要绑自定义域名;邮件只能走 HTTP API,或改用 Telegram 推送 |
| 想要用户系统、浏览量、文章反应等最全功能 | Waline | 绑自定义域名、定期导出;别和高风险项目放在同一个免费 PaaS 账号 |
| 国内访问速度第一,愿意花点钱 | Twikoo / Waline 部署在腾讯云 CloudBase,或国内服务器自托管 | CloudBase 需要付费套餐 |
| 有 VPS,想完全掌控数据、多站点 | 中文博客选 Artalk,偏英文、重隐私选 Remark42 | 要自己负责备份和安全更新 |
| 技术博客、读者基本都有 GitHub、主要面向海外 | Giscus | 不要再选 Utterances(停更)和 Gitalk 1.x(EOL) |
| 已经在用 Disqus | 海外站可以继续用 | 有国内读者的话加 DisqusJS 兜底,或迁到 Waline / Twikoo / Artalk |
| 还在用 Valine | 尽快迁走 | 截止日期 2027-01-12;注意路径编码、Markdown 混排、表情短码(见第六章) |
❌ 不要选:Valine(LeanCloud 停服)、Cusdis(停止维护)、Utterances(停更,用 Giscus 代替)、Gitalk 1.x(EOL、权限过大)。
三、为什么选 Twikoo + Cloudflare Workers + D1
3.1 选择理由
- ☁️ 和 DNS 在同一个 Cloudflare 账号:域名本来就托管在 Cloudflare,不再多依赖一个平台
- 💰 免费:Workers 和 D1 的免费额度对一个个人博客绰绰有余
- 🖥️ 没有服务器:不用管系统更新、证书、备份脚本
- 🧩 官方适配器:Twikoo 2.0.9 把 Cloudflare Workers 适配器重写进了主仓库(
packages/server-cloudflare),用 D1 存数据(1.x 时代是单独的 twikoojs/twikoo-cloudflare 仓库) - 🦋 主题原生支持:Butterfly 3.6.0 内置 Twikoo,改几行配置就能切换
- 👤 匿名评论 + 管理面板:读者不用登录,我能在前端直接管理评论
3.2 免费额度够不够?
Workers 免费版:
| 项目 | 免费额度 | 本站实测 |
|---|---|---|
| 请求数 | 10 万次 / 天(UTC 0 点重置) | 个人博客远远用不完 |
| CPU 时间 | 10 ms / 请求(等待 D1 和网络的时间不计入) | 发表评论(含 jsdom + DOMPurify 消毒)不到 1 秒完成,没有触发 CPU 超限 |
| 脚本体积 | 64 MiB(未压缩) | 上传约 9 MB,gzip 后约 1.4 MB |
| 启动时间 | 1 秒 | 17 至 40 ms |
💡 Cloudflare 在 2026-09-04 取消了 Workers 压缩后体积的限制(以前免费版是压缩后 3 MB),现在只看未压缩体积,免费版和付费版都是 64 MiB。按旧规则,Twikoo 这个 1.4 MB 的 gzip 包其实也放得下。
D1 免费版:
| 项目 | 免费额度 |
|---|---|
| 单库容量 | 500 MB |
| 读取行数 | 500 万行 / 天 |
| 写入行数 | 10 万行 / 天 |
| 单条 SQL 语句 | 100 KB |
| 单行 / 单个值 | 2 MB |
| 每次 Worker 调用的查询数 | 50 次(见下方说明) |
| Time Travel(时间点恢复) | 7 天 |
💡 关于每次调用的查询数:D1 限制页写的是免费版 50 次,但 Workers 限制页和 2026-02-11 的更新日志写的是免费版对 Cloudflare 服务可发 1,000 次子请求。官方文档说法不一致,我按保守的 50 次来规划。
536 条评论(连同那几张 base64 截图)也就一两 MB,离单库 500 MB 的上限还差得远(可以用 npx wrangler d1 info twikoo 看实际大小)。部署后我也实测了一下:首页 29 篇文章的评论数在一次请求里查完,没有问题。
3.3 代价
这个方案也不是没有缺点,部署之前最好心里有数:
- 🔧 要自己从源码构建:Cloudflare 适配器没有发布到 npm(
@twikoojs/cloudflare是 private 包),而且 2.x 的根package.json要求 Node.js 26 和 pnpm 12.5.1。构建只是一次性的,Worker 本身跑在 workerd 上,跟你本机的 Node 版本无关 - 📧 不能用 SMTP 发邮件:Workers 上 25 端口被禁止,587 和 465 也连不上。邮件只能走 Resend / SendGrid / MailChannels 这类 HTTP 邮件 API,或者干脆用 Telegram 推送
- 🛡️ 没有 Akismet 和腾讯云内容安全:Cloudflare 适配器不支持这两个,反垃圾只能靠频率限制、验证码、违禁词、人工审核或 AI 检测
- 🧱
workers.dev在国内被 DNS 污染:必须绑自定义域名,国内访问速度也要看线路 - 📏 免费版限制:主要是 10 ms CPU 和每次调用的查询数。日常读写评论没问题,但一次性批量导入几百条评论就有风险(见第六章)
四、部署教程:Twikoo + Cloudflare Workers + D1
下面以 Twikoo 2.0.15 为例,评论后端域名用 comment.example.com 代替,换成你自己的就行。
4.1 准备工作
- 一个 Cloudflare 账号,域名的 DNS 托管在 Cloudflare
- Git
- Node.js 26(只在构建时需要)
- 不需要提前安装 pnpm 和 wrangler,下面都用
npx调用
💡 我本机是 Node 24,pnpm 直接拒绝安装,因为根
package.json的engines要求node >=26。我不想动系统里的 Node,就下载了一个 Node 26.12.0 的便携版 zip(校验过 SHA256),只在构建用的终端里临时加进 PATH:
# PowerShell:只对当前终端生效 |
4.2 克隆并构建
git clone --depth 1 --branch 2.0.15 https://github.com/twikoojs/twikoo.git twikoo-src |
--filter "@twikoojs/cloudflare..." 的意思是构建 Cloudflare 适配器以及它依赖的所有 workspace 包。构建完成后,产物在 packages/server-cloudflare/dist/index.js。
4.3 创建 D1 数据库
cd packages/server-cloudflare |
命令会输出一段配置片段和 database_id,下一步只需要其中的 id。片段里的 binding 是 twikoo,直接抄的话要改成 DB。
4.4 编写 wrangler.toml 并建表
仓库里没有现成的 wrangler.toml,需要在 packages/server-cloudflare 目录下自己写一个:
name = "twikoo" |
几个要点:
| 配置 | 说明 |
|---|---|
main = "dist/index.js" |
必须指向构建产物,不要直接部署 src/ |
nodejs_compat |
必需,公共层依赖 Node API |
binding = "DB" |
绑定名必须是 DB,适配器按这个名字找数据库 |
workers_dev = true |
配了 routes 之后,wrangler 默认会关掉 workers.dev 地址;想保留就显式写 true |
custom_domain = true |
让 wrangler 自动创建 DNS 记录并签发证书 |
D1 模式下不需要任何 secret 或环境变量,所有设置都在 Twikoo 管理面板里改,存在 D1 的 config 表中。
配置写好后建表:
npx wrangler d1 execute twikoo --remote --file=./schema.sql |
⚠️
wrangler d1 execute默认操作的是本地模拟库,不加--remote的话,你以为建好了表,其实线上数据库什么都没有。💡 按 README 的说法,这一步也可以省略,首次请求时会自动建表。不过我还是习惯手动建好。
4.5 部署并绑定自定义域名
npx wrangler deploy |
第一次部署时报了这个错(这个错误码在 Cloudflare 文档里查不到):
Hostname 'comment.example.com' already has externally managed DNS records (A, CNAME, etc). |
原因是 DNS 里还留着以前指向 Vercel 的 CNAME 记录。Cloudflare 文档也写明了:不能在已经有 CNAME 记录的主机名上创建 Workers 自定义域名。解决办法:
- Cloudflare Dashboard → 你的域名 → DNS → Records
- 删除
comment这条旧记录 - 重新执行
npx wrangler deploy
部署成功后,wrangler 会自动创建 DNS 记录和证书,几分钟内就能访问。
4.6 验证
curl https://comment.example.com/ |
💡 上面是 bash 写法,Windows 上可以在 Git Bash 里执行。PowerShell 5.1 里
curl是Invoke-WebRequest的别名,而且就算换成curl.exe,\续行也不能用,传 JSON 时内层双引号还会被吃掉。在 PowerShell 里直接用自带的命令更省事:
Invoke-RestMethod https://comment.example.com/ |
4.7 第一件事:设置管理员密码
部署完立刻去设置管理员密码! Twikoo 在没有设置密码时,第一个来设置的人就能拿到管理权限。如果被别人抢先,你的评论区就归别人管了。
问题是接入博客要等第五章,这段时间密码一直空着,而且任何人都能直接调用 SET_PASSWORD 接口抢先设置(不需要打开面板)。比较稳妥的做法是:部署完立刻在本地按第五章改好 _config.butterfly.yml,hexo s 后打开 http://localhost:4000 上的任意文章,点击评论列表上方「N 条评论」那一行右侧的齿轮图标(在搜索、刷新图标旁边),进入「Twikoo 管理面板」设置密码(Twikoo 对 localhost 永远放行 CORS),然后再去发布博客。
管理面板有四个功能:
- 📝 评论管理:全站评论按时间倒序,可按昵称 / 邮箱 / 网址 / IP / 内容 / 页面搜索,可筛选全部 / 显示 / 隐藏,每条评论可以查看、隐藏、置顶、删除
- ⚙️ 配置管理:所有设置项都在这里
- 📥 导入:支持 Valine、Artalk、Disqus 等格式
- 📤 导出:定期导出备份
4.8 推荐的管理面板配置
| 配置项 | 示例 / 建议 | 说明 |
|---|---|---|
SITE_NAME |
EdNovas的小站 |
通知里显示的站点名 |
SITE_URL |
https://blog.example.com |
站点地址 |
BLOGGER_NICK |
你的昵称 | 博主昵称 |
BLOGGER_EMAIL |
你的邮箱 | 设置后自己发的评论不会再通知自己 |
CORS_ALLOW_ORIGIN |
https://blog.example.com |
逗号分隔、不要加空格;留空表示允许所有来源。localhost 等本地地址永远放行,本地 hexo s 调试不用加 |
LIMIT_PER_MINUTE |
默认 10 | 名字容易误会:源码里其实是单个 IP 每 10 分钟最多发几条,填 0 关闭 |
LIMIT_PER_MINUTE_ALL |
默认 10 | 全站每 10 分钟最多几条 |
CAPTCHA_PROVIDER |
Turnstile |
配合 TURNSTILE_SITE_KEY / TURNSTILE_SECRET_KEY 使用 Cloudflare 的验证码 |
HIDE_ADMIN_CRYPT |
一个暗号 | 设置后,只有在昵称框输入这个暗号才会显示齿轮入口 |
Cloudflare 版没有 Akismet,如果垃圾评论多,还可以考虑这几个(都在公共层实现,CF 版可用):
AKISMET_KEY填MANUAL_REVIEW:开启人工审核,评论审核后才显示(这个值不会去调用 Akismet)FORBIDDEN_WORDS:命中违禁词的评论标记为垃圾LLM_API_KEY/LLM_API_ENDPOINT/LLM_MODEL:用兼容 OpenAI 接口的大模型做垃圾评论检测
⚠️ 注意这几项在源码里是 else-if 关系,不能随便叠加:
AKISMET_KEY只要非空(包括MANUAL_REVIEW),LLM 检测就不会执行;开了MANUAL_REVIEW后违禁词也不再检查(反正所有评论都要人工审)。CF 版千万别填真实的 Akismet key:它不会生效,还会让 LLM 检测被跳过。
4.9 Telegram 通知(我用的方案)
既然发不了 SMTP 邮件,我就改用 Telegram 接收新评论通知。Twikoo 内置了 Pushoo,配置很简单:
- 在 Telegram 里找 @BotFather,发送
/newbot创建机器人,拿到 bot token - 打开你刚创建的机器人的对话框,点一下 Start(否则机器人没法给你发消息)
- 找 @userinfobot,它会告诉你自己的 chat id
- 在 Twikoo 管理面板里设置:
| 配置项 | 值 |
|---|---|
PUSHOO_CHANNEL |
telegram |
PUSHOO_TOKEN |
<bot_token>#<chat_id>(两者用 # 连接) |
几个注意点:
- 📌 IM 推送只通知博主,回复提醒发给评论者还是得靠邮件
- 📌 配置了 IM 推送后,Twikoo 默认不再给博主发邮件,如果两个都想要,把
SC_MAIL_NOTIFY设为true - 📌 评论邮箱等于
BLOGGER_EMAIL的评论不会推送(按邮箱判断,不看登录状态),测试时换一个邮箱、以访客身份发一条;用隐身窗口只是为了不带上浏览器记住的昵称和邮箱 - 📌 推送内容里包含评论者的邮箱和 IP(Twikoo 的推送模板写死的),所以 chat id 只填你自己和机器人的私聊,不要推到群组或频道
- 📌 Telegram 消息用的是 Markdown 解析模式,而 Twikoo 只转义了
*和_。如果评论里有奇怪的方括号、反引号,推送可能会失败,不过评论本身照常发表
4.10 邮件通知(HTTP API)
如果还是想发邮件,只能用 HTTP 邮件 API:
| 配置项 | 值 |
|---|---|
SMTP_SERVICE |
Resend / SendGrid / MailChannels |
SMTP_PASS |
对应服务的 API Key |
SMTP_USER |
随便填,但不能为空 |
SENDER_EMAIL |
发件地址(需要在邮件服务那边验证过域名) |
SENDER_NAME |
发件人名称,不填的话发件人会显示成 undefined |
SMTP_HOST / SMTP_PORT 在 Workers 上配什么都没用,直连 SMTP 是建立不了连接的。
⚠️ 2.0.15 的源码里,SendGrid / MailChannels 通道会把
"发件人名称" <发件地址>整串当成邮箱地址提交。管理面板的测试邮件能过,真正的评论通知却很可能被接口拒绝(400)。目前建议用 Resend。
4.11 以后怎么升级
cd twikoo-src |
然后别忘了:
- 把博客里前端 CDN 的版本号改成同一个版本(见第五章)
- 看一眼 release notes,有没有数据库结构变更或配置项变化
wrangler.toml是自己写的、没有被 Git 跟踪的文件,切换 tag 不会覆盖它,但最好另外备份一份。如果哪天官方仓库自己加了wrangler.toml,checkout 会因为同名未跟踪文件而中止,先把自己的挪走再切换release-set-version.mjs只能在版本号为 0.0.0 的干净源码上运行,所以升级前一定要先还原(上面的git checkout -- .就是干这个的)
五、Butterfly 配置
5.1 修改 _config.butterfly.yml
comments: |
我的博客是 GitHub Actions 构建的,CI 里还有一份配置模板,那份也要同步改。
Butterfly 3.6.0 初始化 Twikoo 时不传 path,所以 Twikoo 用的是 window.location.pathname,也就是经过百分号编码的路径。这一点在导入旧评论时很关键(见第六章)。
5.2 坑:改了配置,页面上还是 Waline
改完配置一构建,页面上显示的还是 Waline。查了半天,原因在 Hexo 的配置合并逻辑上。
Hexo 5 会把站点的 _config.butterfly.yml 和主题自带的 themes/butterfly/_config.yml 做深度合并,但 hexo-util 的 deepMerge 对数组的处理不是替换,而是去重后追加:
// hexo-util deepMerge 对数组的行为(简化示意) |
主题自带的配置里写的是 [Waline],站点配置是 [Twikoo],合并后变成 [Waline, Twikoo]。而 Butterfly 永远拿 use[0] 当默认评论系统,所以显示的还是 Waline。
解决办法:把 themes/butterfly/_config.yml 里的 comments.use 也改成 Twikoo。以后换评论系统,两个文件都要检查。
5.3 用 twikoo.min.js,并锁定版本号
| 文件 | 大小 | 区别 |
|---|---|---|
twikoo.min.js |
约 357 KB | 不含腾讯云 CloudBase SDK |
twikoo.all.min.js |
约 821 KB | 含 CloudBase SDK |
后端不是腾讯云的话,用 twikoo.min.js 就够了,体积小了一半多。
CDN 地址一定要写死版本号(twikoo@2.0.15),并且和服务端版本保持一致。不写版本号的话,jsDelivr 哪天给你换成了新版,前后端不匹配就可能出问题。
5.4 坑:部署完访客看到的还是旧评论区
博客重新部署后,我用正常浏览器打开,评论区还是 Waline。看了一下响应头:
cf-cache-status: HIT |
原来 Cloudflare 把博客的 HTML 缓存了 5 小时。解决办法:Cloudflare Dashboard → 你的域名 → Caching → Configuration → Purge Everything。
六、旧评论迁移
6.1 为什么不用后台的导入功能?
Twikoo 管理面板的「导入」确实支持 Valine 的 JSON。但在 D1 上,每条评论是一次插入查询,而整个导入是在一次请求里完成的。D1 文档写的免费版每次调用最多 50 次查询(虽然官方文档之间有出入),再加上 10 ms 的 CPU 限制,500 多条评论一次导完,有可能导到一半就停了。
而且我的数据里还有一堆格式问题要处理(下面细说),所以干脆在本地生成 SQL 文件,用 wrangler 直接执行。
6.2 Twikoo 的评论表
| 字段 | 说明 |
|---|---|
_id |
主键(TEXT),我直接沿用了 LeanCloud 原来的 objectId |
nick / mail / mailMd5 / link / avatar |
昵称、邮箱、邮箱 MD5、网址、头像 |
ua / ip / ipRegion |
浏览器 UA、IP、IP 属地 |
master |
是否博主(0 / 1) |
url / href |
页面路径 / 完整地址 |
comment |
评论内容,HTML |
pid / rid |
直接父评论 / 所在楼层的根评论 |
"like" / top / isSpam |
点赞('[]')、置顶、是否垃圾评论 |
created / updated |
毫秒时间戳(INTEGER) |
extra |
扩展字段(JSON) |
6.3 转换时踩的九个坑
① 路径编码不一致(影响 40 条)
Valine 存的是解码后的路径,比如 /支持/;Twikoo 匹配的是 location.pathname,也就是 /%E6%94%AF%E6%8C%81/。不处理的话,所有中文路径页面下的评论都看不到。统一用 encodeURI(decodeURI(url)) 转换。
② 内容格式混杂
- 早期 Valine 存的是 HTML,回复会带一个前缀
<p><a class="at" href="#id">@nick </a> ,。Twikoo 本身会显示「回复 @xxx」,所以把这个前缀去掉了(73 条回复) - 后期 Valine(1.4 之后)和 Waline 存的是原始 Markdown / 纯文本(294 条),用
marked渲染成 HTML,选项和 Twikoo 前端一致(gfm: true, breaks: true) - Waline 的回复带
[@nick](#id):前缀,同样去掉
③ Valine 表情短码
像 :doge:、:bad laugh: 这样的短码(34 条评论、21 种),Twikoo 不认识,会原样显示成文字。我把它们映射成了 Valine 默认表情包的图片地址。
④ 统一消毒
所有内容最后都过一遍 DOMPurify,选项和 Twikoo 服务端一致(禁止 style 标签和 style 属性)。
⑤ 评论里贴的 base64 截图
有 13 条评论直接粘贴了 data:image 格式的 base64 图片,其中 9 条超过 40 KB(最大的一条 393 KB),转成十六进制后单条 INSERT 放不下。D1 单条 SQL 语句上限 100 KB(单行可以到 2 MB,所以存得下,只是一条语句塞不进去)。办法是:先插入一条内容为空的记录,再用 UPDATE ... SET comment = comment || <分段> 每次追加约 40 KB。第一段用赋值而不是追加,这样重复执行也不会重复拼接。
⑥ SQL 转义
所有文本值都写成 CAST(X'<utf8 的十六进制>' AS TEXT),引号、换行、反斜杠全都不用管。注意 CAST 不能省:直接写 X'...' 存进去的是 BLOB,和 url、_id 的文本比较永远匹配不上,评论就「消失」了。
⑦ 楼中楼关系
Twikoo 的 pid 是直接回复的那条评论,rid 是这一楼的根评论。Waline 用的是自增整数 id,我把它们映射回 LeanCloud 原来的 objectId(作为 _id),这样新旧数据的回复关系能对上;Waline 时期新增的 6 条没有 objectId,单独分配了 _id。
另外 LeanCloud 里有 5 条回复的父评论早就被删了,4 月导入 Waline 时它们已经被改成了顶层评论,这次沿用。自己迁移时要注意处理这种情况,否则会断链。
⑧ 时间戳
Twikoo 用毫秒时间戳。老评论直接用 LeanCloud 里精确的 createdAt,Waline 时期的 6 条用 Neon 里导出的 UTC 时间(就是 1.6 节那个时区坑)。
⑨ 博主标记和头像
我自己的 50 条评论设置 master = 1,显示博主标识。头像字段统一留空,让 Twikoo 按邮箱哈希走 GRAVATAR_CDN 生成头像。不要把 qlogo.cn/...&nk=<QQ号> 这种地址写进 avatar:Twikoo 的公开接口不返回邮箱,但会原样返回 avatar,等于把读者的 QQ 号(也就是 QQ 邮箱)公开了。
6.4 关键代码(示意)
转换脚本用 Node.js 写的,核心就这几个函数(省略了读文件和拼装 INSERT 的部分):
import { marked } from 'marked' |
💡 十六进制编码会让体积翻倍,所以 40 KB 一段,生成的语句约 80 KB,刚好在 100 KB 以内。这里按字符切分,base64 图片是纯 ASCII,所以没问题;如果要切中文长文,得按字节算。
生成的 import.sql 大致长这样:
INSERT OR IGNORE INTO "comment" (_id, nick, url, comment, pid, rid, master, created, updated, ...) |
用 INSERT OR IGNORE 的好处是可以放心重跑,已经存在的记录会被跳过。
6.5 先本地,再线上
# 1. 先导入本地模拟库,检查数量和回复关系 |
检查楼中楼有没有断链(pid 和 rid 都要查,Twikoo 按 rid 给回复分楼),可以用这条 SQL(结果应为 0):
SELECT COUNT(*) FROM comment c |
⚠️ 导入线上库期间,wrangler 会提示数据库暂时无法响应查询。数据量小的话很快,但最好挑访问少的时候做。
6.6 迁移结果
| 项目 | 数量 |
|---|---|
| 评论总数 | 536 |
| 回复 | 170 |
| 断链的回复 | 0 |
| 垃圾评论 | 1 |
| 博主评论 | 50 |
导入后我在线上抽查了几个页面:比如「支持」页面显示 21 条顶层评论,E5 那篇文章有 54 个评论楼,回复和表情都正常显示。
6.7 备份
- 💾 LeanCloud 的导出、Neon 的导出、生成的 SQL 都在本地留了一份。这些文件,以及
wrangler d1 export和管理面板「导出」得到的文件,都含有评论者的邮箱和 IP,千万不要提交到公开仓库(博客源码仓库就是公开的) - 🗑️ 迁移完成、备份确认无误后,删掉 LeanCloud 上的评论数据和不再使用的 Neon 项目,它们同样存着评论者的邮箱和 IP。主题配置里旧的
valine.appId/appKey也可以删掉,不过它们还留在 Git 历史里,所以删掉 LeanCloud 上的数据才是关键 - 📤 定期在 Twikoo 管理面板里点「导出」
- 🗄️ 也可以用 wrangler 直接导出整个 D1:
npx wrangler d1 export twikoo --remote --output=twikoo-backup.sql |
D1 自带 Time Travel,免费版可以恢复到 7 天内的任意时间点,算是最后一道保险。
七、踩坑汇总
数据转换的坑见 6.3,这里只列部署和配置时最容易遇到的问题,方便对照报错查:
| 问题 | 原因 | 解决 |
|---|---|---|
| pnpm install 报 Node 版本不符 | 根 package.json 要求 node >=26 |
用 Node 26(便携版即可,只用于构建) |
| 管理面板提示前后端版本不一致 | 源码构建的服务端版本号是 0.0.0 | 构建前执行 release-set-version.mjs |
| 升级时版本脚本报「发布基线校验失败」 | 上次 release-set-version.mjs 改过的 package.json 没还原 |
先 git checkout -- . 再切换 tag |
部署报 [code: 100117] |
旧的 Vercel CNAME 记录还在 | 在 DNS 里删掉旧记录后重新部署 |
配了 routes 后 workers.dev 地址不可用 |
有 routes 时 workers_dev 默认为 false |
显式写 workers_dev = true |
不加 --remote 时线上库里没有表 |
wrangler d1 execute 默认操作本地库 |
加 --remote |
| 改成 Twikoo 后页面还是 Waline | Hexo 合并主题配置时数组是去重追加,变成 [Waline, Twikoo] |
主题自带的 _config.yml 也改掉 |
| 部署后访客看到的还是旧评论区 | Cloudflare 缓存了 HTML 5 小时 | Purge Everything |
| 收不到邮件通知 | Workers 上不能直连 SMTP | 用 HTTP 邮件 API,或者改用 Telegram |
| SendGrid / MailChannels 测试邮件能发,评论通知可能发不出 | 2.0.15 把「名称 + 地址」整串当成发件地址提交 | 改用 Resend |
| Telegram 推送可能失败 | Markdown 解析模式下只转义了 * 和 _ |
评论本身不受影响,可以忽略 |
八、总结
折腾一圈下来,评论区终于又活过来了,而且五年来的 536 条评论一条没丢。几点体会:
- ☁️ 免费 PaaS 账号说没就没,而且可能连个解释都没有。评论后端最好放在自己本来就离不开的基础设施上(我这里就是托管 DNS 的那个 Cloudflare 账号),也不要和其他有风险的项目混在同一个账号里
- 📝 把部署信息写下来:域名指向哪个平台、数据库在哪、怎么登录。我才过了半年就全忘了
- 💾 定期导出备份:管理面板点「导出」或者
wrangler d1 export,别再把数据安全寄托在运气上 - 📌 前后端版本要锁定且一致,主题配置的合并行为要心里有数,切换评论系统后记得清 CDN 缓存
- 🔔 给评论区加个监控:这次评论区挂了一个多月我都没发现。Telegram 推送只能告诉我「有新评论」,后端挂了它是不会响的,所以最好再加一个定时请求评论接口的监控(UptimeRobot 之类的服务,或者用 Workers Cron 自己写一个)
如果你也还在用 Valine,别拖了,2027-01-12 之前一定要迁走。有问题欢迎在下面评论区留言,这次评论区是真的能用了 😄





