ONE CURRENT SIGNAL WeComGateway:在无头 Linux 上双向驱动企业微信的原生网关 公开卡片 ## 它是什么 WeComGateway 是一个运行在 Linux 上的中间件。它让你的程序能够通过标准 HTTP API **收发企业微信消息**,而底层跑的是腾讯官方原版客户端——不是协议逆向,不是 UI 自动化,不是第三方 SDK。 对上游调用者来说,它就是一个本地 REST 服务:`POST /send`、`GET /messages`。 对腾讯服务器来说,它就是一台正常登录的员工电脑。 --- ## 为什么需要它 企业微信生态里,想让程序收发消息,你只有三条路,每条都是死胡同: | 路径 | 问题 | |:---|:---| | **官方 API / Webhook** | 不支持员工个人私聊收发;会话存档是只读的、延迟的、昂贵的 | | **逆向微信协议(iPad/Android)** | 腾讯持续对抗,设备指纹检测 → 封号率极高 | | **UI 自动化(OCR、坐标点击、剪贴板)** | 窗口遮挡就挂、分辨率变了就挂、CPU 占满、丢字漏字 | WeComGateway 走的是第四条路: > **让官方客户端自己跑,我只在它内部挂两个极小的观察点。** --- ## 核心架构 ``` 腾讯服务器 (MMTLS) │ ▼ ┌─────────────────────────────────────┐ │ WXWork.exe (官方原版, Wine 内运行) │ │ │ │ ┌─ capture hook ──┐ │ │ │ 订阅消息到达 │ │ │ │ 订阅消息更新 │ │ │ └────────┬────────┘ │ │ │ 内存映射 │ │ ┌────────┴────────┐ │ │ │ outbound hook │ │ │ │ 原生构造+发送 │ │ │ │ 网络回执确认 │ │ │ └─────────────────┘ │ └──────────────┬──────────────────────┘ │ (loopback) ▼ ┌──────────────────────────────────────┐ │ Gateway Server (port 8900) │ │ - 消息账本 (SQLite, 幂等) │ │ - PII 门禁 (fail-closed) │ │ - 路由审批 (私聊/群聊分离) │ │ - REST API │ └──────────────────────────────────────┘ │ ▼ 你的任意程序 (AI Agent / CRM / 脚本 / 告警机器人) ``` --- ## 三个最难的技术问题,以及怎么解的 ### 1. 消息接收:群消息的"延迟水化" 企微客户端收到群消息时,第一帧回调只带 ID 和时间戳,正文是空的。正文要等内核异步解密完才填进来。 如果你在第一帧就读,拿到空字符串。如果你扫内存等,要么卡死要么越界。 **解法:双回调关联。** 在 C 层挂两个点: - `ConversationMessagesDidReceiveNotification`:记录元数据,压入 pending 池(TTL 30 秒) - `ConversationMessagesDidUpdatedNotification`:拿到同一条消息的已填充实体,按 ID 精确匹配 pending 条目,提取正文 ``` 第一帧到达 → 记录 {id, route, time, body=∅} → 存入 pending[i] ...几十毫秒后... 第二帧更新 → 找到 pending[i].id == 当前 id → body 非空 → 发射事件 ``` 不扫内存、不轮询、不猜地址。只在官方通知链上等。 --- ### 2. 消息发送:不模拟键盘,直接调内部工厂 市面方案用"粘贴文字到输入框 → 模拟回车"发消息。这会丢字、触发风控、和用户剪贴板冲突。 **解法:直接调用客户端内部的富文本构造函数。** 在逆向分析中定位到一个内部 C++ 工厂函数。用 C 代码直接在堆上构造原生消息对象,投递进底层发送队列。对企微来说,这和用户手动打字发送走的是完全一样的代码路径。 发送后不立即标记成功。只有收到客户端内核触发的**网络层交付成功回调**,才确认这条消息真的到了对方。结果不明就不重试——宁可漏发一次也不重复轰炸。 --- ### 3. 进程间通信:零拷贝环形缓冲区 C 注入层和 Python 网关是两个进程。用 HTTP 或 Socket 传消息会有拷贝和序列化开销。 **解法:共享内存映射文件 + 原子自旋锁。** C 层把水化后的事件写入一个固定大小的环形队列(16 槽)。Python 侧按单调递增的 sequence 读取。没有序列化、没有 syscall、没有锁等待。单条消息从内核回调到网关入账,延迟 < 1ms。 --- ## 为什么不会被封号 因为我们没有碰腾讯的任何协议。 | 层面 | 谁负责 | |:---|:---| | QR 扫码登录 | 官方客户端 | | MMTLS/TLS 握手 | 官方客户端 | | 设备指纹注册 | 官方客户端 | | 心跳保活 | 官方客户端 | | 消息加解密 | 官方客户端 | 我们注入的 DLL 只做两件事:在消息到达的回调链上读取已解密的结构体;在发送时调用已有的内部函数。没有 hook 网络层、没有修改协议包、没有伪造设备 ID。 腾讯服务器看到的流量,和一个正常员工用电脑聊天完全一样。 --- ## 安全边界 - **PII 门禁(fail-closed):** 手机号、身份证、银行卡、密码等模式在消息离开网关前被物理替换为 `[REDACTED]`。下游程序永远拿不到原文,除非通过显式授权的窄通道。 - **群聊 mention-only:** 群消息默认只观察,不回复。只有被显式 `@` 才激活处理。防止机器人在群里刷屏。 - **路由审批:** 收、发、模型调用是三个独立的权限开关。任何一个被关闭或过期,对应通道立即熔断,不需要重启。 - **HMAC 匿名化:** 对外暴露的会话和消息 ID 经过单向哈希。即使 API 日志泄漏,也无法反推企微内部原始 ID。 --- ## 部署形态 一台标准 Linux 机器(物理机、VPS 或 Docker 容器均可)。 Wine 负责运行 `WXWork.exe`。首次需要扫码登录一次。之后网关常驻后台,支持 watchdog 自动恢复。 不需要 Windows 服务器,不需要 GUI 桌面环境,不需要企微官方付费席位。 --- ## 一句话 > **WeComGateway 把企业微信变成了一个可编程的 HTTP 服务,同时让腾讯的风控系统认为它只是一台普通的员工电脑。** 关联 等待读取
## 它是什么 WeComGateway 是一个运行在 Linux 上的中间件。它让你的程序能够通过标准 HTTP API **收发企业微信消息**,而底层跑的是腾讯官方原版客户端——不是协议逆向,不是 UI 自动化,不是第三方 SDK。 对上游调用者来说,它就是一个本地 REST 服务:`POST /send`、`GET /messages`。 对腾讯服务器来说,它就是一台正常登录的员工电脑。 --- ## 为什么需要它 企业微信生态里,想让程序收发消息,你只有三条路,每条都是死胡同: | 路径 | 问题 | |:---|:---| | **官方 API / Webhook** | 不支持员工个人私聊收发;会话存档是只读的、延迟的、昂贵的 | | **逆向微信协议(iPad/Android)** | 腾讯持续对抗,设备指纹检测 → 封号率极高 | | **UI 自动化(OCR、坐标点击、剪贴板)** | 窗口遮挡就挂、分辨率变了就挂、CPU 占满、丢字漏字 | WeComGateway 走的是第四条路: > **让官方客户端自己跑,我只在它内部挂两个极小的观察点。** --- ## 核心架构 ``` 腾讯服务器 (MMTLS) │ ▼ ┌─────────────────────────────────────┐ │ WXWork.exe (官方原版, Wine 内运行) │ │ │ │ ┌─ capture hook ──┐ │ │ │ 订阅消息到达 │ │ │ │ 订阅消息更新 │ │ │ └────────┬────────┘ │ │ │ 内存映射 │ │ ┌────────┴────────┐ │ │ │ outbound hook │ │ │ │ 原生构造+发送 │ │ │ │ 网络回执确认 │ │ │ └─────────────────┘ │ └──────────────┬──────────────────────┘ │ (loopback) ▼ ┌──────────────────────────────────────┐ │ Gateway Server (port 8900) │ │ - 消息账本 (SQLite, 幂等) │ │ - PII 门禁 (fail-closed) │ │ - 路由审批 (私聊/群聊分离) │ │ - REST API │ └──────────────────────────────────────┘ │ ▼ 你的任意程序 (AI Agent / CRM / 脚本 / 告警机器人) ``` --- ## 三个最难的技术问题,以及怎么解的 ### 1. 消息接收:群消息的"延迟水化" 企微客户端收到群消息时,第一帧回调只带 ID 和时间戳,正文是空的。正文要等内核异步解密完才填进来。 如果你在第一帧就读,拿到空字符串。如果你扫内存等,要么卡死要么越界。 **解法:双回调关联。** 在 C 层挂两个点: - `ConversationMessagesDidReceiveNotification`:记录元数据,压入 pending 池(TTL 30 秒) - `ConversationMessagesDidUpdatedNotification`:拿到同一条消息的已填充实体,按 ID 精确匹配 pending 条目,提取正文 ``` 第一帧到达 → 记录 {id, route, time, body=∅} → 存入 pending[i] ...几十毫秒后... 第二帧更新 → 找到 pending[i].id == 当前 id → body 非空 → 发射事件 ``` 不扫内存、不轮询、不猜地址。只在官方通知链上等。 --- ### 2. 消息发送:不模拟键盘,直接调内部工厂 市面方案用"粘贴文字到输入框 → 模拟回车"发消息。这会丢字、触发风控、和用户剪贴板冲突。 **解法:直接调用客户端内部的富文本构造函数。** 在逆向分析中定位到一个内部 C++ 工厂函数。用 C 代码直接在堆上构造原生消息对象,投递进底层发送队列。对企微来说,这和用户手动打字发送走的是完全一样的代码路径。 发送后不立即标记成功。只有收到客户端内核触发的**网络层交付成功回调**,才确认这条消息真的到了对方。结果不明就不重试——宁可漏发一次也不重复轰炸。 --- ### 3. 进程间通信:零拷贝环形缓冲区 C 注入层和 Python 网关是两个进程。用 HTTP 或 Socket 传消息会有拷贝和序列化开销。 **解法:共享内存映射文件 + 原子自旋锁。** C 层把水化后的事件写入一个固定大小的环形队列(16 槽)。Python 侧按单调递增的 sequence 读取。没有序列化、没有 syscall、没有锁等待。单条消息从内核回调到网关入账,延迟 < 1ms。 --- ## 为什么不会被封号 因为我们没有碰腾讯的任何协议。 | 层面 | 谁负责 | |:---|:---| | QR 扫码登录 | 官方客户端 | | MMTLS/TLS 握手 | 官方客户端 | | 设备指纹注册 | 官方客户端 | | 心跳保活 | 官方客户端 | | 消息加解密 | 官方客户端 | 我们注入的 DLL 只做两件事:在消息到达的回调链上读取已解密的结构体;在发送时调用已有的内部函数。没有 hook 网络层、没有修改协议包、没有伪造设备 ID。 腾讯服务器看到的流量,和一个正常员工用电脑聊天完全一样。 --- ## 安全边界 - **PII 门禁(fail-closed):** 手机号、身份证、银行卡、密码等模式在消息离开网关前被物理替换为 `[REDACTED]`。下游程序永远拿不到原文,除非通过显式授权的窄通道。 - **群聊 mention-only:** 群消息默认只观察,不回复。只有被显式 `@` 才激活处理。防止机器人在群里刷屏。 - **路由审批:** 收、发、模型调用是三个独立的权限开关。任何一个被关闭或过期,对应通道立即熔断,不需要重启。 - **HMAC 匿名化:** 对外暴露的会话和消息 ID 经过单向哈希。即使 API 日志泄漏,也无法反推企微内部原始 ID。 --- ## 部署形态 一台标准 Linux 机器(物理机、VPS 或 Docker 容器均可)。 Wine 负责运行 `WXWork.exe`。首次需要扫码登录一次。之后网关常驻后台,支持 watchdog 自动恢复。 不需要 Windows 服务器,不需要 GUI 桌面环境,不需要企微官方付费席位。 --- ## 一句话 > **WeComGateway 把企业微信变成了一个可编程的 HTTP 服务,同时让腾讯的风控系统认为它只是一台普通的员工电脑。**