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

name: erispulse-shadow-transcript description: "适用于:新旧版本灰度对照(shadow)、消息收件箱与行为追踪(transcript)。覆盖场景:Когда читать эту статью: при необходимости постепенного развертывания — одновременная пробная работа новой версии, сравнение поведения и принятие решения о переходе или откате. Если вам это не нужно, можете пропустить эту статью — это не повлияет на использование других функций.。Когда нужно читать эту статью: если вам нужны история сообщений, восстановление диалога или настройка стратегии хранения транскриптов. Если не нужно — пропустите, это не повлияет на использование других функций.。"

ErisPulse 影子灰度与消息追踪

This skill is auto-built from the ErisPulse documentation. Repository: https://github.com/ErisPulse/ErisPulse Source paths are annotated per section; see docs/ in the repository for the latest content.

内容大纲

AI 按需读取:只读需要的章节即可,不必全文加载。

影子模块

来源: docs/ru/advanced/shadow.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/advanced/shadow.md)

Теневые модули и постепенный переход

Tip

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

Что вы узнаете: пять уровней изоляции теневых модулей, способ сопоставления поведения (diff), процесс одобренного перехода (promote) и автоматического отката при сбое, а также границы, которые не могут быть пересечены фреймворком.

В этой статье не рассматриваются: общие механизмы горячей перезагрузки и управления принадлежностью модулей (см. систему владельцев (owner)).

Теневой модуль = новая версия того же модуля, которая параллельно работает с текущей версией под отдельным владельцем (например, roll_shadow), получая копии реальных событий и перехватывая исходящие действия для учета, а не отправляя их наружу. Сравнение поведения двух версий происходит в shadow_diff. После подтверждения, что новая версия безопасна, можно выполнить promote, чтобы сразу принять её как основную, или dismiss, чтобы отказаться от неё в любой момент. Код модуля не изменяется, вся работа осуществляется с помощью API во время выполнения: такие же операции, как load / unload / reload, можно вызывать через Dashboard или пользовательские модули управления, без необходимости писать какие-либо конфигурационные параметры.

{!--< tips >!--}

  1. Запуск: await sdk.module.shadow_start("roll", source="путь/к/v2") — новая версия кода работает параллельно со старой под отдельным владельцем (по умолчанию имя владельца берется из пути)
  2. Теневой модуль не участвует в реальной маршрутизации и графе зависимостей: команды с одинаковыми именами выполняются в директории теневого модуля, маршрутизация регистрируется, но не привязывается, тихая трансляция жизненного цикла, module.call и разрешение зависимостей по-прежнему указывают на v1
  3. Принятие решения о переходе всегда осуществляется человеком: await sdk.module.promote_shadow("roll"), при сбое происходит автоматический откат к старой версии; dismiss_shadow позволяет в любой момент отказаться от теневого модуля {!--< /tips >!--}

Быстрый старт

# Код v2: обычный модуль, без теневого режима (любой каталог, например downloads/roll_v2/)
# В онлайн-боте (через Dashboard / модуль управления), запуск грейд-теста одной строкой:
await sdk.module.shadow_start("roll", source="downloads/roll_v2")
# → Теневой модуль с отдельным owner "roll_v2" параллельно работает с v1, исходящие сообщения перехватываются и записываются

# Сравнение поведения во время тестирования:
report = sdk.module.shadow_diff("roll")
# {"shadow_owner": "roll_v2", "count": 3, "aligned": [...]}

После проверки без ошибок:

await sdk.module.promote_shadow("roll")   # Промоутить, при ошибке автоматически откатить к v1
await sdk.module.dismiss_shadow("roll")   # Или отменить теневой режим

Пять изолирующих барьеров

Барьер Механизм
Копия события Теневой обработчик получает независимую копию события (с меткой shadow) — изменения / признание / остановка распространения теневого события влияют только на копию, не затрагивая оригинальную цепочку событий
Выходная дверь Все вызовы DSL Send и Api теневого события перехватываются и регистрируются (возвращается фиктивный ответ), событие не отправляется наружу — теневой обработчик не будет повторно отвечать
Слой перекрытия хранилища Запись KV теневого события происходит в слое перекрытия в памяти и не сохраняется в базу данных; чтение сначала проверяет слой перекрытия, при неудаче — обращается к реальной базе данных (灰度 процесс работает с реальными данными); удаление помечается как "могильный камень"
Блокировка маршрутизации HTTP/WS/SSE маршруты теневого события регистрируются, но не подключаются; команды с тем же именем записываются в каталог теневых команд, методы платформенных событий запрещены к вставке
Тихий жизненный цикл Теневой обработчик не транслирует собственные события жизненного цикла и не участвует в экосистемной зависимости (вызовы module.call и разрешение зависимостей по-прежнему указывают на v1, чтобы избежать зависимости от незавершённого продукта)

