企业微信机器人本质是带密钥的HTTP接口,直接用HttpClient POST JSON至webhook地址即可,无需SDK;注意复用HttpClient、UTF8编码、正确构造JSON字段(text.content或markdown.content)、图片需传base64+原始字节MD5、避免async void、依据errcode精准排错。

直接用 HttpClient POST JSON 就能发,别绕弯
企业微信机器人本质就是一个带密钥的 HTTP 接口,POST 一个合法 JSON 到 https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx 就行。不用 SDK、不依赖企业微信官方库,System.Net.Http 足够——它轻、快、可控,且 .NET Core / .NET 6+ 全系原生支持。
常见错误是手动拼接 URL 参数或用 HttpWebRequest 写一堆 header 控制逻辑,反而容易漏掉 Content-Type: application/json 或编码不一致导致 400;也有人误以为要先调用「上传媒体」接口才能发文本,其实只有 image、file 类型才需要预上传。
-
HttpClient实例建议复用(如static readonly),避免频繁创建引发 socket 耗尽 - 务必用
Encoding.UTF8序列化 JSON,中文否则乱码(响应里显示 “???” 或直接 400) - Webhook 地址里的
key=xxx是唯一凭证,硬编码进配置时必须加密或从环境变量读,切勿提交到 Git
SendTextMessage 和 SendMarkdownMessage 的参数差异很关键
文本和 Markdown 看似只是换了个 msgtype,但字段结构完全不同:文本消息的正文在 text.content,而 Markdown 的内容直接挂在 markdown.content 下,多一层嵌套。错放字段会导致消息静默失败(HTTP 200 但群内无推送)。
更易踩的坑是 @所有人 或 @某人:文本类型需显式填 mentioned_list(数组,如 ["@all"]),而 Markdown 类型不支持 @ 语法——它只渲染纯 Markdown,想 @ 得改用文本类型或混合发送。
- 发文本:
msgtype="text"→ 必含text.content,可选text.mentioned_list - 发 Markdown:
msgtype="markdown"→ 必含markdown.content,不识别@,不支持mentioned_list - 测试时先用最简内容,比如
"测试:hello",排除格式干扰
发图片不是传文件,而是传 base64 + md5
企业微信对图片消息的要求很特殊:不接受 multipart/form-data 文件上传,也不走 media API;而是要求你把图片读成字节数组,转成 base64 字符串,再算出原始字节的 MD5(32 位小写),一起塞进 JSON。
很多人卡在 MD5 上——用文件路径算 MD5、用字符串再编码一次、或大小写混用,都会导致返回 {"errcode":44001,"errmsg":"invalid media_id"}。注意:MD5 必须是对原始二进制字节算的,不是对 base64 字符串算的。
- 读图用
File.ReadAllBytes(path),别用StreamReader(会破坏二进制) - MD5 计算用
using (var md5 = MD5.Create()) { return BitConverter.ToString(md5.ComputeHash(bytes)).Replace("-", "").ToLower(); } -
base64字符串不能加前缀(如data:image/png;base64,),只传纯 base64
异步方法别用 async void,尤其在 Windows Service 里
很多示例代码写 public static async void SendTextMessage(...),这在控制台临时跑跑没问题,但一旦放进 Windows Service、定时任务或 ASP.NET 后台服务,就会造成“火球”(fire-and-forget):异常无法捕获、线程提前释放、消息发一半就丢。
真正可靠的做法是返回 Task,并在调用处 await 或用 .Wait()(Service 中慎用阻塞,推荐 await 配合 Task.Run)。
- 函数签名必须是
public static async Task SendTextMessage(...) - Windows Service 中若需定时推送,用
System.Threading.Timer触发async方法时,得包装一层try/catch+Task.Run避免未处理异常终止服务 - 别信“发不出就重试三次”的简单逻辑——Webhook 失败可能是网络抖动,也可能是 key 过期或被禁用,得结合
errcode响应体判断是否重试
最难 debug 的永远不是怎么发,而是为什么没收到:检查响应体里的 errcode 比看状态码更有用;40097 是 key 错,44001 是 media 问题,40001 是签名失效——这些数字比“发送失败”四个字管用得多。



















