简体中文 English 繁體中文 日本語 Русский
本文为静态镜像,内容以交互版为准 在交互式文档中心打开 →

Система интерактивных сессий

Note

В этой главе требуется ErisPulse 2.8.0+.

ErisPulse реализует "постоянное взаимодействие с пользователем" на уровне фреймворка: от одной wait_reply до периодических напоминаний, ожидания нескольких потоков, взаимоисключающих сессий и восстановления после перезапуска — всё это управляется единым менеджером интерактивных сессий (Core/Event/interaction.py, sdk.interaction).

{!--< tips >!--} Каждая из описанных в этой статье возможностей имеет владельца (owner): ожидание, аренды, таймеры все записывают имя модуля, при регистрации, и при выгрузке модуля / отключении адаптера фреймворк автоматически очищает ожидания и уведомляет ожидаемую сторону, а не заставляет ждать истечения таймаута — это расширение системы владения в контексте взаимодействия (см. Система владения). {!--< /tips >!--}

Ожидание ответа: wait_reply

wait_reply — основа интерактивных сессий: приостанавливает текущую корутину, ожидая ответа от пользователя в следующем сообщении.

from ErisPulse.Core.Event import command

@command("ask")
async def ask_command(event):
    reply = await event.wait_reply(prompt="Введите ваше имя:", timeout=30)
    if reply is None:
        await event.reply("Таймаут")
        return
    await event.reply(f"Привет, {reply.get_text()}!")

Полный список параметров

Параметр Описание Значение по умолчанию
prompt Сообщение-подсказка, отправляемое до приостановки None
timeout Время ожидания (секунды) 60
pattern Фильтр шаблонов (glob: * / ? / [seq]), если не совпадает — продолжить ожидание None
regex Регулярное выражение (должно совпадать с pattern, если заданы оба), если не совпадает — продолжить ожидание None
validator Функция проверки (принимает Event, возвращает bool), если не проходит — продолжить ожидание None
callback Колбэк при получении ответа (альтернатива возвращаемому значению) None
method Метод отправки подсказки "Text"
session Сессионное ожидание: ответ любого участника той же сессии (группы / канала) может быть захвачен False
# Принимает только цифровые суммы, иначе продолжает ожидание
reply = await event.wait_reply("Введите сумму:", regex=r"\d+\s*元", timeout=30)

# Сессионное ожидание: в сценариях совместной работы в группе, любой участник может ответить
reply = await event.wait_reply(session=True, prompt="Кто-нибудь может ответить?")

Когда ожидание будет отменено

Ожидание больше не приводит к "только ожиданию истечения таймаута" — следующие условия немедленно приводят к прекращению (возврат None из wait_reply), а не к ожиданию истечения таймаута:

Триггер Причина отмены (InteractionCancelled.reason) Описание
Модуль-владелец выгружен / отключен owner_unload Очистка владения: кто зарегистрировал ожидание, тот и удаляет его
Адаптер остановлен / перезапущен platform_stop Все ожидания, связанные с платформой, отменяются
В той же сессии появилось новое ожидание / аренда conflict См. ниже "Арбитраж сессий"
Ответивший пользователь заблокирован / модуль-владелец отвязан revoked Повторная проверка прав: по scope и владельцу
Ответ пользователя совпадает — Нормальный путь, возвращает событие ответа

Нижележащее исключение — InteractionCancelled (входит в иерархию InteractionError), wait_reply уже преобразует его в возврат None; вызывающие, которым нужна причина, могут использовать низкоуровневый API sdk.interaction.register().

Полная цепочка проверки при ответе

Когда приходит сообщение с ответом, менеджер взаимодействия проверяет его по следующей последовательности (выполняется до сопоставления команд, приоритет сохраняется — даже если сообщение уже обработано другим процессором, приостановленный диалог может быть завершен):

Соответствие сессионному ключу (точная пользовательская мера → сессионная отмена)
  → Фильтрация текста по pattern / regex (не совпадает — продолжить ожидание)
  → Проверка validator (не проходит — продолжить ожидание)
  → Повторная проверка прав (по scope и владельцу, не проходит — завершить ожидание)
  → Пробуждение ожидающей стороны + признание события (mark_processed)

Сессионные таймеры: remind / escalate

Превращает "таймаут" из возвращаемого значения в управляемый примитив. Таймеры привязаны к сессии взаимодействия и автоматически отменяются при выгрузке модуля / остановке адаптера, лимит активных remind на одну сессию — 5 шт.

remind: напоминание при отсутствии ответа