Наследование конфигурации: теневой обработчик по умолчанию наследует конфигурационные разделы оригинального модуля (иначе серая версия будет отличаться от оригинала), после перевода в основной режим конфигурация применяется на месте.

Честные границы (не остановить)

Промотирование и откат

Процесс promote: создание снимка текущей версии (включая все зависимые модули) → полное удаление → регистрация теневого экземпляра с настоящим именем → загрузка → при сбое на любом этапе автоматический откат, старый экземпляр продолжает обслуживать запросы (семантика "по возможности": эффекты, вызванные уже выполненным on_unload, не могут быть отменены, после отката старый экземпляр находится в состоянии завершения). После успешного промотирования теневые ресурсы утилизируются, и связь снимается; конфигурация оригинального модуля применяется на месте.

Примечание о сохранении изменений: промотирование происходит в режиме выполнения — после перезапуска будет продолжать работать версия v2. Необходимо сохранить новую версию (установить новую версию с помощью pip install -U или заменить файлы плагина). Переключение в режиме выполнения не заменяет пакетный менеджер.

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

消息追踪

来源: docs/ru/advanced/transcript.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/advanced/transcript.md)

Транскрипт сессии (Transcript)

Tip

Когда нужно читать эту статью: если вам нужны история сообщений, восстановление диалога или настройка стратегии хранения транскриптов. Если не нужно — пропустите, это не повлияет на использование других функций.

Транскрипт сессии автоматически записывает сообщения, отправленные и полученные роботом (организованные по ключу сессии platform:detail_type:target_id), предоставляя исторические данные для восстановления диалога (event.history), сравнения модулей-дубликатов, воспроизведения при холодном запуске и т.д. В этой статье описаны модель хранения, что защищает каждый параметр стратегии хранения, а также API-интерфейсы для изменения настроек в процессе работы и реальные последствия снятия ограничений.

Прочитайте раздел «Риски и аудит» перед изменением ограничений — у каждого параметра есть свои причины.

Принцип работы

Сообщение ──append()──▶ Буфер записи (в памяти, двусторонняя очередь, жесткий лимит 4096 строк)
                        │  Каждую 1 секунду батч-запись на диск (максимум 64 строки на транзакцию)
                        ▼
              Таблица transcript в хранилище (по умолчанию sqlite)
                        │  Приблизительно каждые 32 строки запускается ленивая очистка по стратегии хранения
                        ▼
              ┌─ Удаление по FIFO для одной сессии (max_per_session, по умолчанию 50 строк)
              └─ Удаление по TTL для всей таблицы (ttl_hours, по умолчанию 168 = 7 дней)

Три уровня защиты, каждый защищает от разных рисков:

Параметр Значение по умолчанию Что защищает Последствия при отключении (установке в 0)
Жесткий лимит буфера 4096 строк (константа, неизменяемый) Оперативная память: предотвращает переполнение RAM при недоступности хранилища Невозможно отключить — это защита памяти
Ограничение длины текста 2000 символов (константа, неизменяемый) Занимаемое место в памяти и на диске на одну запись Невозможно отключить
max_per_session 50 строк/сессия Рост дискового пространства: предотвращает бесконечный рост истории одной сессии История сессии будет бесконечно расти
ttl_hours 168 (7 дней) Рост дискового пространства: предотвращает бесконечный рост истории всех сессий Таблица transcript будет бесконечно расти

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

Tip

Приложениям, не нуждающимся в памяти сессий, можно полностью отключить: ErisPulse.transcript.enabled = false Это остановит автоматическую запись входящих/исходящих сообщений — не будет записи в буфер и в хранилище, что сэкономит постоянную память и постоянные операции ввода-вывода на стороне хранилища. Транскрипт сессии служит в основном для инъекции контекста в ИИ и предотвращения повторов сообщений. Если ваш робот (или модуль) не использует эти возможности (например, чисто командный робот), рекомендуется отключить.

Конфигурация

[ErisPulse.transcript]
enabled = true          # Включить автоматическую запись
max_per_session = 50    # Количество строк, хранящихся в одной сессии (0 = отключить обрезку для сессии)
ttl_hours = 168.0       # Общая длительность хранения (часы, 0 = никогда не удалять)
self_echo = "bot"       # Обработка эхо-сообщений от бота: bot / skip / user

Переменные окружения также действуют: ERISPULSE_TRANSCRIPT_MAX_PER_SESSION, ERISPULSE_TRANSCRIPT_TTL_HOURS, ERISPULSE_TRANSCRIPT_SELF_ECHO. Изменения конфигурации применяются динамически (в пределах ритма записи в буфер, задержка до ~1 секунды).

Tip

Если память сессий не нужна, отключите её: единственный потребитель транскрипта — это восстановление диалога, сравнение дубликатов и воспроизведение при холодном запуске. Если ваш робот (или развернутые модули) полностью не используют эти возможности, установка enabled = false позволит сэкономить память буфера и фоновые операции записи на диск — при этом обработка сообщений и команд останется неизменной.

