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 按需读取:只读需要的章节即可,不必全文加载。
- 影子模块: 31-51 行
- Быстрый старт: 53-78 行
- Пять изолирующих барьеров: 80-90 行
- Честные границы (не остановить): 92-101 行
- Промотирование и откат: 103-107 行
- Связанные документы: 109-113 行
- 消息追踪: 115-126 行
- Принцип работы: 128-154 行
- Конфигурация: 156-170 行
- Атрибуция сообщений (отправитель и метаданные): 172-224 行
- API-интерфейсы изменения настроек в процессе работы: 226-246 行
- Риски и аудит: что нужно знать перед снятием ограничений: 248-272 行
- Границы с другими механизмами: 274-278 行
- Связанные документы: 280-284 行
影子模块
来源:
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 >!--}
- Запуск:
await sdk.module.shadow_start("roll", source="путь/к/v2")— новая версия кода работает параллельно со старой под отдельным владельцем (по умолчанию имя владельца берется из пути) - Теневой модуль не участвует в реальной маршрутизации и графе зависимостей: команды с одинаковыми именами выполняются в директории теневого модуля, маршрутизация регистрируется, но не привязывается, тихая трансляция жизненного цикла,
module.callи разрешение зависимостей по-прежнему указывают на v1 - Принятие решения о переходе всегда осуществляется человеком:
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": [...]}
- v1 фактическая отправка: из бот-таймлайна в инбоксе (transcript)
- v2 предполагаемая отправка: теневой отчёт (запись в исходящем трафике о "что хотели отправить")
- Оба сопоставляются по
trace_id— для одного и того же сообщения, почему один и другой вариант сработали/не сработали, что хотели отправить/что отправили — всё видно
После проверки без ошибок:
await sdk.module.promote_shadow("roll") # Промоутить, при ошибке автоматически откатить к v1
await sdk.module.dismiss_shadow("roll") # Или отменить теневой режим
Пять изолирующих барьеров
| Барьер | Механизм |
|---|---|
| Копия события | Теневой обработчик получает независимую копию события (с меткой shadow) — изменения / признание / остановка распространения теневого события влияют только на копию, не затрагивая оригинальную цепочку событий |
| Выходная дверь | Все вызовы DSL Send и Api теневого события перехватываются и регистрируются (возвращается фиктивный ответ), событие не отправляется наружу — теневой обработчик не будет повторно отвечать |
| Слой перекрытия хранилища | Запись KV теневого события происходит в слое перекрытия в памяти и не сохраняется в базу данных; чтение сначала проверяет слой перекрытия, при неудаче — обращается к реальной базе данных (灰度 процесс работает с реальными данными); удаление помечается как "могильный камень" |
| Блокировка маршрутизации | HTTP/WS/SSE маршруты теневого события регистрируются, но не подключаются; команды с тем же именем записываются в каталог теневых команд, методы платформенных событий запрещены к вставке |
| Тихий жизненный цикл | Теневой обработчик не транслирует собственные события жизненного цикла и не участвует в экосистемной зависимости (вызовы module.call и разрешение зависимостей по-прежнему указывают на v1, чтобы избежать зависимости от незавершённого продукта) |
Наследование конфигурации: теневой обработчик по умолчанию наследует конфигурационные разделы оригинального модуля (иначе серая версия будет отличаться от оригинала), после перевода в основной режим конфигурация применяется на месте.
Честные границы (не остановить)
- Отправка / API / хранилище KV / единый HTTP-клиент через фреймворк всё можно остановить;
модули, обходящие фреймворк, запускают
aiohttpнапрямую, создают потоки и пишут во внешние системы — фреймворк не может остановить - Чтение и запись ORM не входят в область покрытия (по строкам overlay невозможно реализовать чисто на уровне SQL) — в период теневого режима рекомендуется избегать зависимости от ORM для обеспечения изоляции
- Источник теневого режима — локальный путь: новая версия кода импортируется по пути, загружается отдельным владельцем; один и тот же пакет PyPI
в одном и том же интерпретаторе ограничен ключом
sys.modules, не позволяя одновременно использовать две версии - Аудитор утечек (
sdk.module.audit) видит принадлежность теневых ресурсов; побочные эффекты от обхода фреймворка по крайней мере не будут бесшумными
Промотирование и откат
Процесс promote: создание снимка текущей версии (включая все зависимые модули) → полное удаление → регистрация теневого экземпляра с настоящим именем → загрузка → при сбое на любом этапе автоматический откат, старый экземпляр продолжает обслуживать запросы (семантика "по возможности": эффекты, вызванные уже выполненным on_unload, не могут быть отменены, после отката старый экземпляр находится в состоянии завершения). После успешного промотирования теневые ресурсы утилизируются, и связь снимается; конфигурация оригинального модуля применяется на месте.
Примечание о сохранении изменений: промотирование происходит в режиме выполнения — после перезапуска будет продолжать работать версия v2. Необходимо сохранить новую версию (установить новую версию с помощью pip install -U или заменить файлы плагина). Переключение в режиме выполнения не заменяет пакетный менеджер.
Связанные документы
- Система собственности (owner) — основа механизма изоляции теней с независимым собственником
- Взаимодействие сессии —
trace_idи ящик для входящих сообщений (источник данных с выравниванием по разнице) - Область видимости (scope) — контрольный интерфейс для входа и выхода событий
消息追踪
来源:
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:
- Входящие:
nickname(никнейм на момент события, при поиске возвращается текущий),self_id(ID бота),sub_type,message_id; для эхо-сообщений дополнительноself_echo: true; - Исходящие: контекст отправки (
platform/method/detail_type/target_id/bot_id/trace_id/preview) иmodule(модуль-отправитель, см. ниже).
Для записей старого формата (до появления поля 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()
- Значение
0означает отключение соответствующей стратегии (в логах будет предупреждение); отрицательные числа вызываютValueError; - Переопределение вступает в силу при следующей очистке по стратегии (лениво, задержка до ~32 записей);
- Единичный экземпляр
transcriptможно импортировать из корневого пакета, а также использоватьsdk.transcript.
Риски и аудит: что нужно знать перед снятием ограничений
При отключении max_per_session / ttl_hours (или при значительном увеличении) таблица transcript будет расти бесконечно с ростом количества сообщений. Последствия:
- Заполнение диска — файл sqlite будет постоянно увеличиваться, и при исчерпании диска доступ к хранилищу станет невозможным;
- Ухудшение производительности запросов — стоимость операций
get()/recent()/get_by_trace()будет расти с увеличением таблицы; - Нагрузка на память потребителей (истинная причина 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 — убедитесь, что это ваше действие.
Границы с другими механизмами
- Таймеры сессий взаимодействия / wait_reply: см. Система взаимодействующих сессий, не связана с транскриптом;
- Восстановление истории диалога:
resume(with_history=N)получает последние N записей из транскрипта — если транскрипт обрезан, количество восстанавливаемых записей будет меньше (увеличениеmax_per_sessionувеличит окно восстановления); - Сравнение модулей-дубликатов: зависит от запроса
get_by_trace, короткий TTL транскрипта сократит окно сравнения.
Связанные документы
- Места, где используются записи транскрипта: восстановление диалога Conversation, чтение на уровне событий event.history, сравнение модулей-дубликатов Теневые модули и градиентное внедрение
- Интерфейсы хранилища для аудита размера таблицы → Хранилище и SQL-конструктор запросов