告别不蒜子,自己给博客记个数

不蒜子好像又挂掉了。

之前已经遇到过几次。网页能打开,文章能看,偏偏浏览量那里空着。有时候浏览器还转很久,检查请求,才发现统计接口没有正常返回。以前看到过 504 Gateway Timeout,这次又得等。

本来想,免费的东西,等等也没什么。但一会儿有数字,一会儿没有,总归有点难受。于是又问 Codex:能不能用 GitHub、Cloudflare,或者办公室的Linux,自己记录一下?ai agent 我是离不开了。

不过有个前提:不能一换服务,之前的数字就全变成零。虽然也不是什么惊人的访问量,但好歹积累了这么久啊。

最后折腾成了下面这个样子AI帮忙折腾的,我自己估计得折腾很久。记一下,省得下次再问一遍。

网页还在 GitHub,计数交给 Cloudflare

博客仍然是Markdown写文章,Python生成网页,再把静态网站部署到GitHub Pages。

另外加一个 Cloudflare Worker 接收访问请求,用D1数据库保存计数。大概是这样:

flowchart TD A[读者打开博客] --> B[GitHub Pages 提供网页] B --> C[浏览器加载计数脚本] C --> D[Cloudflare Worker 校验请求] D --> E[D1 保存访问记录并累计] E --> F[返回数字并显示在页面上]

这里的 Worker 不是再转头去问不蒜子,而是操作自己的数据库。否则只是多绕了一层,原来的服务不返回,最后还是没有数字。

没有把主服务放在办公室的 Fedora 上,是因为它也有自己的脾气。办公室可能停电,网络也可能断,电脑还得重启。之前图片服务就用了这台机器,不想再让访问统计也跟着它一起上下班。

这也不代表换到 Cloudflare 就永远不会出问题。只是现在计数保存在哪里、如何备份,我至少能自己查了。

先把授权弄明白

管理 Worker 和 D1 用的是 Cloudflare 的官方工具 Wrangler。这次使用的是 4.154.0。下面的命令都在博客项目根目录运行,默认已经有 Node.js 和 npm。

一开始,授权页面显示成功了,本机却没有拿到凭据。还有一次浏览器跳到 localhost:8976,直接提示:

This site can't be reached
localhost refused to connect
ERR_CONNECTION_REFUSED

这个地址是登录过程临时使用的回调地址,不是博客挂了。但只看浏览器上的“授权成功”也不够,还得确认命令行真的接到了。

后来换成官方的设备授权方式,不再依赖浏览器访问本机回调端口:

npx wrangler@4.154.0 login --device --use-keyring \
  --scopes account:read user:read workers_scripts:write \
  workers_routes:write d1:write zone:read

终端会给出设备码和授权页面。在浏览器里核对设备码,确认需要的权限,再点击 Authorize。等待中的命令不要提前关掉;这次也确实遇到过授权页报错,不能保证换一种方式就一次成功。

最后终端返回 Successfully logged in.,再检查:

npx wrangler@4.154.0 whoami

确认账号和权限都对,这才算连上了。--use-keyring 用于把凭据以加密文件配合系统钥匙串保存,不把登录密钥写进博客仓库。设备码、密钥和完整授权回调地址,也不需要贴进文章里。

数据库、脚本和部署配置

这次新建了独立的 Worker 和 D1 数据库,都叫 seisamuse-counter-d1。没有覆盖原来的旧 Worker。

相关文件各有分工:

文件 用途
scripts/article_counter_20261028.js Worker 的请求处理和计数逻辑
scripts/article_counter_schema_20261028.sql 数据表和触发器
scripts/article_counter_manage_20261028.py 本地初始化、历史基数导入和备份工具
scripts/article_counter_worker_20261028.toml 云端绑定与部署配置
content/scripts/article_counter_20261028.js 浏览器端发送请求、显示数字
data/counter_20261028/history.json 手动确认的历史估算参数

这些是本次配套写好的脚本,下面的命令是在已有它们的前提下记录配置,不是只复制一个 TOML 文件就能得到计数器。

下面创建数据库和导入初始数据的操作,只用于第一次配置的空数据库。现在的数据库已经在计数,以后维护不要整段重新执行。

先创建数据库:

npx wrangler@4.154.0 d1 create seisamuse-counter-d1 \
  --config scripts/article_counter_worker_20261028.toml

把返回的数据库 ID 填进 TOML。公开记录里用占位符代替实际 ID,主要配置如下:

name = "seisamuse-counter-d1"
account_id = "替换为自己的账号ID"
main = "article_counter_20261028.js"
compatibility_date = "2026-10-28"
workers_dev = false

[vars]
COLLECTION_ENABLED = "false"

[[d1_databases]]
binding = "DB"
database_name = "seisamuse-counter-d1"
database_id = "替换为新建数据库的ID"

[[ratelimits]]
name = "LIMITER"
namespace_id = "20261028"
simple = { limit = 300, period = 60 }

[observability]
enabled = false

[[routes]]
pattern = "stats.seis-jun.xyz"
custom_domain = true

main 相对于这个配置文件所在的目录;DB 和 LIMITER 则对应脚本使用的绑定。域名也要换成自己账号中可用的域名,不要照抄我的地址。

这里先把采集关着。数据库还没准备好,就别着急往里面写。

初始化时,先从生成的 site/ 整理页面路径,在本地创建数据库,再导入手动确认的历史基数。管理工具的处理顺序是:

init → import-history(先做完整备份)→ export-seed

初始化时间需要明确指定,历史参数文件中的时间也要与它一致。这次保存的是 2026-10-28T09:18:11.000Z,即北京时间 17:18:11。它是新库的初始化边界,不是从原统计服务导出的时间。

