跳到主要内容

微信 (WeChat)

将 Hermes 连接到 微信(腾讯的个人即时通讯平台)。该适配器使用腾讯的 iLink Bot API 来支持个人微信账号——这与企业微信(WeCom)不同。消息通过长轮询方式传输,因此无需公网端点或 Webhook。

信息

此适配器适用于 个人微信账号(微信)。如需企业/公司微信,请参阅 企业微信适配器

前提条件

  • 一个个人微信账号
  • Python 包:aiohttpcryptography
  • qrcode 包为可选(用于在设置过程中在终端中渲染二维码)

安装所需依赖:

pip install aiohttp cryptography
# Optional: for terminal QR code display
pip install qrcode

设置

1. 运行设置向导

连接微信账号最简单的方式是通过交互式设置向导:

hermes gateway setup

提示时选择 Weixin。向导将执行以下操作:

  1. 向 iLink Bot API 请求一个二维码
  2. 在终端中显示二维码(或提供一个 URL)
  3. 等待您使用微信手机 App 扫描二维码
  4. 提示您在手机上确认登录
  5. 自动将账号凭证保存至 ~/.hermes/weixin/accounts/

确认后,您将看到类似如下消息:

微信连接成功,account_id=your-account-id

向导会保存 account_idtokenbase_url,因此无需手动配置。

2. 配置环境变量

首次通过二维码登录后,请在 ~/.hermes/.env 中至少设置账号 ID:

WEIXIN_ACCOUNT_ID=your-account-id

# Optional: override the token (normally auto-saved from QR login)
# WEIXIN_TOKEN=your-bot-token

# Optional: restrict access
WEIXIN_DM_POLICY=open
WEIXIN_ALLOWED_USERS=user_id_1,user_id_2

# Optional: home channel for cron/notifications
WEIXIN_HOME_CHANNEL=chat_id
WEIXIN_HOME_CHANNEL_NAME=Home

3. 启动网关

hermes gateway

适配器将恢复保存的凭证,连接到 iLink API,并开始长轮询接收消息。

功能特性

  • 长轮询传输 —— 无需公网端点、Webhook 或 WebSocket
  • 二维码登录 —— 通过 hermes gateway setup 实现扫码连接
  • 私信与群组消息 —— 可配置访问策略
  • 媒体支持 —— 图片、视频、文件和语音消息
  • AES-128-ECB 加密 CDN —— 所有媒体传输自动加密/解密
  • 上下文令牌持久化 —— 磁盘持久化,支持重启后继续回复
  • Markdown 格式化 —— 标题、表格和代码块会重新格式化以适配微信阅读
  • 智能消息分块 —— 长消息在逻辑边界(段落、代码块)处自动拆分
  • 输入提示 —— 代理处理时,微信客户端会显示“正在输入…”状态
  • SSRF 保护 —— 下载前验证出站媒体 URL
  • 消息去重 —— 5 分钟滑动窗口防止重复处理
  • 自动重试带退避机制 —— 可从临时 API 错误中恢复

配置选项

config.yaml 中的 platforms.weixin.extra 下设置以下选项:

默认值描述
account_idiLink Bot 账号 ID(必需)
tokeniLink Bot 令牌(必需,由二维码登录自动保存)
base_urlhttps://ilinkai.weixin.qq.comiLink API 基础 URL
cdn_base_urlhttps://novac2c.cdn.weixin.qq.com/c2c媒体传输的 CDN 基础 URL
dm_policyopen私信访问策略:openallowlistdisabledpairing
group_policydisabled群组访问策略:openallowlistdisabled
allow_from[]允许私信的用户 ID 列表(当 dm_policy=allowlist 时)
group_allow_from[]允许响应的群组 ID 列表(当 group_policy=allowlist 时)

访问策略

私信策略(DM Policy)

控制谁可以向机器人发送私信:

行为
open任何人都可以向机器人发送私信(默认)
allowlistallow_from 列表中的用户 ID 可发送私信
disabled所有私信均被忽略
pairing配对模式(用于初始设置)
WEIXIN_DM_POLICY=allowlist
WEIXIN_ALLOWED_USERS=user_id_1,user_id_2

