企业微信智能机器人 .NET SDK —— 基于 WebSocket 长连接通道,提供消息收发、流式回复、模板卡片、事件回调、文件下载解密、媒体素材上传等核心能力。
本 SDK 从官方 @wecom/aibot-node-sdk(https://github.com/WecomTeam/aibot-node-sdk) 协议对齐的 .NET 实现,无需 Node 网关进程,API 设计保持一致。
- 🔗 WebSocket 长连接 —— 客户端主动外连,不需要公网 IP、域名或回调 URL
- 🔐 自动认证 —— 连接建立后自动发送认证帧
- 💓 心跳保活 —— 连续未收到回执即判定连接已死并重连
- 🔄 断线重连 —— 指数退避(1s → 2s → 4s → … → 30s 上限);认证失败与网络断开使用独立计数器
- 🚫 顶下线保护 —— 收到
disconnected_event后不再重连,避免多实例互踢形成风暴 - 🌊 流式回复 ——
StreamSession自动把增量累积成协议要求的全量帧,并按 ack 节流 - 🃏 模板卡片 —— 5 种卡片类型、流式 + 卡片组合、卡片原地更新
- 📤 主动推送 —— 向指定会话推送 Markdown / 卡片 / 媒体,不依赖回调帧
- ⏩ 串行回复队列 —— 同一
req_id串行发送并等待回执 - 🔒 文件下载解密 —— 内置 AES-256-CBC(32 字节块 PKCS#7)
- 📎 媒体素材上传 —— 三步分片上传,自动分级并发与重试
- 🧩 DI / 托管集成 ——
AddWeComAiBot+IWeComAiBotHandler,每条消息独立作用域 - 📦 双目标框架 ——
net8.0/net10.0,源生成序列化、零反射、AOT 友好
dotnet add package WeCom.AiBot
dotnet add package WeCom.AiBot.Extensions.Hosting # 需要 DI / IHostedService 时using Microsoft.Extensions.Hosting;
using WeCom.AiBot.Extensions.Hosting;
using WeCom.AiBot.Streaming;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWeComAiBot(options =>
{
options.BotId = Environment.GetEnvironmentVariable("WECOM_BOT_ID")!;
options.Secret = Environment.GetEnvironmentVariable("WECOM_BOT_SECRET");
});
builder.Services.AddWeComAiBotHandler<MyBotHandler>();
await builder.Build().RunAsync();
// ------------------------------------------------------------
public sealed class MyBotHandler(IMyLlm llm) : IWeComAiBotHandler
{
public async Task OnTextAsync(TextMessageContext ctx, CancellationToken ct)
{
await using StreamSession session = ctx.BeginStream();
// 只喂增量,SDK 负责累积成全量帧、按 ack 节流、保证结束帧送达
await foreach (string delta in llm.StreamAsync(ctx.Content, ct))
await session.AppendAsync(delta, ct);
await session.CompleteAsync(ct);
}
public Task OnEnterChatAsync(EventContext ctx, CancellationToken ct) =>
ctx.ReplyWelcomeAsync("您好!有什么可以帮您的?", ct); // 须在 5 秒内完成
}IWeComAiBotHandler 的所有方法都有默认空实现,只覆写关心的部分即可。
同一 stream.id 的后续帧会覆盖之前的内容。把大模型的 delta 逐个直接发出去,
用户会看到内容不断被替换成最后一小段。
StreamSession 内部累积完整文本再发,因此你只需要喂增量。若要直接用底层 API,
ReplyStreamAsync 的 content 必须是到目前为止的完整文本。
逐个 delta 发送会在队列里积压,延迟越拉越大。StreamSession 在「上一帧未回执」
或「距上次刷帧不足最小间隔」时跳过中间帧,结束帧则一定送达。
| 成员 | 说明 |
|---|---|
StartAsync |
启动后台连接循环(不等待认证) |
StopAsync |
停止连接 |
WaitForAuthenticatedAsync |
等待认证完成;主动推送前应先等待 |
IsConnected / IsAuthenticated / IsDisplaced |
连接状态 |
ReadInboundAsync |
以异步流读取收到的消息与事件 |
| 方法 | 说明 |
|---|---|
BeginStream |
推荐。创建流式会话,自动处理全量刷新与节流 |
ReplyStreamAsync |
发送一帧流式回复(content 须为完整内容) |
ReplyStreamNonBlockingAsync |
同上,但上一帧未回执时跳过 |
ReplyStreamWithCardAsync |
流式 + 模板卡片组合 |
ReplyWelcomeTextAsync / ReplyWelcomeCardAsync |
欢迎语,5 秒窗口 |
ReplyTemplateCardAsync |
回复模板卡片 |
UpdateTemplateCardAsync |
更新卡片,5 秒窗口,task_id 须一致 |
ReplyMediaAsync |
回复媒体消息 |
| 方法 | 说明 |
|---|---|
SendMarkdownAsync |
推送 Markdown |
SendTemplateCardAsync |
推送模板卡片 |
SendMediaAsync |
推送媒体消息 |
| 方法 | 说明 |
|---|---|
UploadMediaAsync |
三步分片上传,返回 media_id(3 天有效) |
DownloadFileAsync |
下载并用消息自带的 aeskey 解密 |
| 事件 | 说明 |
|---|---|
Connected |
WebSocket 已连接(认证未完成) |
Authenticated |
认证成功 |
Disconnected |
连接断开 |
Reconnecting |
即将重连(含序号、延迟、是否由认证失败触发) |
Error |
发生错误 |
Displaced |
被新连接顶下线,此后不再重连 |
| 属性 | 默认值 | 说明 |
|---|---|---|
BotId |
— | 机器人 ID(必填) |
Secret |
— | 机器人 Secret,与 SecretAlias 二选一 |
SecretAlias |
— | 别名如 alias:wecom-bot,运行时解析 |
WebSocketUrl |
wss://openws.work.weixin.qq.com |
私有化部署时改为管理端提供的地址 |
ReconnectBaseDelay |
1s | 重连基础延迟 |
ReconnectMaxDelay |
30s | 重连延迟上限 |
MaxReconnectAttempts |
10 | 断线重连上限,-1 无限 |
MaxAuthFailureAttempts |
5 | 认证失败重试上限,-1 无限 |
HeartbeatInterval |
30s | 心跳间隔 |
MaxMissedHeartbeats |
2 | 连续未回执次数达到即判死 |
ReplyAckTimeout |
5s | 等待回执超时 |
MaxReplyQueueLength |
500 | 单个 req_id 排队上限 |
RemoteCertificateValidationCallback |
— | 自签证书校验(私有化部署) |
ConfigureWebSocket |
— | 底层 ClientWebSocketOptions 逃生舱 |
配置文件里只出现标识符,真实密钥留在进程环境:
options.SecretAlias = "alias:wecom-bot";
默认解析器读环境变量 WECOM_BOT_SECRET_WECOM_BOT(去掉 alias: 前缀、转大写、
非字母数字换成下划线)。可用 AddWeComSecretResolver<T>() 换成 KeyVault 等实现。
| 示例 | 说明 |
|---|---|
samples/WeCom.AiBot.Sample.Echo |
流式回显、欢迎语、卡片交互、文件下载解密 |
samples/WeCom.AiBot.Sample.WebConsole |
浏览器控制台:实时看收到的消息 + 主动推送 + 人工接管 |
samples/WeCom.AiBot.Sample.EuCoreBridge |
桥接 EU.Core.Agent:SSE delta → StreamSession |
需要 .NET SDK 10.0.100+(见 global.json),以及在企业微信管理端创建好的机器人。
$env:WECOM_BOT_ID = '你的机器人ID'
$env:WECOM_BOT_SECRET = '你的机器人Secret'
dotnet run --project samples/WeCom.AiBot.Sample.Echo$env: 只在当前 PowerShell 会话内有效。凭据不要写进代码或提交进仓库。
启动后看到这几行就说明通了:
info: WeCom.AiBot.WeComAiBotClient[0] 正在连接 wss://openws.work.weixin.qq.com …
info: WeCom.AiBot.WeComAiBotClient[0] 连接已建立,发送认证帧。
info: WeCom.AiBot.WeComAiBotClient[0] 认证成功。
然后在企微里找这个机器人对话:
| 操作 | 预期 |
|---|---|
| 当天首次进入会话 | 5 秒内收到欢迎语 |
| 发任意文本 | 逐字流式回显,内容逐步刷新而非重复堆叠 |
| 发含「卡片」二字的文本 | 收到按钮交互卡片 |
| 点卡片按钮 | 卡片原地更新为「已确认 ✅ / 已取消 ❌」 |
| 发图片或文件 | 日志打出解密后字节数,并回一条确认消息 |
| 断网 30 秒再恢复 | 自动重连并恢复收发 |
另起一个同 botId 实例 |
旧实例收到 disconnected_event 后干净退出、不重连 |
⚠️ 同一个botId只允许一条活跃长连接。调试时别同时开两个实例 —— 它们会互相顶下线。
把 WECOM_BOT_SECRET 留空则回落到别名方式,示例内置别名 alias:echo-bot:
$env:WECOM_BOT_SECRET = ''
$env:WECOM_BOT_SECRET_ECHO_BOT = '你的机器人Secret'调试时最缺的两件事——看见机器人收到了什么、随手发一条测试消息——都在这个页面上。
$env:WECOM_BOT_ID = '你的机器人ID'
$env:WECOM_BOT_SECRET = '你的机器人Secret'
dotnet run --project samples/WeCom.AiBot.Sample.WebConsole启动后自动打开 http://127.0.0.1:5080(加 --no-browser 可关掉)。页面分两栏:
- 左栏实时流水 —— 收到的消息、发出的回复、连接状态变化,经 SSE 实时推送。 点任意一条可把它的会话 ID 回填到右栏;图片与文件带「下载」按钮,取的是本机解密后的副本 (企微原始链接 5 分钟失效且内容加密,链不过去)。只留最近 20 个,进程重启即清空。
- 右栏发送面板 —— Markdown、模板卡片(带「填入示例卡片」按钮)、媒体文件上传后推送。
右上角的人工接管开关决定收到消息后怎么办:
| 模式 | 行为 |
|---|---|
| 关(默认) | 机器人自动流式回显,页面同时记录收发两侧 |
| 开 | 不自动回复,消息挂在页面上等你手动回 |
人工回复走的是主动推送而不是被动回复。被动回复必须透传 req_id,而人打字要几十秒
甚至几分钟,那时 req_id 早已过期。代价是回复在企微里不与原消息形成引用关系。
模板卡片在提交前会本地校验 card_type、card_action(文本通知型与图文展示型必填)
与 task_id 字符集,直接给中文提示,省得拿服务端错误码去翻文档。
⚠️ 这个页面能以机器人身份发消息,因此只绑回环地址127.0.0.1且不做鉴权。 需要给同事用就得自己加鉴权并改WECOM_CONSOLE_URL——别直接把它暴露到公网。
别名方式为 alias:web-console,对应 WECOM_BOT_SECRET_WEB_CONSOLE。
被顶下线时本示例不会退出进程(StopHostWhenDisplaced = false),页面上会留一条醒目记录
说明为什么不再收消息——与 Echo 示例的默认行为刻意不同。
$env:WECOM_BOT_ID = '你的机器人ID'
$env:WECOM_BOT_SECRET = '你的机器人Secret'
$env:EUCORE_BASE_URL = 'http://localhost:5000'
$env:EUCORE_AGENT_ID = '已发布 Agent 的 GUID'
dotnet run --project samples/WeCom.AiBot.Sample.EuCoreBridgeEUCORE_RUN_PATH 可直接指定完整路径(优先于 EUCORE_AGENT_ID);
EUCORE_SHOW_TOOL_TRACE=false 关闭「🔧 正在调用」回显。别名为 alias:bridge-bot。
企微的实际下发结构偶尔与文档对不上,排查时原始帧是唯一可信的依据。 模板卡片事件的原始体 Echo 示例默认就会打出来;要看全部推送帧,把日志级别降到 Debug:
$env:Logging__LogLevel__Default = 'Debug'构建报 MSB3027 / MSB3021「文件被锁定」 —— 示例还在跑,bin 下的 DLL 被占用。
先 Ctrl+C 停掉,或:
Get-Process -Name 'WeCom.AiBot.Sample.Echo' -ErrorAction SilentlyContinue | Stop-Process更新卡片被拒 errcode=42045(card_action Missing or Invalid) —— 文本通知型
(text_notice)与图文展示型(news_notice)必填 card_action,且 type 只能取
1(跳转 URL)或 2(打开小程序),不填或取 0 都会被拒。
更新卡片被拒 errcode=40058 —— task_id 必须与回调收到的完全一致,取不到就发出去
该字段会被整个省略,服务端必拒。且更新有 5 秒窗口,收到事件后别做耗时操作再回。
文件下载报 aeskey 不是合法的 Base64 字符串 —— 确认 aeskey 取自同一条消息:
每个下载链接的密钥都不同,链接本身也只有 5 分钟有效期。
认证失败 —— 日志会打出 errcode / errmsg。认证失败与网络断开用独立的重试预算,
到上限后抛 WeComAuthFailureException 并停止重连 —— 这种情况重试无益,去核对
botId 与 secret。
欢迎语没收到 —— enter_chat 只在用户当天首次进入单聊会话时触发,当天再进不会重复触发。
| 方面 | Node SDK | 本 SDK |
|---|---|---|
| 回复队列竞态 | 定时器回调 + 自增 seq 防过期超时误杀 |
每 req_id 一把互斥门 + 作用域超时,该竞态结构上不存在 |
| 流式全量刷新 | 调用方自行拼接完整内容 | StreamSession 自动累积 |
| 分片上传失败 | 收集全部错误后汇总抛出 | 首个分片彻底失败即快速终止,不再白跑流量 |
| 日志 | 自定义 Logger 接口 |
标准 ILogger |
chunk_index |
类型注释写 1 基,实现是 0 基 | 与实现一致(0 基),并在注释中说明 |
| 模板卡片事件 | 声明 event_key / task_id 平铺在 event 下 |
线上实际嵌在 event.template_card_event 里,两种形态都能解析 |
aeskey 解码 |
Buffer.from(k,'base64') 恰好兼容 URL 安全字母表 |
显式宽松解码,兼容 - _ 与缺失填充 |
card_action |
声明为可选,README 示例未带 | 注释标明文本通知型 / 图文展示型必填,否则 errcode=42045 |
dotnet build WeCom.AiBot.sln
dotnet test WeCom.AiBot.sln # net8.0 与 net10.0 各跑一遍单元测试使用内存假传输,不需要真实机器人凭据即可覆盖认证、心跳判死、退避序列、 顶下线禁重连、队列串行与超时、流式累积与截断、加解密往返等行为。
xiaochanghai — https://github.com/xiaochanghai
仓库地址:https://github.com/xiaochanghai/wecom-aibot-dotnet-sdk,欢迎提 Issue 与 PR。
MIT © xiaochanghai