export-seed 生成 SQL 后,再导入刚创建的空 D1:

npx wrangler@4.154.0 d1 execute seisamuse-counter-d1 --remote \
  --config scripts/article_counter_worker_20261028.toml \
  --file data/counter_20261028/initial-seed.sql

本地 .sqlite 文件不是直接上传就能用,D1 导入需要 SQL;这点也可以查官方的导入导出说明。初始 SQL 不是日常发布文件,更不能拿它反复初始化正在使用的库。

然后部署 Worker:

npx wrangler@4.154.0 deploy \
  --config scripts/article_counter_worker_20261028.toml

先在采集关闭的状态下检查绑定和历史基数,确认没问题,再把 COLLECTION_ENABLED 改为 "true",重新部署。这样不至于一边测试,一边把测试访问也算进去。

网页怎么接上,怎么检查

浏览器端的入口放在公共模板 templates/base.html 中,所以文章页和其他页面不需要各自抄一遍:

?v=2 是这次修改显示样式后加的版本标记,避免浏览器还沿用旧脚本。这个页面不再加载旧计数入口,也不在新接口失败后偷偷回退去请求原服务。

接口分成三种,不要拿错了:

  • POST /count:正式网页提交一次浏览,写入计数。
  • GET /views?path=/:只查询某个路径的统计,不增加计数。
  • GET /health:检查服务绑定和采集开关,不查询数据库里的数值。

平时排查可以先用这两个只读请求:

curl --max-time 15 'https://stats.seis-jun.xyz/health'
curl --max-time 15 'https://stats.seis-jun.xyz/views?path=/'

health 返回正常,并不等于数据库查询一定正常,还要看 views。第一次导入后,实际核对到历史浏览量是 80500、历史访客数是 40500,新增计数都是零,再开启采集。

本地 localhost 或 127.0.0.1 预览只读取统计,不提交浏览。否则我自己改个排版、刷新几十次,最后还以为突然来了不少读者。

正式发布一篇新文章时,Worker 会在首次计数前检查它是否确实是正式站点上能访问的 HTML 页面,成功后再登记。以后正常写文章,不需要每次手动把地址填进数据库。

旧数据没有想象中那么好搬

这次最纠结的其实不是部署,是旧数字怎么办。

我记得前几天总浏览量有八万零几百,访客数有四万零几百。最后确认,以 80,500 次浏览、40,500 位访客 作为历史起点。

但这两个数是根据回忆确认的估算,不是从不蒜子后台完整导出的精确记录。新系统把历史基数和新增记录分开保存,再加起来显示,不假装这些数字全是自己从头数出来的。

单篇文章更麻烦。我并没有每篇文章的可靠旧计数,因此没法把八万多次平均分给所有文章,也不能把未知的旧浏览量解释成零。现在单篇页面显示的,是新系统启用后记录到的浏览量,不是这篇文章从发表至今的累计数。

页面上最后只保留数字,估算来源和单篇统计的起点放在悬停提示及隐私说明里。去掉前面的提示词,只是改了显示方式,不是把旧数据变出来了。

以后如果原服务恢复,并且确实能读到可核实的单篇记录,再单独判断能不能补回、怎样避免重复。现在没有安排自动抓取,也不为了等它恢复而停掉新的计数。这个遗憾先留着。

还有,访客数也不等于精确的人数。目前是在浏览器本地保存一个有效期为 365 天的随机编号,服务端保存它的 SHA-256 摘要。同一个编号再次访问,不再新增一个访客。

清除网站数据、更换浏览器或设备,都可能产生新的编号;旧访客与新编号之间也无法去重。浏览器禁止本地存储时,则只记浏览量,不把每次访问都硬算成一个新访客。这个数字适合大概看看,不适合拿来做人口普查。

计数数据库不保存 IP、User-Agent 或随机浏览器编号原文,也不发送网址里的查询参数和页内锚点。不过请求经过 Cloudflare,它在网络传输和安全防护时仍可能处理网络信息,不能因此说成完全没有隐私问题。换了统计方式,隐私说明也得跟着更新。

以后要记得备份

现在数字存进了自己的数据库,至少可以把它导出来了。完整备份用:

npx wrangler@4.154.0 d1 export seisamuse-counter-d1 --remote \
  --config scripts/article_counter_worker_20261028.toml \
  --output backups/counter-full-20261028-203000.sql

这个输出名只是示例,每次要换成新的日期时间。备份放在非公开目录,不跟着网页发布,也不要覆盖上一次的文件。云端导出会暂时阻塞数据库的其他请求,尽量挑访问少的时候做。

还要分清楚:管理脚本的 export 是 JSON 汇总,不是完整备份;backup/restore 面向完整的本地 SQLite;云端完整备份则是上面的 D1 SQL 导出。要恢复时,先导入一个新数据库,核对好了再切换,不直接覆盖正在计数的库。

这次已经导出过云端初始备份,并实际导回本地检查过完整性,两个历史基数都还在。不过定时备份目前还没设置,不能写了“以后要备份”,就当它已经每天自动备份了。

以后发博客,仍然运行原来的 do_deploy.bash。它负责构建和发布静态网页;只有计数服务的代码或配置变了,才另外部署Worker。网页源码、数据库记录和数据库备份,是三件不同的东西。

当然,这个小计数器也不是绝对准确。统计请求可能被拦截,网络会出错,机器人也可能混进来;限流不是完全防刷的保证。接口暂时不可用时,脚本最多等待八秒,有最近的成功缓存就显示缓存,并在悬停提示里注明时间,不一直等,也不自动反复重试。

先这样吧。博客照常写,数字继续往下记。比起盯着数字不动,我更希望下一次打开这些配置,是为了备份,而不是又来查哪里挂了。

参与讨论