写博客静态这事,谁写谁知道。最烦的不是写,是发布。
飞书写完一篇,复制到 Markdown 编辑器,格式十有八九要乱。图片更是噩梦——得一张张保存、上传图床、替换链接。改一次文章,这流程就得重来一遍。
后来我写了这么个东西:一个跑在 Cloudflare Workers 上的同步服务。现在流程变成了这样:
飞书写完 -> 给机器人发句”同步” -> 喝口水 -> 博客已经更新了。
功能
这是个能用的自动化系统,不是玩具:
- 双模式同步:支持文章(posts)和相册(photos)两种文档类型
- 指令驱动:在飞书私聊里发指令,机器人会回复同步结果
- 富文本解析:支持多级标题、加粗斜体删除线、代码、链接、@人、引用、待办,还有嵌套表格
- 增量更新:同步时会对比飞书文档修改时间和 GitHub 提交时间,没改动的文章直接跳过
- 图片处理:自动下载图片、上传 R2、替换链接(R2 优先,GitHub 为备选)
- 元数据:在飞书文档开头写两行字,就能自动生成 Frontmatter,支持分类、标签、标题、地点
- 删除同步:删了飞书文档,发个指令就能删除 GitHub 里的对应文章
设计
用 Cloudflare Workers 的原因很简单:免服务器、速度快、成本低。Workers 和 R2 在同一个内网,图片上传不需要转 Base64,几十张图几秒钟就传完了。
架构
[飞书文档] → [飞书机器人] → [Webhook] → [Cloudflare Workers (Hono)]
│
├─ 鉴权(HMAC-SHA256)
├─ 拉取 Block 数据
├─ 解析成 Markdown
├─ 下载图片
├─ 上传 R2 / GitHub
├─ 拼接 Frontmatter
├─ 推送 GitHub
└─ 触发 Actions → [博客更新] 路由
Workers 使用 Hono 框架,路由清晰:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /health | 健康检查 |
GET | /webhook/feishu | 飞书 URL 验签(GET 参数 challenge) |
POST | /webhook/feishu | 飞书消息处理 |
Block 解析
飞书文档是树状结构,段落里可能嵌套加粗、链接,列表里可能嵌套子列表。代码里用的是递归下降解析器,不是正则匹配。
处理表格时,读取 column_size 按列数遍历子节点,提取内容拼成 Markdown,然后直接 return 阻断递归,避免重复渲染。列表也是类似的思路——增加缩进级别,递归处理子节点。
增量更新
全量同步时,判断是否更新很简单:
- 取飞书文档的
updated_at - 取 GitHub 上该文件的最后一次 commit 时间
- 如果 GitHub 时间 >= 飞书时间,跳过
这样做还有个好处:如果直接在 GitHub 上改了个错别字,再触发飞书同步,代码会发现 GitHub 更新了,自动跳过那篇,不会覆盖你的修改。
相册模式
相册文档不生成 Markdown 内容,而是提取文档中以图片 URL 开头的内容行作为图片列表。图片下载后直接上传到 R2 的 photos/{slug}/ 目录,生成的文件结构:
photos/{slug}/
├── index.md # 元数据和图片列表
└── 0.jpg # 实际图片文件 部署
准备工作
- 飞书企业管理员权限
- GitHub 账号(需要两个仓库:一个放文章 Markdown,一个放前端代码)
- Cloudflare 账号(免费版即可)
配置飞书
- 在飞书开放平台创建企业自建应用,记下 App ID 和 App Secret
- 添加”机器人”能力
- 开通权限:
docx:document:readonly、drive:drive、wiki:wiki:readonly、im:message、im:message:send_as_bot(需要管理员审批) - 配置事件订阅,添加
im.message.receive_v1,URL 先填占位符 - 发布应用
配置 GitHub
生成一个 Personal Access Token (Classic),勾上 repo 和 workflow 权限。
使用
指令
在飞书找到机器人,发送:
同步或sync:全量同步文章和相册同步 <关键词>:搜索并同步单篇文章同步相册 <关键词>:搜索并同步单个相册删除 <文章标题>:删除文章(需要回复 Y 确认)删除 <document_id>:直接删除
元数据
在文档最开头写:
分类: 随笔
标签: 生活, 技术分享
草稿: true
# 标题
正文... 支持的元数据字段:
分类:文章分类标签:逗号分隔的标签列表草稿:设为true则生成draft: truetitle:手动指定文章标题地点:用于相册的地点信息
标题可以不写,程序会自动用正文里第一个 # 后面的内容。标签用英文逗号分隔。
FAQ
发”同步”没反应? 检查飞书权限是否审批通过、应用是否发布、Webhook URL 是否配置正确。
一直显示”跳过”? 说明 GitHub 上的提交时间晚于飞书文档的修改时间。如果确定改了没同步,用单篇强制同步。
R2 配置失败? wrangler.toml 里 bucket_name 只填 R2 的纯名字;R2_PUBLIC_URL 才填访问域名。图片会优先上传 R2,如果 R2 不可用则自动 fallback 到 GitHub。
KV 的作用是什么? KV 用于存储删除操作的待确认状态(5 分钟过期)。如果不用删除功能,可以不配置 KV。
相册和文章有什么区别? 文章同步到 posts/ 目录,会解析富文本内容生成完整 Markdown。相册同步到 photos/ 目录,仅提取图片 URL,不生成正文内容。
这套东西让我从繁琐的发布流程里脱了身。现在只管写,脏活累活都交给代码。