前言
上次聊到我重新开始使用Obsidian,因为喜欢分享记录这些知识,有希望把自己学到的技巧和找到的好东西分享出来的习惯。所以我以前一直都都想做一套从编辑到发送到微信公众号草稿夹的方法,我也在这个上面走过不少弯路。
我之前还做个从Notion到微信公众号的EDGE插件,但是Notion的块不是标准的Markdown格式,还要搞定微信的可信IP问题,众所周知个人电脑基本上不可能有固定IP,所以当时也是自己研究了一个中间件的想法,最后还是卡在了微信公众号的特定安全HTML语言就没有进行下去了。
今天这个Obsidian插件的作者有着同我一样类似的想法,但是他显然比我更优秀,他做的这个插件成功了!所以我拿来可以直接使用,我也想分享出来。
项目地址
https://github.com/DavidLam-oss/obsidian-wechat-converter
效果
我已经在Obsidian上安装了这个插件,大家可以看看效果。

✳️写作发布流程对比:
- 以前的发文过程:
- 使用typora+picgo编辑好本地文章。(本地需要安装picgo工具常驻内存)
- 复制编辑好的markdown格式到mdnice之类的工具中预览微信公众号格式。
- 复制微信公众号格式的html到微信公众号编辑器中。
- 保存、发布。
- 现在的发文过程:
- 使用Obsidian+Image auto upload插件+picgo server编辑文章。(直接在服务器上安装piclist server,本地Obsidian打开自动启用插件,关闭即关,不需要常驻内存的picgo)
- 直接点发布就到了微信公众号的草稿里。(obsidian-wechat-converter本身就自带左右分栏预览,在远程服务器或者使用cloudflare部署代理来解决微信公众号的可信IP)
- 优缺点对比
| 步骤 | 原来 | 现在 |
|---|---|---|
| 编辑 | typora(收费) | Obsidian(免费) |
| 图床 | 本机picgo | 本机picgo或在线piclist server(在线的方式需要懂点配置技巧) |
| 微信公众号预览 | 复制到mdnice(需要打开一个新应用) | 直接分栏预览(不需要打开任何应用) |
| 发布草稿 | 复制到微信公众号编辑器(需PC操作登录微信公众号编辑器) | 一键发布微信公众号编辑器(后续可手机直接发布,省去登录微信公众号编辑器的过程) |
配置页面预览
[!image-swipe] 左右滑动查看图片
![]()
安装(我偷懒了,直接复制了原作者):
- 手动安装
- 从 GitHub Releases 下载最新的
obsidian-wechat-converter.zip插件包。 - 解压并将其中的文件夹放入 Obsidian vault 的
.obsidian/plugins/目录中。 最终路径应为:.../.obsidian/plugins/obsidian-wechat-converter/ - 确保文件夹内至少包含以下文件(三件套运行时):
main.jsmanifest.jsonstyles.css
- 重启 Obsidian 或在设置中刷新插件列表,并启用插件。
- 从 GitHub Releases 下载最新的
- BRAT 安装/更新
如果你使用 BRAT 管理插件更新:
- 安装并启用 BRAT 插件。
- 在 BRAT 中添加仓库:
DavidLam-oss/obsidian-wechat-converter。 - 安装后执行一次冒烟检查:
- 打开转换面板
- 预览渲染
- 复制到公众号
- (可选)一键同步到草稿箱
说明:当前版本已支持标准三件套运行时,BRAT 更新路径与 Obsidian 插件标准发布方式一致。
自动发文(微信白名单代理部署)
作者的github上只有cloudflare一种方案,我也是用的这种,其实作者还有一种自建服务器方案使用的php,因为我不喜欢php,所以没用那种,也懒得改写成其它的语言了,免费的cloudflare额度应该够用了。
-
创建 Cloudflare Worker
- 登录 Cloudflare Dashboard
- 左侧菜单选择 Workers & Pages → Create Application → Create Worker
- 命名(如
wechat-proxy)并点击 Deploy
-
编辑 Worker 代码
点击 Edit code,替换为以下代码:
export default { async fetch(request, env) { const MAX_UPLOAD_BYTES = 10 * 1024 * 1024; const ALLOWED_METHODS = new Set(['GET', 'POST', 'UPLOAD']); const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; const jsonResponse = (payload, status = 200) => new Response(JSON.stringify(payload), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json; charset=utf-8', }, }); const isAllowedWechatUrl = (rawUrl) => { try { const parsed = new URL(rawUrl); return parsed.protocol === 'https:' && parsed.hostname === 'api.weixin.qq.com'; } catch { return false; } }; const toUint8Array = (base64) => { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes; }; // 处理 CORS 预检请求 if (request.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } if (request.method !== 'POST') { return jsonResponse({ error: 'Method Not Allowed' }, 405); } try { const body = await request.json(); const { url, method = 'GET', data, fileData, fileName, mimeType } = body; const normalizedMethod = String(method || 'GET').toUpperCase(); // 安全校验:只允许访问微信 API if (typeof url !== 'string' || !isAllowedWechatUrl(url)) { return jsonResponse({ error: 'Invalid URL. Only https://api.weixin.qq.com/ is allowed.' }, 400); } if (!ALLOWED_METHODS.has(normalizedMethod)) { return jsonResponse({ error: 'Invalid method. Only GET, POST, and UPLOAD are allowed.' }, 400); } let response; if (normalizedMethod === 'UPLOAD') { if (typeof fileData !== 'string' || fileData.length === 0) { return jsonResponse({ error: 'Missing fileData for upload.' }, 400); } const approxBytes = Math.floor(fileData.length * 3 / 4); if (approxBytes > MAX_UPLOAD_BYTES) { return jsonResponse({ error: 'Upload too large. Maximum size is 10 MB.' }, 413); } const safeMimeType = typeof mimeType === 'string' && mimeType.startsWith('image/') ? mimeType : 'application/octet-stream'; const safeFileName = typeof fileName === 'string' && /^[\w.\-]+$/.test(fileName) ? fileName : 'image'; // 文件上传处理:Base64 -> Binary -> FormData const bytes = toUint8Array(fileData); const formData = new FormData(); formData.append('media', new Blob([bytes], { type: safeMimeType }), safeFileName); response = await fetch(url, { method: 'POST', body: formData }); } else { // 普通 JSON 请求 const opts = { method: normalizedMethod }; if (normalizedMethod === 'POST') { opts.headers = { 'Content-Type': 'application/json' }; if (data !== undefined) opts.body = JSON.stringify(data); } response = await fetch(url, opts); } const responseText = await response.text(); let result; try { result = responseText ? JSON.parse(responseText) : {}; } catch { return jsonResponse({ error: 'Upstream returned a non-JSON response.' }, 502); } return jsonResponse(result, response.status); } catch (error) { return jsonResponse({ error: 'Proxy request failed.' }, 500); } } };这个版本比旧示例更适合直接公开在文档里:
- 不记录请求 URL,避免把
appid、secret、access_token写进日志。 - 不回显原始请求体和异常堆栈,减少敏感信息暴露。
- 只允许
GET、POST、UPLOAD,并且只允许访问https://api.weixin.qq.com/。 - 上传大小限制为 10 MB,避免异常大文件拖垮 Worker。
- 上传字段固定为
media,与插件当前行为保持一致。
- 不记录请求 URL,避免把
-
配置微信 IP 白名单
将 Cloudflare 出口 IP 添加到微信公众号白名单(官方 IP 列表):
173.245.48.0/20, 103.21.244.0/22, 103.22.200.0/22, 103.31.4.0/22 141.101.64.0/18, 108.162.192.0/18, 190.93.240.0/20, 188.114.96.0/20 197.234.240.0/22, 198.41.128.0/17, 162.158.0.0/15, 104.16.0.0/13 104.24.0.0/14, 172.64.0.0/13, 131.0.72.0/22 -
插件配置
在插件设置 → 高级设置 → API 代理地址 中填入你的 Worker URL:
https://wechat-proxy.your-account.workers.dev❗️警告:该代理仅建议自用,请不要公开分享 Worker 地址,也不要将其部署为公共服务。
🙏深表感谢:
感谢作者【林小卫很行 (DavidLam)】让我们能够更加方便的写作与发布,节约了时间!
一点思考:
有些工具看起来很好用,又差点意思,并不是不能做的更好,只是如果都做到无感顺畅,就没有中间环节,不满足成为默默无闻的节点,因为这样不利于割韭菜(至少也要打个广告吧),但是这确实是一些独立开发者的生存方式,免费的服务会让开发者失去动力,但又找不到好的变现方式成了这些开发者的窘境! 作为编程爱好者和这些工具的使用者,我很理解那些开发者,不能老是为爱发电,靠打赏过活,我尊重你们,但我可能不会为此付出大的代价,因为商业化逻辑的闭环也不是使用者的问题。 开源项目一直都是一把双刃剑!希望所有优秀的开发者能尽快找到自己适合的商业变现之路!
