记录灵感是一件高度依赖低阻力的事情。
很多时候,生活中最真挚、最动人的思考往往发生在散步途中、厨房煲汤的间隙、或是哄睡宝宝后的片刻放空。在这些场景下,要求作者打开电脑、创建 Markdown 文件、构思 Frontmatter 元数据、推敲排版格式并执行 Git 提交,存在着极高的心理门槛与操作摩擦。其结果往往是:闪烁的灵感在拿起手机发了一条语音或微信消息后,便随风消散在信息的汪洋中,未能沉淀为长久的个人数字资产。
为了解决这一痛点,我们在个人网站中构建了一套基于微信生态与 Serverless 大模型智能编排的全自动随笔发布流水线:无论何时何地,作者只需在手机微信中随手发送一段口语化的文字随笔或一张手机备忘录截图,系统即可在几十秒内自动完成视觉 OCR 提取、真伪标题判定、文学级呼吸感排版、前言引用块隔离、金句提炼、中英标签生成、Git 自动化提交与全网静态加速构建。
本文全面复盘该系统的探索历程、架构设计与核心技术攻坚细节。
1. 系统总体架构与数据流设计
整个发布系统秉持“纯静态为本、边缘计算为桥、大模型为脑”的设计哲学,不引入任何传统关系型数据库与常驻服务器,全链路实现 100% Serverless 托管与零成本运转。
1.1 核心组件与职责划分
| 系统层次 | 技术选型 | 核心职责与设计考量 |
|---|---|---|
| 输入终端 | 微信 + 企业微信插件 | 用户无感触达。支持发送纯文字随笔与备忘录截图,直接在微信主聊天窗口交互。 |
| 边缘网关 | Cloudflare Workers | 负责企业微信 Webhook 握手与消息加解密、环境变量隔离、5 秒超时竞速控制与任务生命周期挂载。 |
| 智能编排 | DeepSeek 旗舰大模型 | 执行多模态视觉 OCR 提取、显式/隐式标题识别、前言与正文结构化剥离、叙事呼吸感分段与语义化 Slug 提炼。 |
| 版本中枢 | GitHub REST API | 无状态写入。基于稳定输入派生确定性路径与 Write-Once 提交,自动写入标准 Markdown 文件至代码主分支。 |
| 构建分发 | GitHub Actions + Hugo | 触发 24 项全量自动化测试(16 项静态站点质量门 + 8 项 Worker 安全、加解密与智能发文测试),编译为纯静态 HTML 并分发至全球 CDN。 |
1.2 端到端流水线数据流转
| 流水线阶段 | 执行主体 | 核心处理逻辑与关键技术细节 |
|---|---|---|
| 1. 灵感录入与触发 | 个人微信 / 企业微信插件 | 作者在微信聊天窗口发送口语文字随笔或手机备忘录长图截图。 |
| 2. 安全加密推送 | 腾讯微信服务器 | 采用 XML 封装 AES-256 密文并携带 msg_signature、timestamp、nonce 推送至 Webhook 网关。 |
| 3. 边缘验签与竞速 | Cloudflare Workers | 原生 Web Crypto 验签与防重放;启动 4.5 秒超时竞速并挂载 ctx.waitUntil 异步生命周期。 |
| 4. 多模态智能编排 | DeepSeek 视觉/文本大模型 | 执行多模态 OCR 视觉解析、标题智能判定与标点清洗、前言引用块隔离、呼吸感分段与标签生成。 |
| 5. 版本中枢安全写入 | GitHub REST API | 基于稳定输入派生 SHA-256 发布指纹,Base64 编码后以 Write-Once 方式提交标准 Markdown 至 main 分支。 |
| 6. 质量门禁与全网分发 | GitHub Actions + Hugo | 触发 24 项全量自动化测试,250ms 极速编译纯静态 HTML 并部署至全球 CDN 节点。 |
2. 关键技术攻坚与工程实现细节
在将微信原生消息转化为符合排版美学与严苛 CI 门禁的静态 Markdown 过程中,我们攻克了五个核心工程难点。
2.1 难点一:微信 5 秒硬性超时与 Worker 异步竞速机制
企业微信 Webhook 对服务端的同步响应有着极其严苛的 5.0 秒硬性超时限制。一旦服务端耗时超过 5 秒未返回有效的 HTTP 200 XML 响应,微信客户端就会直接提示“服务出现故障”或“请求超时”,并丢弃此次交互。
然而,调用顶级大模型对长篇生活随笔进行深度上下文理解、排版推理并完成 GitHub API 写入,往往需要耗费 3~6 秒。如何在不依赖常驻异步消息队列(如 Kafka / RabbitMQ)的前提下,完美解决这一超时矛盾?
我们利用 Cloudflare Workers 的 Promise.race 竞速机制与 ctx.waitUntil() 任务生命周期延长机制,设计了“双模动态响应管线”:
// 核心超时熔断与后台生命周期挂载
const processPromise = (async () => {
const result = await commitMarkdownToGithub({ rawBody: userText, imageUrl: picUrl, env });
return `随笔已精修发布!\n\n标题:《${result.title}》\n标签:${result.tags.join(' · ')}\n日期:${result.date}\n查看:${result.pageUrl}`;
})();
// 4.5 秒快速竞速(为加解密和网络往返预留安全缓冲)
const timeoutPromise = new Promise(resolve => {
setTimeout(() => {
resolve(`随笔已收到!AI 正在进行深度文学排版与精修,即将发布上站,稍后刷新网站即可看到!`);
}, 4500);
});
// 确保即使 HTTP 连接提前关闭,Worker 也会在后台把 GitHub 提交完整执行完毕
if (ctx && typeof ctx.waitUntil === 'function') {
ctx.waitUntil(processPromise);
}
// 竞速决定同步返回给微信的内容
const replyText = await Promise.race([processPromise, timeoutPromise]);
return buildEncryptedWechatXml(replyText, fromUser, toUser, env);
设计价值:
- 若大模型与网络在 4.5 秒内迅速完成,用户微信将立刻收到包含文章真实标题、分类标签与直达链接的完整卡片;
- 若大模型推理耗时超过 4.5 秒,Worker 会在第 4.5 秒先行返回确认 ACK,而后台的
ctx.waitUntil则保证大模型与 GitHub 提交不受任何干扰地平稳落地。
2.2 难点二:企业微信 32 字节 PKCS#7 与 AES-256-CBC 跨语言加解密
企业微信的通信安全协议采用非标准的变长对齐封装规范:
- 密文结构包含:
16 字节随机串 + 4 字节网络大端序消息长度 + 原始 XML 明文 + CorpID + 32 字节补位; - 填充方式为 32 字节对齐的 PKCS#7 变体,而非通用的 16 字节对齐;
- 签名算法需对
Token、Timestamp、Nonce、Encrypt进行字典序升序拼接后执行 SHA-1 摘要。
为了保证 Worker 在边缘运行时的极致性能(冷启动为 0ms),我们没有引入庞大的 Node.js 兼容第三方包(如 crypto-js),而是直接基于浏览器标准的 Web Crypto API 与原生 TypedArray 实现了全套零依赖加解密套件:
async function decryptWechatPayload(encryptedBase64, encodingAESKey, corpId) {
const aesKeyBytes = base64ToUint8Array(encodingAESKey + '=');
const iv = aesKeyBytes.slice(0, 16);
const rawCipher = base64ToUint8Array(encryptedBase64);
const cryptoKey = await crypto.subtle.importKey(
'raw', aesKeyBytes, { name: 'AES-CBC' }, false, ['decrypt']
);
const decryptedBuf = await crypto.subtle.decrypt(
{ name: 'AES-CBC', iv }, cryptoKey, rawCipher
);
const decryptedBytes = new Uint8Array(decryptedBuf);
// 去除 32 字节 PKCS#7 尾部填充
const pad = decryptedBytes[decryptedBytes.length - 1];
const unpadded = decryptedBytes.slice(0, decryptedBytes.length - pad);
// 解析 4 字节大端序消息长度 (Big-Endian)
const view = new DataView(unpadded.buffer, unpadded.byteOffset, unpadded.byteLength);
const msgLen = view.getUint32(16, false);
const msgBytes = unpadded.slice(20, 20 + msgLen);
return new TextDecoder().decode(msgBytes);
}
2.3 难点三:大模型文学级排版治理与双重标题自适应
直接将微信对话转化为公开文章,通常会面临三大排版灾难:
- 口语无断句:一段话连写几百字,缺乏纸质书籍的版心留白与呼吸感;
- 前言混杂:作者常以“写在前面”、“今天随手写几句”等作为导读,若直接混在正文中显得头重脚轻;
- 标题不确定性与标点污染:有时作者明确写了题目(如《秋日暖汤》),有时大模型生成的标题带有多余的书名号《》;若直接渲染为
<h1>《秋日暖汤》</h1>会严重破坏版面规整度。
为了让发布效果对齐站点的排版质感,我们通过 Few-Shot 提示词工程 + 规则引擎 实现了精准治理。
1. 标题“显式采用,隐式提炼”与标点清洗
- 显式标题:若输入开头带有《xxx》、【xxx】、# xxx、“标题:xxx”或首行独立短句,大模型直接提取标题内容;
- 隐式标题:若无显式标题,大模型结合全文深层主旨与情感,自适应提炼 4~8 字文雅标题(如《缺失 & 满足》、《阳光下的心流》);
- 代码级防呆清洗:在后处理层通过正则
title.replace(/^[《【[“"'\s#]+/, '').replace(/[》】]”"'\s]+$/, '')彻底剔除两端多余书名号与括号,保证标题 100% 纯净。
2. 前言引用块隔离与段落留白
- 前言自动转引用框:正文开头的导读语句被规范化为
> 写在前面:…独立引用块; - 呼吸感分段:深入理解情感转折点(由景入理、由昔至今、单句感悟),段与段之间强制使用双换行
\n\n进行空间留白; - 行内空白保护:后处理正则严格采用
/[^\S\r\n]+/g仅压缩行内水平空白,严禁使用/\s+/g误删段落换行符,确保正文段落舒展展开。
2.4 难点四:多模态视觉 OCR 截图解析与无缝图文统一流
在日常生活中,很多随笔往往是作者先写在手机备忘录中。如果要求用户先全选复制文字再发微信,依然存在操作摩擦。
我们利用多模态视觉大模型(deepseek-v4-flash-vision-exp)将图片与文字在架构层面完全归一:
- 微信图片地址提取:微信 Webhook 收到图片时附带
<PicUrl>; - 异步非阻塞图片流:将图片获取与解析放在后台异步任务中,绝不阻塞主线程 5 秒握手;
- 高精 OCR + 文学排版一步到位:多模态模型直接从截图中识别全部文本、提取自定标题与“写在前面”导读,并直接输出结构化排版 JSON,达成与纯文字发文完全一致的纸质排版效果。
2.5 难点五:Slug 碰撞防御与 CI/CD 质量门体系
在将生成好的 Markdown 文件推送到 GitHub 时,如果两次发送了相似主题的内容,大模型可能会生成相同的英文 Slug(例如 autumn-soup)。若盲目覆盖写入,会导致历史旧随笔丢失。
为了实现严格的只写(Write-Once)防覆盖保护,Worker 不再依赖大模型生成的标题、Slug 或当前处理日期,而是从不可变输入派生完整 SHA-256 发布指纹。微信消息使用 MsgId,通用发布接口则组合标题、正文与图片地址;路径采用指纹前 24 位,完整 64 位指纹写入 Frontmatter 供冲突反查:
const stableSeed = msgId
? `wechat:${msgId.trim()}`
: `direct:${JSON.stringify({ title: rawTitle, body: rawBody, imageUrl })}`;
const publishFingerprint = await getStablePublishFingerprint(stableSeed);
const uniqueSlug = `wife-${publishFingerprint.slice(0, 24)}`;
const filePath = `content/wife/${uniqueSlug}.md`;
const putBody = {
message: `feat(wife): AI 精修发布《${polished.title}》`,
content: contentBase64,
branch: branch
};
// 提交时不附带 sha,杜绝任何静默覆盖历史已有文件的可能
const res = await fetch(`https://api.github.com/repos/${repo}/contents/${filePath}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${githubToken}`,
'Accept': 'application/vnd.github+json',
'User-Agent': 'CF-Worker'
},
body: JSON.stringify(putBody)
});
若 GitHub 返回 422,Worker 会进一步读取目标文件,并核对 Frontmatter 中的完整 publish_id。只有发布指纹一致时才按合法重试返回成功;路径存在但指纹不一致时会明确报错,从而避免同标题正文丢失、哈希碰撞或参数错误被静默吞掉。
此外,全站配备了严格的自动化测试门禁(npm test),包含 16 项静态站点质量门测试(死链扫描、重复 ID、未闭合 Markdown 语法拦截、零 Emoji 扫描、控制字符检测)与 8 项 Worker 安全、加解密与智能发文测试(SHA-1 验签、0~256 字节 AES 块边界、XSS 协议白名单清洗、并发锁与幂等状态机、意图路由、草稿预览确认与撤回删除),保证每一篇通过微信自动发布的文章都能达到最高标准的质量与安全。
3. 端到端部署配置与凭据获取详解
为了帮助读者在零服务器成本的前提下完整复现本系统,本节详细记录从企业微信后台、GitHub 权限管控到 Cloudflare Workers 边缘网关的完整配置实操指南。
3.1 第一步:企业微信自建应用与微信插件配置
企业微信作为系统的主要输入端,既能提供企业级稳定的 Webhook 回调能力,又通过「微信插件」实现了普通微信界面的无感交互。
1. 注册与获取企业 ID
- 访问 企业微信管理后台 注册或登录(个人即可免费注册,无需企业资质认证);
- 进入 「我的企业」 $\to$ 「企业信息」,滑到最底部复制 企业 ID(CorpID),记录为
WECHAT_CORP_ID。
2. 创建自建应用并获取 Secret
- 导航至 「应用管理」 $\to$ 「应用」 $\to$ 「自建」,点击 创建应用;
- 应用名称填写为
碎碎念小助手(或博客发文助手),上传喜欢的圆形头像; - 创建成功后进入该应用详情页,找到 Secret,点击“查看”,系统会向手机端企业微信发送确认消息,查看并记录该 Secret 为
WECHAT_CORP_SECRET。
3. 设置 API 接收消息(Webhook)
- 在应用详情页中,下滑找到 「功能」 $\to$ 「接收消息」 $\to$ 「设置 API 接收」;
- URL 填写为您在 Cloudflare Workers 分配的公网地址:
https://<worker-subdomain>.workers.dev/wechat; - Token 点击“随机获取”,记录为
WECHAT_TOKEN; - EncodingAESKey 点击“随机获取”(43 位随机字符),记录为
WECHAT_ENCODING_AES_KEY; - 关键时序提示:此时请勿点击保存! 微信后台在点击保存时会立即向该 URL 发起一次带有
msg_signature的 GET 请求以验证服务器有效性。必须先完成下方第 3.3 节的 Cloudflare 代码部署与环境变量注入后,再返回此处点击保存。
4. 开启微信插件(日常无需打开企微 App)
- 进入 「我的企业」 $\to$ 「微信插件」;
- 用您日常使用的个人微信扫描页面中的二维码,关注 「企业微信插件」 微信公众号;
- 关注后,个人微信的聊天列表中就会永久出现 「碎碎念小助手」 聊天窗口,平时直接在微信里发文字或图片即可!
3.2 第二步:GitHub 细粒度访问令牌(Fine-grained PAT)配置
为了遵循安全工程中的最小权限原则(Principle of Least Privilege),我们坚决废弃了拥有账户全局所有仓库读写权限的 Classic Token,全面切换为 GitHub 官方推荐的细粒度访问令牌。
1. 创建限定范围的 Token
- 登录 GitHub,点击右上角头像 $\to$ Settings $\to$ Developer settings $\to$ Personal access tokens $\to$ Fine-grained tokens;
- 点击 Generate new token;
- Token name:填写
myweb-wechat-publisher; - Expiration:选择过期时间(建议
90 days,定期轮换); - Resource owner:选择您的 GitHub 个人账户;
- Repository access:务必选择 Only select repositories,并在下拉列表中只勾选您的个人网站代码仓库(如
yuruchuan/myweb),严禁选择 All repositories; - Repository permissions:
- 找到
Contents权限项,选择Access: Read and write(允许读取校验现有文件与提交新 Markdown 文章); - 其他所有权限(如 Actions, Secrets, Administration, Pull requests 等)一律保持
No access;
- 找到
- 点击 Generate token,立即复制生成的
github_pat_xxxx字符串,记录为GITHUB_TOKEN(页面关闭后不可再次查看)。
3.3 第三步:Cloudflare Workers 部署与环境变量配置
Cloudflare Workers 作为边缘计算核心,承担了企业微信消息加解密、超时竞速控制、大模型排版调用与 GitHub API 安全写入的全部重任。
1. 创建 Worker
- 登录 Cloudflare 控制台 ,在左侧导航栏选择 Compute (Workers & Pages) $\to$ Workers;
- 点击 Create Worker,命名为
wechat-post-publisher,点击 Deploy 完成初始创建; - 点击 Edit code,将本项目仓库中
scripts/wechat-worker/index.js的完整代码复制并覆盖粘贴到编辑器中,点击 Save and deploy。
2. 配置环境变量与 Secrets(敏感密钥隔离)
进入该 Worker 的 Settings $\to$ Variables and Secrets 标签页,严格区分明文变量与加密密钥进行添加:
| 变量/密钥名称 | 类型 | 配置说明 / 示例值 |
|---|---|---|
WECHAT_TOKEN | Secret (加密) | 企业微信后台 API 接收消息设置的 Token |
WECHAT_ENCODING_AES_KEY | Secret (加密) | 企业微信后台 API 接收消息设置的 43 位 EncodingAESKey |
WECHAT_CORP_SECRET | Secret (加密) | 企业微信自建应用的 Secret |
GITHUB_TOKEN | Secret (加密) | GitHub 生成的 Fine-grained PAT (github_pat_...) |
AI_API_KEY | Secret (加密) | DeepSeek / OpenAI 控制台申请的 API密钥 |
API_PUBLISH_SECRET | Secret (加密) | 自定义外部 API 直发接口的 Bearer 鉴权密码 |
WECHAT_CORP_ID | Text (明文) | 企业微信后台的企业 ID(ww...) |
GITHUB_REPO | Text (明文) | 目标代码仓库路径(如 yuruchuan/myweb) |
GITHUB_BRANCH | Text (明文) | 网站源码所在主分支(如 main) |
AI_BASE_URL | Text (明文) | 大模型网关地址(如 https://api.deepseek.com) |
AI_MODEL | Text (明文) | 默认调用模型(如 deepseek-chat 或 deepseek-v4-flash-vision-exp) |
SITE_BASE_URL | Text (明文) | 个人网站线上域名(如 https://ruchuan.org.cn) |
完成部署闭环:在 Cloudflare 完成上述 Secrets 保存后,回到 第 3.1 节第 3 步 的企业微信后台,点击 保存。此时企微发起 GET 握手测试,Worker 将即时通过 SHA-1 验签并返回解密后的
echostr,提示“保存成功”!
3.4 踩坑实录与核心避坑指南 (Pitfalls & Lessons Learned)
在整套系统的迭代打磨与自动化回归测试过程中,我们遇到并攻克了多个极其隐蔽的“深水区”技术陷阱,现将关键教训系统梳理如下:
坑 1:企业微信 32 字节 PKCS#7 填充边界计算陷阱
- 现象:标准 PKCS#7 采用 16 字节对齐,而企业微信协议强制使用 32 字节对齐。如果简单使用常见公式
32 - (rawData.length % 32 || 32),在数据长度恰好为 32 的整数倍时会算出补位为 0,导致密文在腾讯服务端解密报“非法的填充字节”; - 解法:严格使用精确余数条件分支:并在自动化测试套件中编写了覆盖 0 到 256 字节全长度边界 的往返加解密穷举断言,彻底消除此类边缘崩溃。
const remainder = rawData.length % 32; const padLen = remainder === 0 ? 32 : 32 - remainder;
坑 2:微信网络重试与跨边缘 Isolate 幂等失效(AI 随机性碰撞)
- 现象:由于微信 5 秒超时机制,若网络偶发抖动,腾讯服务器会向不同地区的 Cloudflare 边缘节点发起重试请求。每个边缘 Isolate 的内存是相互隔离的,如果文件路径(Slug)依赖大模型随机生成的英文标题(如 Isolate A 得到
autumn-tea-xxx.md,Isolate B 得到hot-tea-yyy.md),两个节点都会成功向 GitHub 提交,导致同一条随笔产生两篇重复文章; - 解法:
- 彻底解耦大模型输出,文件路径完全由不可变输入源(
MsgId或正文 SHA-256)派生确定性发布指纹(publish_id); - 无论多少个全球边缘节点并发重试,算出的 GitHub 文件路径 100% 绝对一致;
- GitHub 提交一律采用 Write-Once 模式(不传
sha),保证全链路零重复、零覆盖。
- 彻底解耦大模型输出,文件路径完全由不可变输入源(
3. 坑 3:GitHub 422 错误盲目忽略导致的“假成功”误报
- 现象:GitHub REST API 在遇到参数错误、分支不存在、Token 权限不足时统一返回
HTTP 422 Unprocessable Entity。如果简单将所有 422 视为“文件已存在”并返回 200,当分支配置写错时,系统会向微信误报“发布成功”,但仓库中实际空无一物; - 解法:当收到 422 时,Worker 必须反向发起一次精准
GET查询,读取目标文件并比对其 Frontmatter 中的publish_id。只有确认该文件确实存在且指纹完全匹配时才按合法幂等放行;若文件不存在或指纹不匹配,立即抛出异常并向微信上报真实错误。
4. 坑 4:Markdown XSS 清洗中的 HTML 实体绕过
- 现象:攻击者可能利用 Markdown 链接语法结合 HTML 实体编码构造恶意载荷(如
[点击](javascript:alert(1)))。普通的文本正则匹配很难识别此类转义伪协议; - 解法:Worker 内置了递归的
decodeHtmlEntities解码器,在 URL 协议校验前统一将十六进制、十进制与命名实体解码为原始字符,配合白名单协议过滤,且 Hugo 模板层彻底废除safeURL放行,实现从输入到渲染的纵深安全防御。
5. 坑 5:回调验证的时序死锁(先部署还是先保存?)
- 现象:首次配置时,在企微后台点击保存会提示“URL 验证失败”;
- 解法:牢记 “先在 Cloudflare 部署代码并配置 Secrets $\to$ 后在企微后台点击保存” 的操作顺序。只要 Worker 能正常响应带有签名参数的 GET 请求,企微后台即可一次性保存成功。
4. 实践总结与方法论升华
通过这套系统的构建与迭代,我们达成了以下三项核心收益:
- 将创作阻力降至绝对零度: 作者不再需要感知 Markdown 语法、Frontmatter 结构或 Git 命令,在微信中发送口语碎碎念或备忘录截图即可完成全流程发布;
- 大模型赋能的“智能内容管道”: 大模型不仅是文本生成的工具,更是非结构化数据向高度规范化静态内容转换的智能中间件。通过多模态 Few-Shot 提示工程,大模型能够精准执行视觉提取、语病修正、结构分段与情感升华;
- 极简、稳健的 Serverless 架构: 全系统依托 Cloudflare 边缘计算、DeepSeek API 与 GitHub 基础设施运行,无需维护任何虚拟机或数据库,月度运营成本恒为零,且天然具备高可用与抗并发能力。
技术的终极价值在于服务生活。当繁琐的工程细节被精巧的自动化流水线默默承载,我们便能将全部注意力留给生活本身,在每一个平凡而温暖的瞬间里,安然记录、静心沉淀。