群组策略(Group Policy)

控制机器人在哪些群组中响应:

行为
open机器人在所有群组中响应
allowlist机器人仅在 group_allow_from 列表中的群组 ID 中响应
disabled所有群组消息均被忽略(默认)
WEIXIN_GROUP_POLICY=allowlist
WEIXIN_GROUP_ALLOWED_USERS=group_id_1,group_id_2
备注

个人微信账号的默认群组策略为 disabled(与企业微信默认为 open 不同)。这是有意为之,因为个人微信账号可能加入大量群组。

媒体支持

入站(接收)

适配器接收用户发送的媒体附件,从微信 CDN 下载,解密后本地缓存,供代理处理:

类型处理方式
图片下载、AES 解密,并缓存为 JPEG 格式。
视频下载、AES 解密,并缓存为 MP4 格式。
文件下载、AES 解密,并缓存。保留原始文件名。
语音若有文字转录,提取为文本;否则下载 SILK 格式的音频并缓存。

引用消息:来自被引用(回复)消息的媒体也会被提取,使代理能够了解用户回复的内容上下文。

AES-128-ECB 加密 CDN

微信媒体文件通过加密 CDN 传输。适配器会透明地处理此过程:

  • 入站(Inbound): 使用 encrypted_query_param URL 从 CDN 下载加密媒体,然后使用 AES-128-ECB 加密算法配合消息负载中提供的文件级密钥进行解密。
  • 出站(Outbound): 文件在本地使用随机生成的 AES-128-ECB 密钥加密,上传至 CDN,加密后的引用信息包含在出站消息中。
  • AES 密钥长度为 16 字节(128 位)。密钥可以以原始 base64 或十六进制编码形式到达 —— 适配器会自动处理这两种格式。
  • 此功能需要安装 cryptography Python 包。

无需任何配置 —— 加密与解密过程自动完成。

出站(发送)

方法发送内容
send带有 Markdown 格式的文本消息
send_image / send_image_file原生图片消息(通过 CDN 上传)
send_document文件附件(通过 CDN 上传)
send_video视频消息(通过 CDN 上传)