Атрибуция сообщений (отправитель и метаданные)

Все интерфейсы запросов (get / recent / get_by_trace / event.history) возвращают записи с атрибутивными полями, позволяющими различать "кто отправил" и "какой модуль отправил":

Поле Значение
sender Идентификатор отправителя: для входящих сообщений — user_id, для исходящих — self_id бота / self.user_id при эхо-сообщениях
meta Метаданные на момент записи (белый список, не полный raw event)

Белый список полей meta:

Для записей старого формата (до появления поля meta) meta будет пустым {}; при обращении meta.get("nickname", "") будет возвращаться пустая строка, что обеспечивает обратную совместимость; при отсутствии поддержки ALTER в хранилище также будет возвращаться {}.

Модуль отправки (meta.module)

На одном экземпляре могут отправлять сообщения несколько модулей (чат, управление, напоминания). Поле meta.module в исходящих записях указывает отправителя, приоритет значений: current_owner > current_caller > "":

Сценарий отправки Значение module
Модуль вызван через module.call Имя целевого модуля
Модуль отправляет внутри обработчика событий Имя модуля, зарегистрировавшего обработчик (автоматически вставляется в процессе распределения)
Рамка или без модульного контекста ""

Поле module также присутствует в нагрузке хуков message.sent / message.sending (send_ctx).

Эхо-сообщения от бота (self_echo)

Некоторые платформы/протоколы отправляют эхо-сообщения от бота как обычные события (например, OneBot v11 с report-self-message, Telegram outgoing message). Рамка определяет эхо по условию self.user_id == user_id и обрабатывает их согласно настройке self_echo:

Значение Действие
"bot" (по умолчанию) Запись сохраняется, но role="bot", sender=self_id, meta.self_echo=true
"skip" Не записывается (если адаптер также вызывает message.sent, то исходящая запись уже есть)
"user" Запись с role="user" (старый способ, резервный вариант)

На платформах, где эхо не отправляется (self.user_id != user_id всегда), поведение идентично отключенной функции.

Фильтрация по сессии в recent

from ErisPulse import transcript

# Глобальное окно времени по всем сессиям (семантика холодного запуска, по умолчанию)
transcript.recent(3600)

# Ограничение одной сессией: фильтрация на стороне сервера, избегает глобального запроса и последующей фильтрации на клиенте
transcript.recent(3600, session_key="onebot11:group:123")

session_key=None сохраняет семантику по всем сессиям; пустая строка "" эквивалентна отсутствию фильтрации (для совместимости).

API-интерфейсы изменения настроек в процессе работы

Для динамического изменения настроек (например, временного увеличения срока хранения для определённой сессии):

from ErisPulse import transcript

# Переопределение (приоритет над конфигурацией; действует только в памяти, после перезапуска возвращается значение из конфигурации)
transcript.set_retention(max_per_session=500, ttl_hours=24 * 30)

# Получение текущих значений и источника их получения
transcript.get_retention()
# {'max_per_session': 500, 'ttl_hours': 720.0, 'overridden': ['max_per_session', 'ttl_hours']}

# Восстановление значений из конфигурации
transcript.reset_retention()

Риски и аудит: что нужно знать перед снятием ограничений

При отключении max_per_session / ttl_hours (или при значительном увеличении) таблица transcript будет расти бесконечно с ростом количества сообщений. Последствия:

  1. Заполнение диска — файл sqlite будет постоянно увеличиваться, и при исчерпании диска доступ к хранилищу станет невозможным;
  2. Ухудшение производительности запросов — стоимость операций get() / recent() / get_by_trace() будет расти с увеличением таблицы;
  3. Нагрузка на память потребителей (истинная причина OOM) — потребители, загружающие историю целиком в память (восстановление диалога с историей, панель с полной выгрузкой, ваш код get(session, n) с большим n), будут потреблять всё больше памяти, что в крайних случаях приведёт к OOM kill.

Перед отключением задайте себе три вопроса: сколько истории нужно? Какой объём? Кто будет удалять? Если ответ зависит от "никогда не удалять", правильным решением будет архивирование (периодически перемещать старые данные в специальную базу), а не бесконечное хранение в транскрипте.

Средства аудита (регулярная проверка размера таблицы):

# Получение количества строк и объёма с помощью ORM/SQL-конструктора (пример для sqlite)
from ErisPulse import storage

row = storage.Table("transcript").raw_sql("SELECT COUNT(*) AS n FROM transcript")
-- Прямой запрос к файлу sqlite (объём таблицы)
SELECT COUNT(*) FROM transcript;

Получение предупреждения 保留策略已被关闭 в логах означает, что кто-то установил значение в 0 — убедитесь, что это ваше действие.

Границы с другими механизмами

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