1. 项目概述一个纯粹的微信公众号草稿管理工具如果你和我一样运营着一个技术类的微信公众号那你一定对后台那个简陋的草稿箱编辑器和繁琐的发布流程深有体会。每次想用本地写好的 Markdown 或 HTML 文章发布要么得手动复制粘贴忍受格式错乱要么就得依赖一些第三方工具把自己的账号安全交到别人手里。这种割裂感对于追求效率和掌控感的开发者来说实在是一种折磨。今天要聊的这个项目wechat-oa就是为解决这个痛点而生的。它不是什么大而全的公众号管理平台而是一个极其专注、纯粹的命令行工具核心目标只有一个让你能像操作本地文件一样自由地管理微信公众平台的草稿箱。它基于微信官方的开放接口不引入任何第三方依赖所有操作都直接与微信服务器对话安全、透明、可控。简单来说它就是你本地环境与公众号草稿箱之间的一座“专用桥梁”。这个工具特别适合以下几类朋友技术博主/独立开发者习惯用 VS Code、Typora 等专业编辑器写作希望实现“本地写作一键同步到草稿箱”的流畅工作流。团队协作场景文章内容在 Git 仓库中管理需要将定稿后的 HTML 文件快速上传至公众号后台供运营同事预览或发布。自动化脚本爱好者希望将公众号更新集成到 CI/CD 流水线中实现定时或触发式的内容同步。对数据安全敏感的用户不希望使用第三方托管服务要求所有凭证和内容都在自己可控的环境下处理。接下来我会带你从零开始彻底拆解这个工具的设计思路、实现细节并分享我在深度使用和改造过程中积累的一手经验与避坑指南。2. 核心设计思路与架构解析2.1 为什么选择官方 API 而非第三方 SDK这是本项目最根本的设计决策。市面上有很多优秀的第三方微信公众号 SDK如wechatpy,itchat等它们封装完善功能强大。但wechat-oa选择了最“原始”的方式直接使用requests库调用微信官方 API。这么做的理由非常充分极致的轻量与透明项目核心依赖只有requests和Pillow。这意味着依赖树极其简单几乎不会遇到版本冲突问题部署和移植成本极低。所有与微信服务器的交互逻辑都明明白白地写在代码里没有黑盒出了问题你可以顺着代码逻辑直接定位到具体的 API 请求和响应。避免依赖风险第三方 SDK 虽好但其维护状态、更新频率、对最新 API 的支持程度都是不确定因素。一旦 SDK 停止维护或出现严重 Bug你的整个工作流就会面临风险。直接使用官方 API只要微信接口不变你的工具就永远可用。功能高度聚焦第三方 SDK 通常试图覆盖所有功能消息管理、用户管理、菜单管理、素材管理等而本项目只关心“草稿管理”。自己实现可以做到代码最小化只包含必要的逻辑理解和修改起来都更容易。安全可控所有认证信息AppID, AppSecret和访问令牌Access Token的流转都发生在你的本地环境或你完全掌控的服务器上不存在信息泄露到第三方服务的风险。架构示意图概念层面[你的本地HTML文件] - [wechat-oa 工具 (Python脚本)] - [HTTPS请求] - [微信官方API服务器] - [你的公众号草稿箱]整个链路清晰、简短、无旁路。2.2 工具的核心工作流拆解这个工具虽然命令不多但背后对应着微信公众平台开放 API 中关于“草稿箱”的完整操作闭环。理解这个工作流对于后续排查问题和进行高级定制至关重要。凭证准备与鉴权工具启动后首先从config.json读取APP_ID和APP_SECRET。使用这两项凭证向微信服务器发起请求换取一个有时效性的access_token。这个token是后续所有 API 调用的“通行证”。草稿列表拉取携带access_token调用GET接口获取当前草稿箱列表。微信接口会返回一个包含media_id草稿唯一标识、标题、更新时间等信息的列表。草稿创建与更新创建读取本地的 HTML 文件解析内容并自动或手动为其生成封面图。然后将标题、作者、内容、封面图素材ID打包通过POST接口提交给微信成功后返回新草稿的media_id。更新与创建类似但需要指定目标草稿的media_id。微信 API 会直接用新内容覆盖旧草稿。草稿删除根据指定的media_id调用删除接口将草稿从后台移除。封面图生成这是一个独立的实用功能。根据文章标题调用Pillow库在本地生成一张固定尺寸微信推荐比例的图片并保存预览。这个图片文件可以手动上传到微信素材库获取其media_id用于创建草稿。注意这里有一个关键细节也是新手最容易困惑的地方。微信 API 的“草稿箱”接口和“永久素材”接口是两套不同的体系。本工具操作的“草稿” (draft) 特指公众号后台“草稿箱”里的内容它不同于“素材管理”里的图文消息。草稿的media_id是临时的仅用于在草稿箱内管理不能直接用于群发。群发时需要先将草稿“发布”为正式的图文素材获取另一个media_id。本工具专注于“草稿”阶段的管理。3. 环境配置与核心代码深度解析3.1 从零开始的详细配置指南很多教程把配置步骤写得过于简略导致用户在第一步就卡住。这里我结合踩坑经验给你一份保姆级的配置流程。第一步获取核心凭证 (AppID AppSecret)登录 微信公众平台 。进入“设置与开发” - “基本配置”。在“公众号开发信息”板块你会看到AppID应用ID和AppSecret应用密钥。AppSecret通常需要扫码验证后才会显示。重要安全提示AppSecret是最高权限的密钥绝不能泄露或提交到公开的 Git 仓库。这也是为什么项目使用config.json并通过.gitignore排除的原因。一旦泄露应立即在公众平台重置。第二步处理 IP 白名单问题高频错误点在“基本配置”页面往下翻找到“IP白名单”设置。这是一个强烈推荐启用的安全功能。如果你在本地电脑运行工具你需要知道你电脑当前网络的公网 IP。可以访问ip.sb或cip.cc这类网站查看。将这个 IP 地址添加到白名单中。如果你在云服务器如阿里云ECS运行工具你需要添加该云服务器的公网 IP。错误码 40125如果调用 API 时返回此错误并提示“invalid ip”百分之百就是你的调用服务器 IP 不在白名单内。请仔细核对并添加。第三步配置文件与项目初始化# 1. 克隆项目或下载源码 git clone https://github.com/andy8663/wechat-oa.git cd wechat-oa # 2. 创建虚拟环境强烈推荐避免污染系统Python python -m venv venv # Windows 激活: venv\Scripts\activate # Linux/Mac 激活: source venv/bin/activate # 3. 安装依赖就两个非常快 pip install requests Pillow # 4. 复制并编辑配置文件 cp config.example.json config.json用文本编辑器打开config.json填入你的信息{ APP_ID: 你的AppID字符串类型, APP_SECRET: 你的AppSecret字符串类型, author: 默认作者名如张三 }author字段不是必填的但建议设置这样创建草稿时会自动填充作者信息。3.2 核心模块代码解读与增强建议我们深入看看wechat_push.py里的几个关键函数理解其原理并探讨一些可以增强稳定性和易用性的点。1. 访问令牌 (access_token) 的获取与管理这是所有微信 API 调用的基石。工具里通常有一个_get_access_token()函数。import requests import json import time class WeChatOA: def __init__(self, app_id, app_secret): self.app_id app_id self.app_secret app_secret self.access_token None self.token_expire_time 0 # token过期的时间戳 def _get_access_token(self): # 如果内存中的token未过期直接返回 if self.access_token and time.time() self.token_expire_time: return self.access_token # 否则重新获取 url https://api.weixin.qq.com/cgi-bin/token params { grant_type: client_credential, appid: self.app_id, secret: self.app_secret, } resp requests.get(url, paramsparams).json() if access_token in resp: self.access_token resp[access_token] # 微信返回的 expires_in 是有效秒数通常为7200秒2小时 # 我们设置一个提前5分钟过期的安全缓冲 self.token_expire_time time.time() resp[expires_in] - 300 return self.access_token else: raise Exception(fFailed to get access_token: {resp})要点解析这里实现了一个简单的内存级缓存。expires_in是微信返回的有效期但网络延迟、服务器时间差等因素可能导致实际有效期略短。设置一个提前5分钟300秒的缓冲期是业界常见做法可以避免在临界点调用 API 失败。增强建议对于需要长时间运行或高频调用的脚本可以将access_token持久化到文件或 Redis 中避免每次运行脚本都重新获取也能支持多进程/多机器共享同一个有效 token。2. 创建草稿 (create_draft) 函数的关键细节创建草稿需要组装一个符合微信 API 要求的复杂 JSON 数据包。def create_draft(self, title, content, authorNone, thumb_media_idNone): :param title: 文章标题 :param content: 文章HTML内容 :param author: 作者可选不传则使用配置中的默认作者 :param thumb_media_id: 封面图片的素材media_id可选 url fhttps://api.weixin.qq.com/cgi-bin/draft/add?access_token{self._get_access_token()} # 1. 构建文章体 articles [{ title: title, author: author or self.default_author, digest: self._generate_digest(content), # 需要实现一个生成摘要的函数 content: content, content_source_url: , # 原文链接可留空 thumb_media_id: thumb_media_id, # 核心封面图ID need_open_comment: 0, # 是否打开评论 only_fans_can_comment: 0, # 是否仅粉丝可评论 }] # 2. 组装请求数据 data { articles: articles } # 3. 微信API要求JSON数据中不能有HTML转义字符但content是HTML # 这里需要特别注意直接json.dumps整个data即可requests会处理 headers {Content-Type: application/json} resp requests.post(url, datajson.dumps(data, ensure_asciiFalse), headersheaders).json() if resp.get(errcode) 0: return resp.get(media_id) # 返回新草稿的ID else: raise Exception(fFailed to create draft: {resp})核心难点 -thumb_media_id这是创建草稿最麻烦的一步。你不能直接传图片 URL 或本地路径必须传一个已经上传到微信素材库的图片的media_id。这意味着你需要先调用“新增临时素材”或“新增永久素材”接口上传图片拿到返回的media_id再用来创建草稿。原项目的cover命令只是生成了预览图上传步骤需要手动完成或额外编码实现。HTML 内容处理微信 API 对 HTML 内容有严格的过滤和转换规则。例如某些标签如script会被过滤外链图片需要先上传为素材。最好先将完整的 HTML 提交如果报错再根据错误信息调整。摘要生成digest摘要字段如果不传微信会自动截取正文前一部分。但自动截取效果往往不好。建议实现一个_generate_digest函数从纯文本内容中截取 80-120 个字作为摘要。4. 实战操作构建完整的本地写作发布流水线原工具提供了基础命令但离“流畅的流水线”还有距离。下面我分享一套自己打磨后的实践方案将本地 Markdown 写作、图片处理、草稿同步全流程串联起来。4.1 准备工作目录结构与工具链我建议建立如下目录结构来管理你的公众号文章项目my-wechat-articles/ ├── config.json # wechat-oa 配置文件已加入.gitignore ├── drafts/ # 存放草稿 │ ├── 20240520-article-a/ │ │ ├── index.md # 主文章 Markdown │ │ ├── images/ # 文章引用的本地图片 │ │ │ ├── cover.png # 封面图可自动生成 │ │ │ └── diagram1.svg │ │ └── article.html # 由 index.md 生成的最终 HTML │ └── 20240525-article-b/ │ └── ... ├── scripts/ │ ├── publish.py # 增强的发布脚本 │ └── generate_cover.py # 增强的封面图生成脚本 └── templates/ └── wechat.html # 微信专用的 HTML 模板核心工具链写作Typora / VS Code (with Markdown extensions)格式转换pandoc或markdownjinja2库。将 Markdown 转换为符合微信排版的 HTML。图片管理使用图床如阿里云OSS、腾讯云COS或微信素材库。本地图片需要先上传。自动化使用wechat-oa作为最后一步的同步工具。4.2 关键步骤实现详解步骤一从 Markdown 到微信友好 HTML微信的 CSS 支持有限直接转换的 HTML 样式可能很丑。我们需要一个模板。templates/wechat.html示例!DOCTYPE html html head meta charsetutf-8/ title{{ title }}/title style /* 微信内置的富文本编辑器样式兼容性最好 */ body { font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, sans-serif; font-size: 17px; line-height: 1.6; color: #333; padding: 15px; max-width: 700px; margin: 0 auto; word-wrap: break-word; } h1, h2, h3 { font-weight: 600; margin-top: 1.5em; margin-bottom: 0.8em; } h1 { font-size: 22px; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; } h2 { font-size: 20px; } p { margin: 0 0 1em 0; } img { max-width: 100% !important; height: auto !important; display: block; margin: 1em auto; } code { background-color: #f6f8fa; padding: 0.2em 0.4em; border-radius: 3px; font-family: SFMono-Regular, Consolas, monospace; } pre { background-color: #f6f8fa; padding: 1em; overflow: auto; border-radius: 5px; } pre code { background: none; padding: 0; } blockquote { border-left: 4px solid #dfe2e5; color: #6a737d; padding-left: 1em; margin-left: 0; } ul, ol { padding-left: 2em; } a { color: #0366d6; text-decoration: none; } a:hover { text-decoration: underline; } /style /head body {{ content }} /body /html然后用一个 Python 脚本 (scripts/convert.py) 进行转换import markdown from jinja2 import Template import sys import os def md_to_wechat_html(md_file_path, output_html_path): # 读取 Markdown with open(md_file_path, r, encodingutf-8) as f: md_content f.read() # 提取标题假设第一行是 # 标题 title 默认标题 lines md_content.strip().split(\n) if lines[0].startswith(# ): title lines[0][2:].strip() # 转换 Markdown 为 HTML html_content markdown.markdown(md_content, extensions[extra, codehilite, tables]) # 套用模板 with open(./templates/wechat.html, r, encodingutf-8) as f: template Template(f.read()) final_html template.render(titletitle, contenthtml_content) # 输出 with open(output_html_path, w, encodingutf-8) as f: f.write(final_html) print(f转换成功: {md_file_path} - {output_html_path}) return title, output_html_path if __name__ __main__: if len(sys.argv) 2: print(Usage: python convert.py path_to_md_file) sys.exit(1) md_file sys.argv[1] html_file os.path.splitext(md_file)[0] .html md_to_wechat_html(md_file, html_file)步骤二自动化处理图片与封面这是流水线中最复杂的一环。我们的目标是文章内引用的本地图片自动上传到微信素材库并将 Markdown 中的![](./images/diagram1.png)替换为微信素材的 URL。这里给出一个简化版的思路实际实现需要考虑素材管理避免重复上传、错误重试等解析生成的 HTML找出所有img src本地路径标签。调用微信“新增临时素材”接口/cgi-bin/media/upload类型为image上传图片。微信会返回一个media_id和一个临时的图片 URLurl字段。将 HTML 中的图片src替换为这个临时 URL。注意临时素材有效期为 3 天仅用于草稿预览。正式发布前需要将关键图片上传为“永久素材”。封面图处理逻辑类似但封面图需要先上传拿到其media_id用于创建草稿的thumb_media_id参数。步骤三集成与一键发布将以上步骤整合到一个增强的publish.py脚本中# scripts/publish.py import sys import os from convert import md_to_wechat_html from wechat_oa import WeChatOA # 假设我们把核心类重构为了 wechat_oa.py import json def main(): if len(sys.argv) 2: print(Usage: python publish.py path_to_md_file [action]) print(Actions: preview (default), upload) sys.exit(1) md_path sys.argv[1] action sys.argv[2] if len(sys.argv) 2 else preview # 1. 转换 Markdown 到 HTML title, html_path md_to_wechat_html(md_path) print(f文章标题: {title}) # 2. 处理图片这里调用图片上传函数略 # processed_html, cover_media_id process_images(html_path, title) # 3. 初始化微信工具 with open(../config.json, r) as f: config json.load(f) client WeChatOA(config[APP_ID], config[APP_SECRET]) if action preview: # 生成本地预览 print(fHTML 已生成: {html_path}) # 调用系统命令在浏览器中打开预览 # os.system(fopen {html_path}) # for Mac # os.system(fstart {html_path}) # for Windows elif action upload: # 4. 读取处理后的 HTML with open(html_path, r, encodingutf-8) as f: content f.read() # 5. 创建草稿 try: # 假设 cover_media_id 已通过 process_images 函数获得 media_id client.create_draft(title, content, thumb_media_idcover_media_id) print(f✅ 草稿创建成功! Media ID: {media_id}) print(f 请前往公众号后台草稿箱查看和发布。) except Exception as e: print(f❌ 创建草稿失败: {e}) else: print(f未知操作: {action}) if __name__ __main__: main()这样你的工作流就简化为# 在文章目录下 python ../scripts/publish.py index.md preview # 本地预览 # 确认无误后 python ../scripts/publish.py index.md upload # 上传到公众号草稿箱5. 高频问题排查与进阶技巧在实际使用中你几乎一定会遇到下面这些问题。我把它们和解决方案整理成了表格方便你快速查阅。5.1 常见错误码与解决方法速查表错误码错误信息 (示例)可能原因解决方案40001invalid credential, access_token is invalid or not latest1.access_token过期。2.access_token在其他设备被刷新当前 token 失效。1. 检查 token 缓存逻辑确保在过期前刷新。2. 实现 token 失效自动重试机制捕获此错误后清空本地 token 缓存重新获取一次并重试原操作。40125invalid appsecret或invalid ip1.AppSecret填写错误或已重置。2. 调用服务器的 IP 不在公众号设置的 IP 白名单中。1. 去公众平台“基本配置”核对或重置AppSecret。2. 登录公众平台在“基本配置”-“IP白名单”中添加当前服务器的公网 IP。45009reach max api daily quota limit调用频率超限。微信对每个接口都有每日/每分钟调用次数限制。1. 查看 官方文档 了解具体限制。2. 在代码中加入延时避免短时间密集调用。例如批量操作时在请求间加入time.sleep(1)。88000No permission to use this article可能尝试操作了不属于本公众号的草稿或media_id无效。检查media_id是否正确且是否属于当前配置的公众号。-1system error微信服务器内部错误通常是暂时的。等待片刻后重试。如果是重要操作需要实现重试逻辑。JSON 解析错误非标准 JSON 响应1. 网络问题导致响应体不完整。2. 微信服务器返回了非 JSON 内容如 HTML 错误页面。1. 检查网络连接。2. 在代码中打印原始的响应文本 (resp.text)而不仅仅是resp.json()以便查看真实返回内容。5.2 进阶技巧与避坑指南access_token的全局管理不要每次调用都获取这是最低级的错误会迅速耗尽 API 调用配额。务必实现缓存。分布式环境下的挑战如果你在多台服务器或容器中运行脚本内存缓存会失效。解决方案是使用一个中心化的存储比如一个简单的文件需处理锁问题、数据库如 SQLite或 Redis。所有实例都从这个中心存储读写 token。HTML 内容的“净化”微信后台对 HTML 有严格的过滤。以下内容大概率会被清除或导致格式错乱script,iframe,form等标签。外链的 CSS 和 JavaScript。过大的图片需先压缩。复杂的 CSS 样式如position: fixed。尽量使用微信内置富文本编辑器支持的那些内联样式。最佳实践先在微信后台的富文本编辑器里编辑一篇简单文章然后查看其 HTML 源码以其为样式基准。或者使用我上面提供的那个简化模板。图片上传策略优化临时 vs 永久素材草稿中的图片使用“临时素材”接口上传即可有效期3天足够预览。但如果你希望草稿能长期保存或者后续要用于群发则必须使用“永久素材”接口。避免重复上传可以建立一个本地数据库或索引文件记录图片本地路径-微信 media_id的映射。上传前先查一下如果已有且未过期则直接使用旧的media_id能节省大量时间和 API 配额。错误处理与日志生产环境使用的脚本必须加入完善的错误处理和日志记录。使用 Python 的logging模块将关键步骤开始获取 token、上传图片、创建草稿、请求参数、响应结果以及发生的异常都记录下来。这将是排查线上问题的唯一依据。import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(name)s - %(levelname)s - %(message)s, handlers[logging.FileHandler(wechat_publish.log), logging.StreamHandler()]) logger logging.getLogger(__name__) try: media_id client.create_draft(...) logger.info(fDraft created successfully. Media ID: {media_id}) except Exception as e: logger.error(fFailed to create draft. Error: {e}, exc_infoTrue) # exc_info 会打印堆栈跟踪与 CI/CD 集成你可以将上述流水线脚本集成到 GitHub Actions 或 GitLab CI 中。例如每当main分支的drafts/目录下有新的 Markdown 文件被合并就自动触发转换和上传草稿的操作。关键点在 CI 环境中config.json中的敏感信息AppSecret必须通过仓库的Secrets功能注入绝不能硬编码在代码里。IP 白名单也需要设置为 CI 运行器的 IP。这个项目wechat-oa提供了一个坚实可靠的起点。它用最简洁的方式解决了公众号内容管理中的一个核心痛点。围绕它构建的本地化、自动化写作发布流水线能真正将你从繁琐的后台操作中解放出来让你更专注于内容创作本身。希望这篇超详细的拆解和实战指南能帮助你打造出最适合自己的那一套高效工具。如果在实践过程中遇到新的问题不妨回头看看微信的官方开发文档或者到项目的 GitHub 页面与开发者交流社区的智慧总是能带来惊喜。