所有出站媒体均通过加密 CDN 上传流程:

  1. 生成一个随机的 AES-128 密钥
  2. 使用 AES-128-ECB + PKCS#7 填充对文件进行加密
  3. 通过 iLink API 请求上传 URL(getuploadurl
  4. 将密文上传至 CDN
  5. 发送消息并附带加密媒体引用

上下文令牌持久化

iLink Bot API 要求每个出站消息必须回传与特定对端关联的 context_token。适配器维护一个基于磁盘的上下文令牌存储:

  • 每个账户+对端的令牌保存在 ~/.hermes/weixin/accounts/<account_id>.context-tokens.json
  • 启动时,先前保存的令牌会被恢复
  • 每条入站消息都会更新对应发送者的存储令牌
  • 出站消息会自动包含最新的上下文令牌

这确保了即使网关重启后仍能保持回复连续性。

Markdown 格式化

微信个人聊天不原生支持完整的 Markdown 渲染。适配器会对内容进行重格式化以提升可读性:

  • 标题# 标题)→ 转换为 【标题】(一级标题)或 **标题**(二级及以上)
  • 表格 → 重格式化为带标签的键值列表(例如:- 列名: 值
  • 代码块 → 保持原样(微信可良好渲染)
  • 过多的空白行 → 合并为双换行

消息分块

长消息会智能拆分以适配聊天传输:

  • 单条消息最大长度:4000 字符
  • 拆分点优先选择段落边界和空行
  • 代码块保持完整(不会在块内拆分)
  • 缩进的续行(重格式化表格/列表中的子项)与父项保持在一起
  • 超大单个块将回退至基础适配器的截断逻辑

输入状态指示

适配器会在微信客户端显示输入状态:

  1. 当消息到达时,适配器通过 getconfig API 获取 typing_ticket
  2. 每个用户的 typing_ticket 缓存 10 分钟
  3. send_typing 发送输入开始信号;stop_typing 发送输入停止信号
  4. 网关在代理处理消息期间自动触发输入状态指示

长轮询连接

适配器使用 HTTP 长轮询(非 WebSocket)接收消息:

工作原理

  1. 连接: 验证凭证并启动轮询循环
  2. 轮询: 调用 getupdates,设置 35 秒超时;服务器保持请求打开,直到有消息到达或超时
  3. 分发: 入站消息通过 asyncio.create_task 并发分发
  4. 同步缓冲: 持久化的同步游标(get_updates_buf)保存在磁盘,确保适配器重启后能从正确位置恢复

重试行为

在 API 错误时,适配器采用简单的重试策略:

条件行为
临时错误(第 1–2 次)2 秒后重试
重复错误(第 3 次及以上)退避 30 秒,然后重置计数器
会话过期(errcode=-14暂停 10 分钟(可能需要重新登录)
超时立即重新轮询(正常长轮询行为)

去重

入站消息通过消息 ID 进行去重,窗口为 5 分钟。这可防止网络波动或重叠轮询响应导致的重复处理。

令牌锁

同一令牌仅允许一个 Weixin 网关实例使用。适配器在启动时获取作用域锁,并在关闭时释放。若已有其他网关正在使用相同令牌,启动将失败并显示提示性错误信息。

所有环境变量

变量必需默认值描述
WEIXIN_ACCOUNT_IDiLink Bot 账号 ID(来自二维码登录)
WEIXIN_TOKENiLink Bot 令牌(通过二维码登录自动保存)
WEIXIN_BASE_URLhttps://ilinkai.weixin.qq.comiLink API 基础 URL
WEIXIN_CDN_BASE_URLhttps://novac2c.cdn.weixin.qq.com/c2c媒体传输的 CDN 基础 URL
WEIXIN_DM_POLICYopen私信访问策略:openallowlistdisabledpairing
WEIXIN_GROUP_POLICYdisabled群组访问策略:openallowlistdisabled
WEIXIN_ALLOWED_USERS(空)用逗号分隔的用户 ID,用于私信白名单
WEIXIN_GROUP_ALLOWED_USERS(空)用逗号分隔的群组 ID,用于群组白名单
WEIXIN_HOME_CHANNEL用于定时任务/通知输出的聊天 ID
WEIXIN_HOME_CHANNEL_NAMEHome主频道的显示名称
WEIXIN_ALLOW_ALL_USERS网关级别标志,允许所有用户(由设置向导使用)

故障排除

问题解决方法
Weixin startup failed: aiohttp and cryptography are required安装两者:pip install aiohttp cryptography
Weixin startup failed: WEIXIN_TOKEN is required运行 hermes gateway setup 完成二维码登录,或手动设置 WEIXIN_TOKEN
Weixin startup failed: WEIXIN_ACCOUNT_ID is required.env 文件中设置 WEIXIN_ACCOUNT_ID,或运行 hermes gateway setup
Another local Hermes gateway is already using this Weixin token首先停止其他网关实例——每个令牌仅允许一个轮询器
会话已过期(errcode=-14您的登录会话已过期。重新运行 hermes gateway setup 扫描新的二维码
设置过程中二维码已过期二维码最多自动刷新 3 次。如果持续过期,请检查网络连接
机器人不响应私信检查 WEIXIN_DM_POLICY —— 若设置为 allowlist,发送者必须在 WEIXIN_ALLOWED_USERS
机器人忽略群消息群组策略默认为 disabled。请将 WEIXIN_GROUP_POLICY 设置为 openallowlist
媒体下载/上传失败确保已安装 cryptography。检查对 novac2c.cdn.weixin.qq.com 的网络访问权限
Blocked unsafe URL (SSRF protection)外部媒体 URL 指向私有/内部地址。仅允许公共 URL
语音消息显示为文本若微信提供语音转文字,适配器将使用文本。这是预期行为
消息出现重复适配器通过消息 ID 去重。若仍见重复,请检查是否运行了多个网关实例
iLink POST ... HTTP 4xx/5xxiLink 服务端 API 错误。请检查令牌有效性及网络连接
终端二维码无法渲染安装 qrcodepip install qrcode。或打开二维码上方打印的 URL