Weixin (WeChat)
Команды установки Python-зависимостей на этой странице рассчитаны на чекаут исходников, подготовленный через PM. После изменения зависимостей заново активируйте чекаут и перезапустите Hermes.
Подключите Hermes к WeChat (微信) — личному мессенджеру от Tencent. Адаптер работает через iLink Bot API Tencent для персональных аккаунтов WeChat; это не то же самое, что WeCom (корпоративный WeChat). Сообщения приходят по long-polling, поэтому публичный эндпоинт или вебхук не нужны.
Этот адаптер рассчитан на личные аккаунты WeChat (微信). Если вам нужен корпоративный WeChat, смотрите адаптер WeCom.
Вход по QR-коду связывает Hermes с идентичностью iLink-бота (например, a5ace6fd482e@im.bot), а не с полноценным управляемым из скриптов обычным личным аккаунтом WeChat. Из этого вытекает следующее:
- Идентичность iLink-бота, как правило, нельзя пригласить в обычные группы WeChat так, как это делается с обычным контактом.
- iLink обычно не доставляет события обычных групп WeChat (включая
@-упоминания личного аккаунта, использованного для входа по QR) в шлюз для большинства аккаунтов бот-типа. @-упоминание личного аккаунта WeChat, которым вы сканировали QR-код, — это не то же самое, что@-упоминание iLink-бота: бот живёт как отдельная идентичность.- Настройки
WEIXIN_GROUP_POLICY/WEIXIN_GROUP_ALLOWED_USERS(см. ниже) срабатывают только тогда, когда iLink действительно возвращает групповые события для вашего типа аккаунта. Если этого не происходит, групповые сообщения никогда не дойдут до Hermes, какую бы политику вы ни задали.
На практике у большинства установок стабильно работают только личные сообщения к iLink-боту. Если доставка из групп не заработала после настройки, ограничение лежит на стороне iLink, а не в Hermes. При запуске шлюз пишет WARNING всякий раз, когда WEIXIN_GROUP_POLICY выставлен в любое значение, отличное от disabled.
Требования
- Личный аккаунт WeChat
- Python-пакеты:
aiohttpиcryptography - Отрисовка QR в терминале идёт в комплекте, если Hermes установлен с extra-пакетом
messaging
Установите нужные зависимости:
# Включает aiohttp и поддержку QR-кода в терминале
python -c "import pm; pm.sync_venv(['messaging'], explicit=True)"
Установка
1. Запустите мастер настройки
Проще всего подключить аккаунт WeChat через интерактивную настройку:
hermes gateway setup
Когда появится выбор, укажите Weixin. Мастер сделает следующее:
- Запросит QR-код у iLink Bot API
- Покажет QR-код в терминале (или даст URL)
- Подождёт, пока вы отсканируете код мобильным приложением WeChat
- Попросит подтвердить вход на телефоне
- Автоматически сохранит учётные данные аккаунта в
~/.hermes/weixin/accounts/
После подтверждения вы увидите сообщение вроде такого:
微信连接成功,account_id=your-account-id
Мастер сохранит account_id, token и base_url, так что прописывать их вручную не придётся.
2. Задайте переменные окружения
После первого входа по QR пропишите как минимум ID аккаунта в ~/.hermes/.env:
WEIXIN_ACCOUNT_ID=your-account-id
# Опционально: переопределить токен (обычно сохраняется сам после входа по QR)
# WEIXIN_TOKEN=your-bot-token
# Опционально: ограничить доступ
WEIXIN_DM_POLICY=open
WEIXIN_ALLOWED_USERS=user_id_1,user_id_2
# Опционально: вернуть прежнее поведение с разбиением многострочных сообщений
# WEIXIN_SPLIT_MULTILINE_MESSAGES=true
# Опционально: домашний канал для cron и уведомлений
WEIXIN_HOME_CHANNEL=chat_id
WEIXIN_HOME_CHANNEL_NAME=Home
3. Запустите шлюз
hermes gateway
Адаптер восстановит сохранённые учётные данные, подключится к iLink API и начнёт long-polling за сообщениями.
Возможности
- Транспорт на long-poll — публичный эндпоинт, вебхук или WebSocket не требуются
- Вход по QR-коду — подключение сканированием через
hermes gateway setup - Личные сообщения — настраиваемые политики доступа; групповая переписка зависит от того, доставляет ли iLink групповые события для подключённой идентичности (для аккаунтов iLink-бота это часто не так — см. предупреждение выше)
- Поддержка медиа — картинки, видео, файлы и голосовые сообщения
- Шифрованный CDN на AES-128-ECB — автоматическое шифрование и расшифровка при любой передаче медиа
- Хранение контекстных токенов — преемственность ответов сохраняется на диске и переживает перезапуски
- Форматирование Markdown — Markdown сохраняется (заголовки, таблицы, блоки кода), поэтому клиенты WeChat с поддержкой Markdown отрисовывают его как родной
- Умная нарезка сообщений — пока текст укладывается в лимит, он остаётся одним пузырём; разбиваются по логическим границам только слишком длинные сообщения
- Индикаторы набора текста — пока агент обрабатывает запрос, в клиенте WeChat виден статус «печатает…»
- Защита от SSRF — исходящие URL медиа проверяются перед скачиванием
- Дедупликация сообщений — скользящее окно в 5 минут не даёт обработать сообщение дважды
- Автоповтор с задержкой — восстанавливается после временных ошибок API
Параметры конфигурации
Задаются в config.yaml в секции platforms.weixin.extra:
| Ключ | По умолчанию | Описание |
|---|---|---|
account_id | — | ID аккаунта iLink-бота (обязательно) |
token | — | Токен iLink-бота (обязательно, сохраняется сам после входа по QR) |
base_url | https://ilinkai.weixin.qq.com | Базовый URL iLink API |
cdn_base_url | https://novac2c.cdn.weixin.qq.com/c2c | Базовый URL CDN для передачи медиа |
dm_policy | open | Доступ к личным сообщениям: open, allowlist, disabled, pairing |
group_policy | disabled | Доступ к группам: open, allowlist, disabled |
allow_from | [] | ID пользователей с доступом к личным сообщениям (при dm_policy=allowlist) |
group_allow_from | [] | ID групп с доступом (при group_policy=allowlist) |
split_multiline_messages | false | Если true, многострочные ответы разбиваются на несколько сообщений (прежнее поведение). Если false, многострочный ответ остаётся одним сообщением, пока не превысит лимит длины. |
text_batch_delay_seconds | 0.3 | Период тишины (в секундах, максимум 2.0) перед тем, как накопленная очередь быстрых текстовых сообщений уйдёт одним объединённым запросом. iLink доставляет сообщения по отдельности, и этот дебаунс избавляет от запуска агента на каждый фрагмент. Поставьте 0, чтобы отправлять каждое сообщение сразу. |
text_batch_split_delay_seconds | 1.0 | Увеличенная задержка отправки (максимум 4.0; не меньше text_batch_delay_seconds) для случая, когда последний фрагмент близок к порогу разбиения (длинные сообщения, которые iLink мог нарезать). |
Политики доступа
Политика личных сообщений
Определяет, кто может писать боту в личку:
| Значение | Поведение |
|---|---|
open | Написать боту может кто угодно (по умолчанию) |
allowlist | Писать могут только пользователи из allow_from |
disabled | Все личные сообщения игнорируются |
pairing | Режим сопряжения (для первичной настройки) |
WEIXIN_DM_POLICY=allowlist
WEIXIN_ALLOWED_USERS=user_id_1,user_id_2
WEIXIN_ALLOWED_USERS — это входящий фильтр, а не система приглашений. Вход по QR
связывает с Hermes одну идентичность iLink-бота. Другие люди не сканируют QR-код
Hermes собственными аккаунтами — им нужно написать подключённому iLink-боту или
контакту через WeChat, и Hermes обработает личное сообщение только тогда, когда
Weixin-ID отправителя есть в WEIXIN_ALLOWED_USERS.
Рабочий порядок настройки такой:
- Один раз свяжите Hermes командой
hermes gateway setupи запишите подключённый аккаунт iLink-бота. - Попросите каждого разрешённого пользователя отправить личное сообщение этому боту/контакту.
- Возьмите ID отправителя из логов шлюза или из полезной нагрузки входящего события.
- Добавьте эти ID в
WEIXIN_ALLOWED_USERSи перезапустите шлюз.
Если с Hermes общается только тот аккаунт, что сканировал QR-код, проверьте, что остальные пишут именно идентичности iLink-бота, а не личному аккаунту WeChat, которым выполнялся вход по QR. iLink-бот — отдельная идентичность, а маршрутизация обычных контактов и групп WeChat может быть ограничена поведением iLink со стороны Tencent.
Политика групп
Определяет, в каких группах бот отвечает, когда iLink доставляет групповые события для подключённой идентичности. Для идентичностей iLink-бота, полученных входом по QR (например, ...@im.bot), групповые события обычно не доставляются вовсе, поэтому эта политика может ни на что не влиять — см. предупреждение об ограничении iLink-бота в начале страницы.
| Значение | Поведение |
|---|---|
open | Бот отвечает во всех группах (если события доставляются) |
allowlist | Бот отвечает только в группах, чьи ID перечислены в group_allow_from (если события доставляются) |
disabled | Все групповые сообщения игнорируются (по умолчанию) |
WEIXIN_GROUP_POLICY=allowlist
# ВНИМАНИЕ: это список ID групповых чатов через запятую, а НЕ ID участников,
# хотя в имени переменной и есть слово "USERS". Учитывайте это при настройке.
WEIXIN_GROUP_ALLOWED_USERS=group_id_1,group_id_2
По умолчанию для Weixin политика групп — disabled (в отличие от WeCom, где по умолчанию open). Так задумано: личный аккаунт WeChat может состоять во множестве групп, а идентичности iLink-бота, как правило, вообще не получают обычные групповые сообщения WeChat. При запуске шлюз пишет WARNING, если вы выставите WEIXIN_GROUP_POLICY в любое значение, кроме disabled.
Поддержка медиа
Входящие (приём)
Адаптер принимает медиавложения от пользователей, скачивает их с CDN WeChat, расшифровывает и кэширует локально для обработки агентом:
| Тип | Как обрабатывается |
|---|---|
| Изображения | Скачиваются, расшифровываются AES и кэшируются как JPEG. |
| Видео | Скачивается, расшифровывается AES и кэшируется как MP4. |
| Файлы | Скачиваются, расшифровываются AES и кэшируются. Исходное имя файла сохраняется. |
| Голос | Если есть текстовая расшифровка, она извлекается как текст. Иначе скачивается и кэшируется само аудио (формат SILK). |
Цитируемые сообщения: медиа из цитируемых сообщений (на которые отвечают) тоже извлекается, чтобы у агента был контекст того, на что отвечает пользователь.
Шифрованный CDN на AES-128-ECB
Медиафайлы WeChat передаются через шифрованный CDN. Адаптер берёт это на себя незаметно для пользователя:
- Входящие: шифрованное медиа скачивается с CDN по URL с
encrypted_query_param, а затем расшифровывается алгоритмом AES-128-ECB по ключу для конкретного файла, который приходит в полезной нагрузке сообщения. - Исходящие: файлы шифруются локально случайным ключом AES-128-ECB, выгружаются на CDN, а в исходящее сообщение подставляется шифрованная ссылка.
- Ключ AES — это 16 байт (128 бит). Ключи могут приходить как сырой base64 или в hex-кодировке; адаптер понимает оба формата.
- Для этого нужен Python-пакет
cryptography.
Настраивать ничего не надо — шифрование и расшифровка идут автоматически.
Исходящие (отправка)
| Метод | Что отправляет |
|---|---|
send | Текстовые сообщения с форматированием Markdown |
send_image / send_image_file | Нативные сообщения с картинкой (через выгрузку на CDN) |
send_document | Вложения-файлы (через выгрузку на CDN) |
send_video | Видеосообщения (через выгрузку на CDN) |
Любое исходящее медиа проходит через шифрованную выгрузку на CDN:
- Сгенерировать случайный ключ AES-128
- Зашифровать файл алгоритмом AES-128-ECB с дополнением PKCS#7
- Запросить URL для выгрузки у iLink API (
getuploadurl) - Выгрузить шифротекст на CDN
- Отправить сообщение с шифрованной ссылкой на медиа
Хранение контекстных токенов
iLink Bot API требует, чтобы вместе с каждым исходящим сообщением для конкретного собеседника возвращался context_token. Адаптер держит дисковое хранилище контекстных токенов:
- Токены сохраняются по паре аккаунт+собеседник в
~/.hermes/weixin/accounts/<account_id>.context-tokens.json - При запуске ранее сохранённые токены восстанавливаются
- Каждое входящее сообщение обновляет сохранённый токен для этого отправителя
- В исходящие сообщения автоматически подставляется свежий контекстный токен
Благодаря этому ответы сохраняют преемственность даже после перезапусков шлюза.
Форматирование Markdown
Клиенты WeChat, подключённые через iLink Bot API, отрисовывают Markdown напрямую, поэтому адаптер сохраняет разметку, а не переписывает её:
- Заголовки остаются заголовками Markdown (
#,##, …) - Таблицы остаются таблицами Markdown
- Блоки кода остаются ограждёнными блоками кода
- Лишние пустые строки вне блоков кода схлопываются до двойного перевода строки
Нарезка сообщений
Сообщение уходит одним чатом всякий раз, когда укладывается в лимит платформы. На части разбивается только слишком длинная полезная нагрузка:
- Максимальная длина сообщения — 4000 символов
- Сообщения в пределах лимита остаются целыми, даже если в них несколько абзацев или переносов строк
- Слишком длинные сообщения разбиваются по логическим границам (абзацы, пустые строки, блоки кода)
- Блоки кода по возможности сохраняются целиком (не режутся посередине, пока сам блок не превысит лимит)
- Отдельные блоки сверх лимита уходят на логику усечения базового адаптера
- Задержка в 0,3 с между фрагментами не даёт WeChat сбрасывать сообщения по rate-limit, когда отправляется сразу несколько частей
Индикаторы набора текста
Адаптер показывает в клиенте WeChat статус набора:
- Когда приходит сообщение, адаптер получает
typing_ticketчерез APIgetconfig - Тикеты набора кэшируются на 10 минут для каждого пользователя
send_typingшлёт сигнал начала набора;stop_typing— сигнал остановки- Пока агент обрабатывает сообщение, шлюз сам включает индикаторы набора
Соединение по long-poll
Для приёма сообщений адаптер использует HTTP long-polling (а не WebSocket):
Как это работает
- Подключение: проверка учётных данных и старт цикла опроса
- Опрос: вызов
getupdatesс таймаутом 35 секунд; сервер удерживает запрос, пока не придут сообщения или не истечёт таймаут - Диспетчеризация: входящие сообщения раздаются параллельно через
asyncio.create_task - Буфер синхронизации: постоянный курсор синхронизации (
get_updates_buf) сохраняется на диск, чтобы после перезапуска адаптер продолжил с нужной позиции
Поведение при повторах
При ошибках API адаптер действует по простой схеме повторов:
| Условие | Поведение |
|---|---|
| Временная ошибка (1-я–2-я) | Повтор через 2 секунды |
| Повторяющиеся ошибки (3 и больше) | Пауза 30 секунд, затем сброс счётчика |
Сессия истекла (errcode=-14) | Пауза 10 минут (может понадобиться повторный вход) |
| Таймаут | Сразу новый опрос (нормальное поведение long-poll) |
Дедупликация
Входящие сообщения дедуплицируются по ID сообщений с окном в 5 минут. Так двойная обработка не происходит при сетевых сбоях или перекрывающихся ответах опроса.
Блокировка токена
В каждый момент времени один токен может использовать только один экземпляр шлюза Weixin. При запуске адаптер берёт ограниченную блокировку и снимает её при остановке. Если тот же токен уже занят другим шлюзом, запуск завершается понятным сообщением об ошибке.
Все переменные окружения
| Переменная | Обязательна | По умолчанию | Описание |
|---|---|---|---|
WEIXIN_ACCOUNT_ID | ✅ | — | ID аккаунта iLink-бота (из входа по QR) |
WEIXIN_TOKEN | ✅ | — | Токен iLink-бота (сохраняется сам после входа по QR) |
WEIXIN_BASE_URL | — | https://ilinkai.weixin.qq.com | Базовый URL iLink API |
WEIXIN_CDN_BASE_URL | — | https://novac2c.cdn.weixin.qq.com/c2c | Базовый URL CDN для передачи медиа |
WEIXIN_DM_POLICY | — | open | Политика доступа к личным сообщениям: open, allowlist, disabled, pairing |
WEIXIN_GROUP_POLICY | — | disabled | Политика доступа к группам: open, allowlist, disabled |
WEIXIN_ALLOWED_USERS | — | (пусто) | ID пользователей через запятую для списка разрешений личных сообщений |
WEIXIN_GROUP_ALLOWED_USERS | — | (пусто) | ID групповых чатов через запятую (не ID участников) для списка разрешений групп. Имя переменной досталось от прежней версии — оно ждёт ID групп, а не пользователей. |
WEIXIN_HOME_CHANNEL | — | — | ID чата для вывода cron и уведомлений |
WEIXIN_HOME_CHANNEL_NAME | — | Home | Отображаемое имя домашнего канала |
WEIXIN_ALLOW_ALL_USERS | — | — | Флаг уровня шлюза для разрешения всех пользователей (используется мастером настройки) |
Устранение неполадок
| Проблема | Решение |
|---|---|
Weixin startup failed: aiohttp and cryptography are required | Установите оба пакета: python -c "import pm; pm.sync_venv(['messaging'], explicit=True)" |
Weixin startup failed: WEIXIN_TOKEN is required | Запустите hermes gateway setup, чтобы завершить вход по QR, или задайте WEIXIN_TOKEN вручную |
Weixin startup failed: WEIXIN_ACCOUNT_ID is required | Задайте WEIXIN_ACCOUNT_ID в .env или запустите hermes gateway setup |
Another local Hermes gateway is already using this Weixin token | Сначала остановите другой экземпляр шлюза — на один токен допускается лишь один поллер |
Сессия истекла (errcode=-14) | Срок действия вашей сессии вышел. Запустите hermes gateway setup заново и отсканируйте новый QR-код |
Проактивная отправка (cron / уведомление) падает с ret=-2 errmsg=prepare failed или unknown error | У собеседника протух context_token (от него давно не было входящих сообщений). Адаптер считает это устаревшей сессией, а не лимитом запросов, и один раз переотправляет без токена, так что сообщение всё равно доходит. Если iLink и после этого отвечает prepare failed (или сбрасывать было нечего — бот только что сопряжён), отправка (текста или медиа) падает с iLink sendmessage session not ready … the user must send the bot a message first (or re-pair); кулдаун лимита запросов для неё не включается. Только другие ответы -2 запускают backoff/кулдаун лимита запросов, и ошибка кулдауна содержит сырые ret/errcode/errmsg |
| QR-код истёк во время настройки | QR обновляется автоматически до 3 раз. Если он продолжает истекать, проверьте сетевое соединение |
| Бот не отвечает на личные сообщения | Проверьте WEIXIN_DM_POLICY — если стоит allowlist, отправитель должен быть в WEIXIN_ALLOWED_USERS |
| Бот игнорирует групповые сообщения | По умолчанию политика групп — disabled. Поставьте WEIXIN_GROUP_POLICY=open или allowlist, но учтите: идентичности iLink-бота из входа по QR (...@im.bot), как правило, вообще не получают обычные групповые сообщения WeChat. Если в логах шлюза нет сырых входящих событий по группам, ограничение лежит на стороне iLink, а не в Hermes. |
| Скачивание/выгрузка медиа не работает | Убедитесь, что установлен cryptography. Проверьте сетевой доступ к novac2c.cdn.weixin.qq.com |
Blocked unsafe URL (SSRF protection) | Исходящий URL медиа указывает на приватный/внутренний адрес. Разрешены только публичные URL |
| Голосовые сообщения приходят как текст | Если WeChat даёт расшифровку, адаптер берёт текст. Это ожидаемое поведение |
| Сообщения дублируются | Адаптер дедуплицирует по ID сообщения. Если видите дубли, проверьте, не запущено ли несколько экземпляров шлюза |
iLink POST ... HTTP 4xx/5xx | Ошибка API от сервиса iLink. Проверьте действительность токена и сетевую связность |
| QR-код не отрисовывается в терминале | Переустановите с extra-пакетом messaging: cd ~/.hermes/hermes-agent && python -c "import pm; pm.sync_venv(['messaging'], explicit=True)". Либо откройте URL, напечатанный над QR-кодом |