给博客造一个小记发布台
项目大体全部由 GLM 5.3 Flash 完成,这篇文章的初稿也由其写作完成。表达上虽然经过人工修缮,但是很多地方还是没什么人味)就这样吧。这模型的能力也挺垃圾的,不是送 Token 我不用。
起因:小记有了,发布太麻烦
之前给博客添加了一个信息瀑布流碎碎念的 moments 功能,但是当时没有管后续的发布方式,还是很原始的编辑 YAML 文件的形式,对齐格式都有点折磨。
所以这次的目标很明确:做一个网页发布台,手机电脑都能用,写完点一下发布,图片自动进图床,YAML 自动提交。
第一轮方案设计
动手前先做方案。核心架构要嵌入目前博客部署的 workflow:博客是纯静态的,main 推送 → GitHub Actions 构建 → gh-pages → GitHub Pages 和 Cloudflare Pages 双端展示。
所以发布台的本质只能是把图片收进图床,把一条新记录提交进 moments.yml,然后让现有的 CI 自动重建。
通过和 Agent 的问答定下来三个选择:
- 认证方式:发布台必须只有我能用。选了 Cloudflare Access + GitHub 登录(Cloudflare Zerotrust,后面细说)。
- 功能范围:发布 + 编辑 + 删除全做,一步到位。
- 图床:图片直接进我已有的 R2 桶
image.wendaining.top,用它的自定义域对外提供。
另外有两个实现细节:
- 图片延迟上传:选图、粘贴、拖入的图片只暂存在浏览器内存里,可以随意增删重排,直到点「发布」才真正压缩上传。中途放弃不会在 R2 里留垃圾。
- 日期锁定:编辑已有小记时日期字段只读,这主要是因为一个单独的 moment 的域名是绑定在日期上的。
开工前的侦察
先摸了一圈家底,几个结论直接影响了后面的走向:
- 本机
ghCLI 已经用我的账号登录,token 有repo+workflow权限。这意味着 GitHub 侧的大部分操作(建仓库、存 secrets、推 workflow)可以让 AI 直接代劳。- 这里 Agent 说需要使用 Fine-Grained PAT,但是我这里嫌麻烦,说可以用 gh cli 的 token 我就直接用了,但是这个似乎是有过期风险的。
- (而且我也其实不是很清楚这个 Token 的作用是什么,问 LLM,得到的 TLDR 式答复是「你的 token 就是“给 AI 一把你的 GitHub 操作钥匙”;
repo + workflow表示这把钥匙能改代码仓库和 GitHub Actions,Fine-Grained PAT 只是把这把钥匙切得更细。」,好吧。
- Agent 装了 Cloudflare 官方插件,agent 直接操作我的 Cloudflare 账号。
坑一:Agent 的 Tool List 动态加载出错
计划阶段我问 AI:Cloudflare 的账号管理 MCP 能不能用?它当时的判断是「我的工具列表里只有文档检索,账号管理类工具没挂载」,然后开始疯狂查文档来佐证这个结论。
这个确实很神秘,按理来说一个 Agent 的 Tool List 是随着对话动态加载的,而且我的 MCP 也没有很多,全部加载进去是够的,这个只能是 bug。我重启一下,就全出来了。
Cloudflare One 解析
让 LLM 写的解析,其实基本就是 Cf 官方文档的翻译(?,加上一些解析。
Cloudflare One 是 Cloudflare 的企业安全产品套件的统称, marketing 名字叫「Zero Trust SASE」,拆开看就三件事:
- Zero Trust:「零信任」是一种安全模型——不再假设「内网里的东西都可信」,而是每个访问请求都要验证身份和权限,不管你从哪来。
- Access:零信任模型里最常用的一个功能,本质上是一个挡在你应用前面的身份验证关卡(术语叫 Identity-Aware Proxy)。访客先被拦下来登录,登录信息符合策略才放行到后面的应用,否则连你的服务器长什么样都看不到。
- 组织(Organization):你账号下的 Zero Trust 实体,有一个全局唯一的团队域(形如
xxx.cloudflareaccess.com),所有登录回跳都走这个域。
对个人博客来说,这套东西免费额度完全够用,效果是:给发布台套上一层「只有我能打开」的壳,一行认证代码都不用自己写。
实际操作顺序是这样的:
- 控制台点一下「Enable Access」,选免费计划(不用绑卡)。这一步会自动创建组织并分配团队域——**我的是 Cloudflare 随机生成的
wild-rice-7d8e.cloudflareaccess.com,不过之后可以改,问题不大。 - 添加 Identity Provider(登录方式):告诉 Access「用什么登录」。选 GitHub 的话,需要自己在 GitHub 上建一个 OAuth App(拿到 Client ID 和 Secret 填给 Access),OAuth App 的回调地址填
https://你的团队域/cdn-cgi/access/callback。 - 创建自托管(self-hosted)Access 应用:声明「这个域名/路径受保护」,并在应用上挂策略。
- 策略(Policy):规则形如「Allow + 满足某条件」。我的配置是邮箱精确匹配我自己的 GitHub 主邮箱;另外还有一种
non_identity决策,专门给服务令牌(Service Token)用——不走浏览器登录的程序化访问,用 Client ID + Secret 换一个 JWT,E2E 测试全靠它。
Worker 如何绑定资源
先解释一下 Worker 的 JS 代码一般是如何上传的:
- 通过 Wrangler 上传
- 使用 Cloudflare MCP 提供的 API 上传
总之都是提交上去,而不是和 Git 仓库绑定什么的。
另外提及一下 Cloudflare Worker 的绑定(Binding):
Worker 本身是一段跑在 Cloudflare 边缘的 JS,绑定就是把外部资源「挂」进它的运行环境,代码里直接当变量用:
r2_bucket绑定:代码里env.IMAGES.put(key, bytes)一行就写入 R2 桶,不用配 S3 密钥、不用签名。secret_text绑定:加密的环境变量,存 GitHub token 这种敏感值。plain_text绑定:普通配置(图床域名、博客地址之类)。
部署这次不用 wrangler ,全走 REST API,三步:
POST /accounts/{id}/workers/workers:创建 Worker 资源(此时还没有代码)。POST .../workers/{id}/versions:上传一个版本,包含代码模块(base64)和全部绑定。POST .../scripts/{name}/deployments:把流量 100% 指向这个版本。
再补:PUT /accounts/{id}/workers/domains 把 moments.wendain.ing 绑成 Worker 的自定义域(DNS 和证书自动配);关掉 workers.dev 预览入口,让发布台只剩 Access 保护的那一个门(moments.wendain.ing)。
坑二:LLM 尝试直接 base64 编码
这里莫名其妙的,我也没管过程,完成之后大概理解了一下到底是干嘛:
我们上一步说是需要上传 JS 代码给 Worker,通过一个 HTTP POST 请求。但是,这个 HTTP 请求里面如何携带 JS 文件?
本来一个合理的做法是直接放在 POST 请求的 content: 里面,但是实际 Cloudflare Workers API 的某些上传接口要求源码内容使用 base64 编码。于是,原始 JS 代码就使用 base64 编码传上去。
但是这模型纯逆天,自己用思考而非工具调用来完成这个 Base64 编码的过程,结果就搞出来一坨,一直报 malformed base64 content。
最终的解法:
- 工具调用里内嵌源码原文(可读、可检查,而不是 base64 密文);
- 让 Cloudflare 沙箱在执行时现场编码成 base64;
- 沙箱顺手返回每个文件的 SHA-256,本地比对——全一致才继续,不一致就重发对应文件。
这个「哈希校验闭环」后面救了不止一次。包括后来重写生成器脚本时,还踩过一个 shell 转义的坑:在 bash 双引号里写 \n,传到 Node 里变成了真实换行,直接把生成的代码字符串弄断了。解法是把生成器本身写成文件来执行,彻底绕开 shell 转义地狱。
E2E:一次全绿
部署完成后做了完整的端到端测试:创建临时服务令牌 → 上传一张 1×1 PNG → 确认它从图床域名正常回源 → 发布一条测试小记 → 编辑它(确认 ID 不变)→ 删除它(确认 R2 对象同步清理)→ 删令牌。
中间发现 Cloudflare 沙箱的出站 fetch 有白名单,访问不了我的发布台域名,但本地 curl 直达——HTTP 200,后面整个 E2E 都在本地跑通了。
「前端文件在哪?」逆天 GLM 5.3 Flash

这个简直是逆天,跑完之后我一看项目里面全是 JS 代码,我一问,得到这回复,真绷不住了。
这 LLM 为了偷懒,整个界面(HTML + CSS + JS)被内联成了 src/ui.js 里的一个巨型模板字符串。于是让它重构,拆成 HTML CSS JS 就好了。
重构中途还发现一个更隐蔽的 bug:仓库根 .gitignore 里的 public/ 规则(本意是忽略 Hexo 构建产物)把 moments-composer/public/ 也静默忽略了——git add 根本不会加上它,提交里差点没有前端文件。修复方式是把规则锚定为根目录的 /public/。
中场插曲:要不要搬进博客域名下?
中途还讨论过一个方案:取消独立子域名,把发布台挂到 blog.wendain.ing/moments-composer 路径下,让它看起来像博客的一个子应用。可行性结论是可行的——Workers 路由可以按路径挂在 Pages 自定义域上,Worker 优先于静态服务——但需要改造 Worker 的路径处理、重建路径级的 Access 应用,最后决定搁置:现有的独立子域名已经稳定工作,没必要为了好看折腾。这个方案随时可以单独再做。
submodule:让发布台独立成仓
把 moments-composer/ 从博客仓库里拆出去,变成一个 git submodule,指向独立的私有仓库 wendaining/moments-composer。主要是因为 composer 是个自成一体的应用,它的提交历史混在博客内容提交很怪。独立成仓之后,博客仓库只保留一个指针。
几个关键动作:
git subtree split -P moments-composer:把 composer 相关的提交历史从博客仓库里抽出来,推成新仓库的 main——历史不丢。- blog 侧换成 gitlink:
git rm -r旧目录,git submodule add新仓库。Hexo 构建完全不依赖这个目录,blog 的 CI 零改动。 - 重新部署 Worker:仓库结构变了,线上跑的代码要和新仓库对齐。又是「源码内嵌 + 沙箱编码 + SHA-256 比对」那套流程,这次八个文件一次通过。
这一段踩的坑:
- 坑:根
.gitignore的public/静默忽略。就是上面说的那个,git add不报错、不提示,提交里就是没有这几个文件,直到rm -rf之后才发现要靠记忆重建。- 教训:给嵌套目录写忽略规则时,永远用
/锚定根。
- 教训:给嵌套目录写忽略规则时,永远用
- 坑:rebase 被子模块挡住。远端有我在另一台机器推的新提交,rebase 时 git 要把
moments-composer/从「子模块」换回「普通文件目录」,被已填充的子模块工作树挡住,报了一个莫名其妙的「无法分离头指针」。解法是git submodule deinit清空它,rebase 完再--init恢复。 - 坑:detached HEAD。
git submodule update --init恢复子模块时按惯例处于游离头指针,我接着提交的两个 commit 全在游离头上,git push origin main推的还是旧分支——push 显示成功,实际什么都没推上去。直到发现远端树里根本没有.github目录才反应过来。解法是git checkout -B main把分支掰回来。
最后一块拼图:子模块自动对齐
submodule 有个问题(维护成本):composer 仓库更新后,blog 里的指针不会自己动,需要手动 git submodule update --remote 再提交一次。
第一版方案是两边都做 CI:blog 侧一个 workflow 检查并对齐指针,composer 侧推送后通知它。跑起来后发现 blog 侧那份是多余的——而且它内部 git submodule update --remote 克隆私有子仓库时还遇到了凭证问题(checkout 的凭证头覆盖不到子模块克隆),连续失败。
按「少一点是一点」的原则,blog 侧的直接删掉,composer 侧的 workflow 改为自己动手:通过 Git Trees API 把 blog 的 gitlink 直接提交成新的 SHA——不需要克隆 blog,不需要动子模块,三个 API 调用(读当前指针 → 建新 tree 和 commit → PATCH ref)就完成对齐,幂等且失败自动重试。
中间又踩了一个坑:workflow 的 YAML 里写 heredoc,终结符 EOF 被 YAML 的缩进带得不再顶格,shell 直接报 here-document delimited by end-of-file。改成 printf | gh api --input 的管道写法,问题消失。
实测结果:composer 推一个提交,几秒后 blog 的 main 上自动出现一条 chore(composer): sync submodule to xxx,指针同步完成。从此我只需要关心 composer 仓库本身,blog 完全不用管。
收个尾
现在这套东西的最终形态:
moments-composer:独立私有仓库,前端三件套 + Worker 后端五模块 + 部署脚本,测试齐全。- 线上一个 Cloudflare Worker,Access(GitHub 登录)保护,只认我的 GitHub 邮箱;图片落 R2,走独立图床域名。
- blog 里一个 submodule 指针,由 composer 侧 CI 自动保持最新。
- Hexo 那边一行代码没改,校验、ID、永久链接、Giscus 全是原来的体系。