@command("ticket")
async def ticket_command(event):
    await event.reply("Заявка создана, результат будет здесь")
    # Если нет ответа за 5 минут, мягко напомнить; любой ответ от пользователя автоматически отменяет напоминание
    event.remind(300, "Есть ли кто-нибудь? С результатом свяжемся")
    reply = await event.wait_reply(timeout=3600)
    ...

escalate: обязательное уведомление по истечении времени

event.escalate(1800, lambda e: notify_master(f"Заявка не обработана 30 минут: {event.get_command_args()}"))

Единственное отличие от remind: не отменяется ответом пользователя — действие "повышения" (уведомление владельца, передача в ручной обход) — это обязательное уведомление по истечении времени, отменяется только вручную cancel() / выгрузкой модуля / остановкой адаптера.

remind escalate
Действие по истечении Отправка текста / выполнение callback Выполнение callback
Ответ пользователя Автоматически отменяется Не влияет
Очистка владения (выгрузка / остановка платформы) Отменяется Отменяется
Лимит на сессию 5 Не ограничен (очистка по владению в качестве резерва)

Многопоточное ожидание: expect + select

Ожидание нескольких условий одновременно, первое поступившее — выполняется — типичный сценарий: ожидание одобрения администратором и одновременно ожидание отмены пользователем, голосование между участниками.

which, reply = await event.select(
    event.expect(pattern="同意*", user="10001"),
    event.expect(pattern="拒绝*", user="10002"),
    event.expect(validator=lambda e: e.get_text() == "搁置", session=True),
    timeout=60,
)
if which is None:
    await event.reply("В течение 60 секунд не было ответов")
elif which == 0:
    await event.reply("Одобрено")
elif which == 1:
    await event.reply("Отклонено")

{!--< tips >!--} select по сравнению с ручной обработкой asyncio.wait в многопоточном режиме: автоматическая очистка несовпадающих ожиданий, автоматическое признание захваченного события, все проверки прав и очистка владения автоматически выполняются — не нужно управлять Future вручную. {!--< /tips >!--}

Взаимоисключающие сессии: acquire / hold / get_owner_of

Владение переходит от "ресурсов" к "сессиям" — "кто сейчас занят этим пользователем" становится первостепенным вопросом.

# Запрос: кто сейчас взаимодействует с этой сессией? (если свободна — возвращает None)
owner = sdk.interaction.get_owner_of(event)
if owner and owner != "MyModule":
    return  # Другой модуль уже в диалоге, избегаем мешать

# Взаимоисключающая аренда: эксклюзивная сессия (политика deny, если занята — возвращает None)
lease = sdk.interaction.acquire(event)          # По умолчанию TTL 1 час, можно передать ttl=
if lease is None:
    return  # Уже занята
try:
    ...  # Эксклюзивное взаимодействие
finally:
    lease.release()

Форма контекстного менеджера (при неудаче выбрасывает SessionOccupiedError):

with sdk.interaction.hold(event) as lease:
    ...  # Автоматически освобождается при выходе

Аренда поддерживает renew(ttl) для продления; TTL ленивый — просроченная аренда автоматически удаляется при следующем обращении.

Conversation.resume() при возобновлении диалога автоматически приобретает аренду (см. Многошаговый диалог "Восстановление = захват") — восстановленный диалог автоматически владеет сессией, другие модули не могут вмешаться.

Почтовый ящик сессии: event.history

Единая запись недавних сообщений в сессии (со стороны пользователя и бота), используется как общая база контекста для AI, предотвращения повторов, анализа поведения — модули больше не хранят историю отдельно.

messages = await event.history(20)   # Последние 20 сообщений в сессии, в порядке возрастания времени
for m in messages:
    print(m["role"], ":", m["text"])  # role: "user" / "bot"

Транзакции сообщений: message_tx

Все исходящие отправки в транзакции автоматически записываются; при исключительном выходе автоматически отменяются отправленные сообщения (если адаптер не реализует delete_message, пропускается, но журнал записей остаётся).

async with event.message_tx():
    await event.reply("Обрабатываю, подождите")
    result = await do_something()          # Здесь выбрасывается исключение →
    await event.reply(f"Готово: {result}")   # Сообщение "Обрабатываю..." автоматически удаляется

Отправка вне транзакции не записывается (нулевой накладной расход); get_send_receipts() позволяет просмотреть отправленные в текущей транзакции сообщения.

Трассировка цепочки: trace-id

Каждое входящее событие автоматически получает идентификатор трассировки (используется event["id"], если отсутствует — генерируется), который проходит через:

При обработке сообщения несколькими модулями, один и тот же ID можно использовать для объединения всей цепочки (логи / медленные запросы / аудит).

Связь с другими системами

Связанные документы