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

name: erispulse-errors-troubleshooting description: "适用于:遇到异常/组件不工作时的诊断路径:异常总览、排障指南、bug 追踪。覆盖场景:Все пользовательские исключения ErisPulse наследуются от ErisPulseError. Исключения底层 библиотек (например, aiohttp, aiomysql и др.) будут перехвачены внутри фреймворка и преобразованы в соответствующие исключения ErisPulse. В коде бизнес-логики не следует зависеть от типов исключений底层 библиотек.。Документация отслеживает известные ошибки ErisPulse SDK и их исправления, отсортированные по времени исправления.。"

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/errors.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/advanced/errors.md)

Система исключений и руководство по обработке

Note

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

Все пользовательские исключения ErisPulse наследуются от ErisPulseError. Исключения底层 библиотек (например, aiohttp, aiomysql и др.) будут перехвачены внутри фреймворка и преобразованы в соответствующие исключения ErisPulse. В коде бизнес-логики не следует зависеть от типов исключений底层 библиотек.

{!--< tips >!--}

  1. Для широкого перехвата: перехватите ErisPulseError (базовый класс всех исключений фреймворка)
  2. Для точной обработки: перехватывайте по модулям (например, ModuleCallTimeoutError, StorageUnreachableError)
  3. Операции с хранилищем по умолчанию не выбрасывают исключения: при сбое записывается в лог и возвращается False / None / default; для отслеживания изменений состояния подключений подписывайтесь на события storage.unreachable / storage.recovered {!--< /tips >!--}

Обзор исключений

ErisPulseError                      # Базовый класс всех исключений фреймворка
├── ClientError                     # Базовый класс исключений HTTP/WS клиента (Core/client)
│   ├── ClientConnectionError       # Ошибка на уровне соединения: неудача разрешения DNS, отказ в подключении, недоступность сети
│   ├── ClientTimeoutError          # Ошибка таймаута запроса
│   └── HTTPStatusError             # Ошибка HTTP статус-кода (например, 4xx/5xx и raise_for_status)
├── WebSocketError                  # Базовый класс исключений WebSocket (WebSocket соединение в Core/client)
│   └── WebSocketDisconnect         # Отключение WebSocket (общее для сервера/клиента)
├── ConnectionRegistryError         # Базовый класс исключений реестра соединений (Core/connections, 2.10+)
│   ├── ConnectionNotFoundError     # Соединение не существует/было отключено и отменено (connections.get/close)
│   └── ConnectionPermissionError   # Закрытие чужого соединения без прав (закрытие доступно только создателю)
├── StorageError                    # Базовый класс исключений хранилища (Core/storage)
│   └── StorageUnreachableError     # Недоступен бэкенд хранилища (исчерпаны попытки подключения: недоступна база данных/ошибка учетных данных)
├── InteractionError                # Базовый класс исключений интерактивной сессии (Core/Event/interaction)
│   ├── InteractionCancelled        # Приостановленное ожидание/аренда отменена (wait_reply возвращает None)
│   └── SessionOccupiedError        # Исключительная аренда сессии занята (hold() не удалось получить)
├── ModuleError                     # Базовый класс исключений модульной системы (Core/module)
│   └── ModuleCallError             # Базовый класс исключений вызова модуля
│       ├── ModuleNotAvailableError # Целевой модуль не зарегистрирован/не активен/инициализация не удалась (включая ленивую загрузку)
│       ├── ServiceNotProvidedError # Целевой модуль не объявил эту услугу (не в белом списке meta.services)
│       └── ModuleCallTimeoutError  # Выполнение вызываемого метода превысило таймаут (по умолчанию 30s)
├── ShadowError                     # Базовый класс исключений теневого модуля (Core/shadow)
│   ├── ShadowStateError            # Недопустимое состояние: целевой модуль не загружен/уже есть активный теневой модуль/не привязан теневой модуль (start/dismiss)
│   ├── ShadowSourceError           # Источник теневого модуля недоступен: путь не существует/отсутствует загрузчик/ошибка загрузки (start)
│   └── ShadowPromoteError          # Ошибка повышения: теневой модуль не зарегистрирован/не загружен/загрузчик не имеет снимка/ошибка перезагрузки (promote)
└── StrictModeError                 # Критическое нарушение строгого режима (прерывает процесс запуска, loaders/strict)

Структурированные свойства

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

Исключение Свойства
ClientError (с подклассами) .url URL запроса, .method метод запроса, .attempts количество попыток (когда исчерпаны повторные попытки)
HTTPStatusError .status код состояния, .message сообщение ответа
WebSocketDisconnect .code код закрытия, .reason причина закрытия
ConnectionNotFoundError .connection_id идентификатор подключения
ConnectionPermissionError .connection_id идентификатор целевого подключения, .owner владелец подключения
StorageUnreachableError .backend имя бэкенда (sqlite/mysql/postgres), .cooldown время охлаждения в секундах
ModuleCallError (с подклассами) .module имя целевого модуля, .method имя целевого метода
ModuleCallTimeoutError Наследует .module/.method, также есть .timeout лимит времени ожидания (секунды)
InteractionCancelled .reason причина отмены, .wait_key ключ сессии
SessionOccupiedError .wait_key ключ сессии, .owner владелец
StrictModeError .violations список нарушений
from ErisPulse.Core.Bases.errors import ClientError

try:
    resp = await sdk.client.post(url, json=payload)
except ClientError as e:
    print(f"Запрос не удался {e.method} {e.url}, всего попыток {e.attempts}: {e}")

Описание исключений и места их возникновения

Серия исключений Client — Core/client.py / Core/Bases/client.py

Исключения sdk.client / HTTP-клиента и WebSocket-клиента при发起 запросов:

Исключение Место возникновения Типичные сценарии
ClientError Уровень обёртки запросов Другие клиентские ошибки (низкоуровневые aiohttp-исключения уже преобразованы)
ClientConnectionError Этап установки соединения Целевой сервис недоступен, DNS-ошибка, соединение отклонено
ClientTimeoutError Этап выполнения запроса Превышение времени ожидания запроса
HTTPStatusError raise_for_status() Код состояния ответа 4xx/5xx
from ErisPulse.Core.Bases.errors import ClientTimeoutError

try:
    resp = await sdk.client.get("https://api.example.com", timeout=5)
except ClientTimeoutError:
    ...

Серия исключений WebSocket — Core/client.py (send / receive)

Исключение Место возникновения Типичные сценарии
WebSocketError Методы WS-приёма/передачи Соединение закрыто, получено неожиданное сообщение, низкоуровневые WS-исключения
WebSocketDisconnect Методы WS-приёма/передачи Удалённая сторона корректно разорвала соединение (фреймворк автоматически переподключится)

Серия исключений регистрации соединений — Core/connections.py (2.10+)

Исключение Место возникновения Типичные сценарии
ConnectionNotFoundError connections.get / assign / dismiss / close Идентификатор соединения не существует или уже отключён
ConnectionPermissionError Некорректный вызов conn.close() Переключение модулей при закрытии чужого соединения (отправка и группы не ограничены)

Серия исключений Storage — Core/storage / Core/Bases/sql_base.py

Исключение Место возникновения Типичные сценарии
StorageError Уровень хранения Базовый класс исключений, связанных с хранением
StorageUnreachableError Этап создания пула База данных недоступна / неверные учетные данные / сетевая изоляция, исчерпаны попытки повтора

Семантика неудачных операций хранения: Операции KV и запросов по умолчанию не выбрасывают исключений — в случае неудачи запись в лог ERROR и возврат False / None / default (чтобы избежать блокировки работы фреймворка из-за проблем с соединением). Следовательно, бизнес-код обычно не перехватывает StorageUnreachableError (он в основном используется для прямого взаимодействия с底层存储 или пользовательских backends). Для получения информации о состоянии соединения во время выполнения следует подписываться на события жизненного цикла storage.unreachable / storage.recovered (см. События жизненного цикла).

Поведение при неудачном подключении описано в разделе Backend хранения → Поведение при неудачном подключении.

Взаимодействие — Core/Event/interaction.py

wait_reply / сеанс аренды / таймер напоминания:

Исключение Место возникновения Типичные сценарии
InteractionError Уровень взаимодействия сессии Базовый класс исключений сессии взаимодействия
InteractionCancelled При отмене ожидания Установлено на ожидаемом future (ожидание может перехватить и получить .reason)
SessionOccupiedError При неудачной аренде hold() Сеанс уже занят другим владельцем (.owner позволяет узнать владельца)

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

Серия исключений Module — Core/module.py (sdk.module.call)

Исключение Место возникновения Типичные сценарии
ModuleError Система модулей Базовый класс исключений системы модулей
ModuleCallError module.call() Базовый класс исключений вызова модуля
ModuleNotAvailableError module.call() / ленивое получение атрибута Целевой модуль не зарегистрирован / не включён / инициализация не удалась
ServiceNotProvidedError module.call() Метод не объявлен в белом списке meta.services целевого модуля
ModuleCallTimeoutError module.call() Вызываемая корутина превысила время ожидания (по умолчанию 30 сек.)

"Целевой модуль недоступен" в зависимости от пути доступа:

Путь доступа Исключение
await sdk.module.call("X", "method") ModuleNotAvailableError (типизированное)
sdk.module.X.attr (ленивое получение атрибута, после неудачной инициализации) ModuleNotAvailableError
sdk.module.X (при обращении к неактивированному модулю) AttributeError (стандартное поведение Python, hasattr зависит от этой семантики)
from ErisPulse.Core.Bases.errors import ModuleNotAvailableError, ServiceNotProvidedError

try:
    history = await sdk.module.call("Chat", "get_history", session_id, n=20)
except ModuleNotAvailableError:
    ...  # Целевой модуль не существует / не включён
except ServiceNotProvidedError:
    ...  # Целевой модуль не предоставляет этот сервис

Серия исключений Shadow — Core/shadow.py (тенированные модули)

Исключение Место возникновения Типичные сценарии
ShadowError Механизм тенирования Базовый класс исключений тенирования (общий перехват всех ошибок теней)
ShadowStateError shadow_start() / dismiss_shadow() Цель не загружена / уже активная тень / тень не привязана
ShadowSourceError shadow_start() Исходный путь не существует / отсутствует загрузчик плагинов / класс для загрузки отсутствует / загрузка не удалась
ShadowPromoteError promote_shadow() Тень не зарегистрирована / не загружена / загрузчик не поддерживает снапшоты / неудачная перезагрузка при повышении

Внутренняя проверка параметров фреймворка (ValueError)

Проверка параметров строителя запросов к хранилищу (пустой тип столбца, Insert не dict, небезопасный тип столбца и т.д.) выбрасывает стандартный ValueError — такие ошибки относятся к ошибкам программирования на этапе разработки, нормальный бизнес-код не должен перехватывать их, а должен исправлять вызов.

Неудача стандартных действий адаптера не выбрасывает исключений: возвращается словарь ответа с retcode (семантика протокола, например, retcode=10002 означает, что действие не реализовано) — это параллельный канал ошибок по сравнению с ClientError на уровне клиента. При разработке адаптера необходимо обрабатывать оба канала.

Рекомендации по перехвату

from ErisPulse.Core import ErisPulseError  # Базовый класс экспортируется через Core

try:
    ...
except ErisPulseError as e:
    ...  # Общий перехват: все пользовательские исключения фреймворка

Отключение шумного складывания

При остановке (или отмене фонового цикла) долгоживущих процессов, цикл событий часто выводит серию бесполезных сообщений вида Task was destroyed but it is pending!, которые являются шумом от интерпретатора. Встроенная в фреймворк функция складывания подобных шумов позволяет за 2-секундный интервал снизить уровень подобных сообщений до TRACE, объединить их и вывести только один раз, добавив в конец суффикс «(ещё N подобных сообщений было складываний)». Это сознательное снижение шума: наличие суффикса складывания не означает, что фреймворк подавил настоящие исключения. Бизнес-исключения (все категории в таблице выше) выводятся полностью. Складывание применяется только к шумам от интерпретатора при завершении, бизнес-исключения не затрагиваются. Какие уровни исключений заслуживают сохранения в логах, а какие можно снизить/складывать, определяется по правилам инженерной надежности ·分级异常留痕.

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

Выберите в зависимости от намерения:

已知问题追踪

来源: docs/ru/bug-tracker.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ru/bug-tracker.md)

Трекер ошибок

Документация отслеживает известные ошибки ErisPulse SDK и их исправления, отсортированные по времени исправления.

Для читателя Никакое программное обеспечение не может быть идеальным, даже самые внимательные разработчики могут оставить небольшие ошибки. В этот трекер включены только проблемы, которые реально влияют на работу — те, что слишком незначительны, чтобы даже назвать их «легкими», здесь не представлены. Хотя в списке значится много пунктов «серьезного» уровня, цель публичной записи этих ошибок — сделать отслеживание и восстановление более плавным, а не вызывать беспокойство: возможность видеть, записывать и исправлять проблемы — это доказательство постоянного улучшения проекта. Не стоит беспокоиться, увидев этот список: это инструмент для диагностики, а не источник страха.

Как читать и правила поддержки

  • Каждая запись об ошибке содержит структурированные поля: описание проблемы, анализ корневой причины, диапазон версий, в которых она влияет, схему исправления и т.д. Рекомендуется перед обновлением проверить, охватывает ли «диапазон версий» используемую вами версию.
  • Если вы хотите добавить новую запись об ошибке, добавьте её в соответствующее место, следуя описанным ниже полям и классификации серьезности/типа.

Описание полей

Обязательные поля

Поле Описание
Проблема Внешнее проявление ошибки, наблюдаемое пользователем. Стремитесь указать сообщения об ошибках или типичные сценарии
Причина Анализ корневой причины, указывающий на конкретные дефекты кода (включая «цепочку корневых причин» для сложных сценариев)
Влияющие версии Затронутый диапазон версий, формат введено версия - исправлено версия (включая бета-версии с обеих сторон)
Исправленная версия Номер конкретной версии, в которой была исправлена ошибка
Содержание исправления Краткое описание исправления, включая ключевые изменения в коде
Дата исправления Дата публикации соответствующей версии исправления, в формате YYYY/MM/DD
Серьезность Отмечено в соответствии с «шкалой серьезности»
Тип Отмечено в соответствии с «классификацией типов», может быть комбинированным (например, адаптер / маршрутизация)

Дополнительные поля

Поле Описание Сценарии применения
Шаги воспроизведения Минимальный путь для воспроизведения ошибки Рекомендуется для сложных и случайных ошибок
Связанные Ссылки на соответствующие Issues / PR / Commit Добавляется при наличии внешних обсуждений
Регрессионное тестирование Позиция тестовых случаев для проверки исправления и предотвращения повторного появления Добавляется, когда написаны тесты pytest

Шкала серьезности

Идентификатор Уровень Критерии Типичные проявления
🔴 Серьезный Причина сбоя процесса, потеря или повреждение данных, полная недоступность основных функций, уязвимости безопасности OOM Kill, невозможность отправки сообщения, невозможность загрузки модуля, неудачное горячее перезагрузка
🟡 Средний Нарушение функциональности, но есть пути обхода, недоступность неосновных функций, случайные проблемы Неверное определение состояния, повторное срабатывание, истечение кэша, неточное сообщение об ошибке
🟢 Легкий Не влияет на основную функциональность, проблемы качества кода или пользовательского опыта, потенциальные риски, не проявившиеся Устаревший API, мертвый код, отсутствие предупреждения в журнале

Классификация типов

Тип Охват
Система конфигурации ConfigManager, чтение и запись конфигурации, схема конфигурации, горячая перезагрузка
Система событий Event модуль (command/message/notice/request/meta), распределение событий, регистрация обработчиков
Адаптер AdapterManager, BaseAdapter, разбор аккаунтов, состояние Bot, промежуточное ПО
Маршрутизация RouterManager, HTTP/WebSocket/SSE маршруты, ограничение скорости, CORS
Клиент HttpClient, ClientWebSocket, aiohttp обертка
Хранилище StorageManager, SQLite, SQL генератор, вложенные ключи
Загрузка Loader, LazyModule, ModuleInitializer, строгий режим, обнаружение модулей
CLI epsdk команда, init/run/install, анализ параметров, обработка сигналов
Рантайм sdk.run/restart/uninit, жизненный цикл, сигналы, подпроцессы

Шаблон записи

При добавлении новой записи об ошибке используйте следующий формат:

### [BUG-XXX] Заголовок

**Проблема**: Описание проблемы (сообщение об ошибке или типичная ситуация)
**Причина**: Анализ корневой причины
**Влияющие версии**: Введено версия - Исправлено версия
**Исправленная версия**: x.x.x
**Содержание исправления**: Описание исправления
**Дата исправления**: YYYY/MM/DD

<!-- Дополнительные поля -->
**Шаги воспроизведения**: (рекомендуется для сложных ошибок)
**Связанные**: (ссылки на Issues/PR)
**Регрессионное тестирование**: (путь к тестовому случаю)

**Серьезность**: 🔴 Серьезный | 🟡 Средний | 🟢 Легкий
**Тип**: Система конфигурации / Система событий / Адаптер / Маршрутизация / Клиент / Хранилище / Загрузка / CLI / Рантайм

Общая статистика

Серьезность Количество
🔴 Серьезный 16
🟡 Средний 26
🟢 Легкий 3
Итого 45
Тип Количество
Система событий 12
Система конфигурации 12
Адаптер 7
CLI 3
Хранилище 3
Загрузка 4
Маршрутизация 2
Клиент 1
Рантайм 1

Примечание: Одна ошибка может относиться к нескольким типам, таблица отображает по основному типу. Примечание: Номера BUG-028 / BUG-031 отсутствуют (зарегистрированы и отменены, для сохранения стабильности нумерации не перенумерованы).


Исправленные ошибки

[BUG-001] Повторная регистрация обработчика событий приводит к многократному обработке события

Проблема: Использование нескольких @message / @notice декораторов для регистрации обработчиков приводит к тому, что одно и то же событие будет многократно срабатывать, вызывая многократное выполнение команды и повторное логирование.

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

Влияющие версии: 2.2.0-dev.0 - 2.2.1-dev.0

Исправленная версия: 2.2.1-dev.0

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

Дата исправления: 2025/08/18

Серьезность: 🔴 Серьезный

Тип: Система событий


[BUG-002] Ошибка типа пути конфигурации адаптера в команде init

Проблема: При использовании команды ep init для интерактивной инициализации, выбор конфигурации адаптера приводит к ошибке типа:

Интерактивная инициализация не удалась: unsupported operand type(s) for /: 'str' and 'str'

Причина: В версии 2.3.7 при изменении пути к файлу конфигурации, метод параметры типа не согласованы. _configure_adapters_interactive_sync принимает параметр типа str, но внутри используется оператор / класса Path для объединения путей.

Влияющие версии: 2.3.7 - 2.3.9-dev.1

Исправленная версия: 2.3.9-dev.1

Содержание исправления: Изменение типа параметра _configure_adapters_interactive_sync с str на Path, передача объекта Path при вызове.

Дата исправления: 2026/03/23

Серьезность: 🟡 Средний

Тип: CLI


[BUG-003] Команды не работают после перезапуска

Проблема: После вызова sdk.restart() команды, зарегистрированные с помощью @command, не срабатывают, что проявляется в том, что отправленная команда не вызывает ответ бота.

Причина: adapter.shutdown() очищает событийную шину, но состояние _linked_to_adapter_bus в BaseEventHandler не сбрасывается в False, поэтому _process_event метод считает, что уже подключен к событийной шине адаптера, и пропускает повторную регистрацию.

Влияющие версии: 2.2.x - 2.4.0-dev.2

Исправленная версия: 2.4.0-dev.3

Содержание исправления: Введение отслеживания состояния _linked_to_adapter_bus, при _clear_handlers() отключение от шины, при следующем register() автоматическая повторная регистрация, адаптация к сценариям shutdown/restart.

Дата исправления: 2026/04/09

Серьезность: 🔴 Серьезный

Тип: Система событий


[BUG-004] Обработчики событий жизненного цикла не очищаются

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

Причина: Словарь lifecycle._handlers никогда не очищается в uninit(), после перезапуска старые и новые обработчики существуют одновременно.

Влияющие версии: 2.3.0 - 2.4.0-dev.2

Исправленная версия: 2.4.0-dev.3

Содержание исправления: Очистка lifecycle._handlers в завершающем процессе Uninitializer (после всех событий), чтобы очистить старые обработчики.

Дата исправления: 2026/04/09

Серьезность: 🟡 Средний

Тип: Рантайм


[BUG-005] Event.is_friend_add/is_friend_delete detail_type не соответствует стандарту OB12

Проблема: Event.is_friend_add() проверяет detail_type == "friend_add", Event.is_friend_delete() проверяет detail_type == "friend_delete", но стандарт OneBot12 определяет значения detail_type как "friend_increase" и "friend_decrease". Несоответствие с notice.py в значениях, используемых декораторами on_friend_add/on_friend_remove, приводит к тому, что обработчики, зарегистрированные с помощью декораторов, при срабатывании события возвращают False для соответствующих методов is_friend_add()/is_friend_delete().

Причина: wrapper.py использует нестандартные имена, в то время как notice.py использует правильные имена стандарта OB12.

Влияющие версии: С момента реализации

Исправленная версия: 2.4.2-dev.1

Содержание исправления: Изменение значения, проверяемого is_friend_add(), с "friend_add" на "friend_increase", is_friend_delete() с "friend_delete" на "friend_decrease".

Дата исправления: 2026/04/13

Серьезность: 🟡 Средний

Тип: Система событий


[BUG-006] adapter.clear() не очищает _started_instances, приводя к неправильному состоянию после перезапуска

Проблема: Метод AdapterManager.clear() очищает _adapters, _adapter_info, обработчики и _bots, но пропускает _started_instances. Если адаптер запущен, а затем вызывается clear(), _started_instances сохраняет висящие ссылки, что приводит к неправильному состоянию после перезапуска.

Причина: При введении _started_instances в 2.4.0-dev.1, метод clear() не был обновлен для очистки этого множества.

Влияющие версии: 2.4.0-dev.1 - 2.4.2-dev.0

Исправленная версия: 2.4.2-dev.1

Содержание исправления: Добавление self._started_instances.clear() в метод clear().

Дата исправления: 2026/04/13

Серьезность: 🟡 Средний

Тип: Адаптер


[BUG-007] command.wait_reply() использует устаревший asyncio.get_event_loop()

Проблема: Метод CommandHandler.wait_reply() использует asyncio.get_event_loop() для создания будущего и получения временной метки, этот метод устарел в Python 3.10+ и в асинхронном контексте следует использовать asyncio.get_running_loop(). Несоответствие с методом wait_for() в wrapper.py, который использует get_running_loop().

Причина: При разработке использовался старый API, в то время как новый метод wait_for() использует правильный API, но старый код не был обновлен.

Влияющие версии: 2.3.0-dev.0

Исправленная версия: 2.4.2-dev.1

Содержание исправления: Замена asyncio.get_event_loop() на asyncio.get_running_loop() в command.py.

Дата исправления: 2026/04/13

Серьезность: 🟢 Легкий

Тип: Система событий


[BUG-008] Событие отключения Bot при shutdown повторно отправляется

Проблема: При вызове adapter.shutdown() для закрытия всех адаптеров, _update_bot_status() будет повторно отправлять событие отключения Bot в процессе закрытия, что приводит к многократной маркировке одного и того же набора Bot как отключенных и многократному срабатыванию событий жизненного цикла adapter.bot.offline.

Причина: Введённая в 2.4.0-dev.1 система отслеживания состояния Bot не установила флаг "в процессе закрытия" во время shutdown(), поэтому _update_bot_status() не может различить нормальное отключение и каскадное отключение во время закрытия.

Влияющие версии: 2.4.0-dev.1 - 2.4.2-dev.1

Исправленная версия: 2.4.2-dev.1

Содержание исправления: Добавление флага _is_being_shutdown в AdapterManager, установка его в True в начале shutdown() и сброс в конце; _update_bot_status() проверяет этот флаг и пропускает повторную отправку во время закрытия.

Дата исправления: 2026/04/21

Серьезность: 🟡 Средний

Тип: Адаптер


[BUG-009] Синхронный доступ к BaseModule без завершения инициализации

Проблема: При синхронном доступе к атрибуту лениво загруженного BaseModule, модуль использует loop.create_task() для асинхронной инициализации, но не ожидает завершения, что может привести к гонке состояний.

Причина: _ensure_initialized() для BaseModule использует loop.create_task(self._initialize()) и немедленно возвращает, не обеспечив завершения инициализации.

Влияющие версии: 2.4.0-dev.0 - 2.4.2-dev.1

Исправленная версия: 2.4.2-dev.2

Содержание исправления: В синхронном контексте инициализация BaseModule заменяется на asyncio.run(self._initialize()), чтобы убедиться, что инициализация завершена, прежде чем вернуться. Сохраняется прозрачность прокси-объекта, пользователь не должен осознавать различие между синхронным и асинхронным контекстом.

Дата исправления: 2026/04/21

Серьезность: 🟡 Средний

Тип: Загрузка системы


[BUG-010] Многопоточная запись конфигурации приводит к потере данных

Проблема: В многопоточной среде, когда несколько потоков одновременно вызывают config.setConfig(), операция _flush_config() чтение-изменение-запись не является атомарной, что может привести к потере части записей.

Причина: Хотя _flush_config() использует RLock, между чтением и записью файла нет защиты файловым блоком, и Timer _schedule_write может быть запущен несколько раз, вызывая перезапись.

Влияющие версии: 2.3.0 - 2.4.2-dev.1

Исправленная версия: 2.4.2-dev.2

Содержание исправления:

  1. Добавление механизма файлового блока (_file_lock), чтобы обеспечить атомарность операций с файлом
  2. Использование временного файла для записи, затем атомарное переименование (os.replace/os.rename)
  3. Улучшение логики отмены и перезапуска таймера _schedule_write

Дата исправления: 2026/04/21

Серьезность: 🔴 Серьезный

Тип: Система конфигурации


[BUG-011] Ctrl+C не может остановить программу в Windows

Проблема: При прямом запуске python main.py в Windows, нажатие Ctrl+C не останавливает программу. Программа запускается и выводит информацию о сервере маршрутизации, но Ctrl+C не реагирует, и программу можно остановить только через диспетчер задач. При запуске через epsdk run остановка возможна, но epsdk run запускается через подпроцессы.

Причина: ASGI-сервер Hypercorn в функции serve() зарегистрировал свой обработчик SIGINT, который перезаписал механизм обработки KeyboardInterrupt Python. При запуске Hypercorn в фоновом задании через asyncio.create_task() внутренний процесс остановки Hypercorn не вызывается, так как он ожидает режима worker_serve, что приводит к тому, что сигнал Ctrl+C поглощается Hypercorn, но не вызывает никаких действий очистки.

Влияющие версии: 2.3.6 - 2.4.2

Исправленная версия: 2.4.3-dev.0

Содержание исправления:

  1. Замена ASGI-сервера Hypercorn на Uvicorn (pyproject.toml зависимость)
  2. Использование uvicorn.Server._serve() для запуска сервера, обход контекста управления сигналами capture_signals()
  3. Использование server.should_exit = True для обеспечения корректной остановки, с отменой задания в фоновом режиме при таймауте
  4. Синхронное удаление механизма очистки подпроцессов и модуля runtime/cleanup.py (механизм очистки подпроцессов больше не нужен)

Дата исправления: 2026/04/28

Серьезность: 🔴 Серьезный

Тип: CLI / Рантайм


[BUG-012] После горячего перезапуска модули с обновленным кодом не применяются

Проблема: При выполнении sdk.restart() (мягкий перезапуск) новые версии модулей/адаптеров, установленные через epsdk install, не применяются, и логика по-прежнему работает по старой версии. Необходим полный перезапуск процесса для загрузки последнего кода.

Причина: _do_restart() при повторной инициализации вызывает entry_point.load(), но эта функция возвращает закэшированный объект модуля из sys.modules, а не загружает его заново с диска.

Влияющие версии: Ранние версии - 2.4.3-dev.1

Исправленная версия: 2.4.3-dev.1

Содержание исправления: При uninit() и перед init() очищается кэш загруженных модулей/адаптеров в sys.modules, чтобы entry_point.load() загружал последний код с диска. Добавлены вспомогательные методы _collect_top_level_modules() и _invalidate_module_cache(), которые используют top_level.txt или значение entry-point для вывода имен верхнего уровня модуля.

Дата исправления: 2026/05/03

Серьезность: 🔴 Серьезный

Тип: Загрузка системы / Рантайм


[BUG-013] Ошибка в логике сортировки стратегии загрузки модуля

Проблема: ModuleLoadStrategy предоставляет поле priority для декларации приоритета инициализации модуля, но реализация сортировки имеет ошибку, что приводит к тому, что модули не инициализируются в ожидаемом порядке приоритета, а по умолчанию сортируются по entry_points().

Причина: Ошибка в логике сортировки в реализации стратегии загрузки: initialize_modules() не использует priority для сортировки списка модулей.

Влияющие версии: 2.3.4 - 2.4.5-dev.2

Исправленная версия: 2.4.5-dev.3

Содержание исправления: Перед циклом initialize_modules() сортируем список модулей по убывающему priority. Модули с одинаковым priority сохраняют свой относительный порядок (стабильная сортировка).

Дата исправления: 2026/05/15

Серьезность: 🟡 Средний

Тип: Загрузка системы


[BUG-014] Возвращение None промежуточным ПО адаптера приводит к потере данных события

Проблема: adapter.emit() при выполнении цепочки промежуточного ПО OneBot12, если какой-либо промежуточный ПО возвращает None (например, забыл return data), последующие промежуточные ПО и все обработчики событий получают processed_data в виде None, что приводит к полной потере обработки события.

Причина: В реализации цепочки промежуточного ПО processed_data = await middleware(processed_data) не проверяется, возвращается ли None, и предыдущее значение заменяется.

Влияющие версии: unknown - 2.4.5-dev.3

Исправленная версия: 2.4.5-dev.4

Содержание исправления: При возврате None промежуточным ПО игнорируется это значение, сохраняется исходные данные, и выводится предупреждение уровня warning.

Дата исправления: 2026/05/15

Серьезность: 🔴 Серьезный

Тип: Адаптер / Система событий


[BUG-015] Путь к файлу конфигурации зависит от рабочей директории

Проблема: Путь к файлу конфигурации ConfigManager по умолчанию является относительным путем "config/config.toml", который зависит от os.getcwd() в процессе выполнения. Если рабочая директория изменяется во время выполнения (например, с помощью os.chdir()), операции чтения и записи конфигурационного файла будут указывать на неправильное местоположение, что приводит к потере конфигурации или чтению устаревших данных.

Причина: В __init__ хранится относительный путь, а не абсолютный путь, полученный при инициализации.

Влияющие версии: 2.3.7 - 2.4.5-dev.3

Исправленная версия: 2.4.5-dev.4

Содержание исправления: В ConfigManager.__init__() абсолютный путь преобразуется из относительного с помощью os.path.abspath().

Дата исправления: 2026/05/15

Серьезность: 🟡 Средний

Тип: Система конфигурации


[BUG-016] BaseStorage не различает значение None и отсутствие ключа

Проблема: BaseStorage.get_multi() / __getattr__() не могут различать "ключ отсутствует" и "значение ключа является None", пользователь, явно сохранив None, при последующем чтении получает результат, будто ключ отсутствует.

Причина: Логика получения значения использует value is None для проверки существования ключа, отсутствует отдельный маркер "отсутствия".

Влияющие версии: Ранние версии - 2.4.6-dev.6

Исправленная версия: 2.4.6-dev.6

Содержание исправления: Введение _SENTINEL для различения "ключ отсутствует" и "значение равно None", они больше не путаются.

Дата исправления: 2026/06/07

Серьезность: 🟡 Средний

Тип: Хранилище


[BUG-017] Флаг auto_accept маршрута WebSocket теряется после перезапуска службы

Проблема: После перезапуска службы (например, sdk.restart()) флаг auto_accept всех маршрутов WebSocket становится False, и клиенты, ожидавшие автоматического подключения, остаются в состоянии ожидания, не получая ответа, что приводит к зависанию WebSocket-соединения.

Причина: _restore_routes_from_records() при восстановлении маршрутов из записей жестко устанавливает auto_accept в False, не читая значение из оригинальной записи; при расширении кортежа метаданных маршрута до трёх элементов не было обновлено логики восстановления.

Влияющие версии: 2.3.8-dev.0 - 2.4.6-dev.6

Исправленная версия: 2.4.6-dev.6

Содержание исправления: Кортеж метаданных маршрута расширен до (handler, auth_handler, auto_accept), _restore_routes_from_records() читает реальное значение auto_accept из записи, а не жестко устанавливает False.

Дата исправления: 2026/06/07

Серьезность: 🔴 Серьезный

Тип: Маршрутизация


[BUG-018] Параллельные вызовы HTTP/WS-клиентов приводят к краху и утечке соединений

Проблема: HTTP и WebSocket-клиенты в Core/client.py имеют несколько стабильностных ошибок в многопоточной среде, которые могут привести к утечке соединений или краху процесса:

Причина: Первоначальная реализация клиента (2.4.6-dev.5) не имеет защиты от параллелизма и неправильной обработки иерархии исключений aiohttp и пользовательских исключений ErisPulse.

Влияющие версии: 2.4.6-dev.5 - 2.4.8

Исправленная версия: 2.4.8

Содержание исправления:

  1. Добавление _recv_lock для синхронизации всех вызовов receive() / receive_text() / receive_bytes()
  2. Добавление _session_lock для защиты создания сессии; _drain_sessions() изменен на асинхронный метод и действительно закрывает старые сессии
  3. Переупорядочение обработки исключений в request(): asyncio.TimeoutError → aiohttp.ClientConnectionError (запуск пересоздания сессии) → aiohttp.ClientError → ClientError (пропуск) → Exception
  4. Исправление обработки send_json() для параметра mode, передача заголовков по умолчанию в _get_ws_session(), устранение гонки при закрытии, HttpResponse.__aexit__ не вызывает release() дважды

Дата исправления: 2026/06/12

Серьезность: 🔴 Серьезный

Тип: Клиент


[BUG-019] При горячем перезагрузке адаптера возникает конфликт маршрутов, приводящий к сбою перезагрузки

Проблема: При горячем перезагрузке адаптера (например, из-за перезапуска или неудачной попытки) из-за того, что предыдущие маршруты (например, onebot11_default) не были очищены, возникает конфликт маршрутов WebSocket, приводящий к сбою перезагрузки. Необходим полный перезапуск процесса для восстановления.

Причина: AdapterManager.shutdown() только удаляет маршруты по unregister_all_by_namespace(platform), но адаптеры (например, OneBot11) регистрируют маршруты WebSocket с именем пространства onebot11_{account_name}, что приводит к тому, что очистка не выполняется; маршруты, оставшиеся после неудачной попытки, также не очищаются.

Влияющие версии: Ранние версии - 2.4.9

Исправленная версия: 2.4.9

Содержание исправления:

  1. При регистрации маршрута с помощью current_owner ContextVar автоматически отслеживается принадлежность owner → namespace
  2. Добавлен unregister_all_by_owner(owner), чтобы при остановке/перезапуске удалять маршруты по принадлежности, охватывая мелкозернистые имена пространства
  3. Добавлен stop_adapter(platform) (остановка = очистка), привязывающий остановку адаптера к очистке связанных ресурсов
  4. Добавлен API фреймворка adapter.restart(platform), которое должно вызываться сторонними модулями вместо прямого обращения к экземпляру адаптера

Дата исправления: 2026/06/12

Серьезность: 🔴 Серьезный

Тип: Адаптер / Маршрутизация


[BUG-020] В режиме non-reload ep run <script> не находит подпакеты в каталоге скрипта

Проблема: При использовании ep r .\main.py в режиме non-reload, если скрипт имеет относительные импорты (например, from qg import ...), возникает ошибка No module named 'qg'. Режим --reload работает нормально.

Причина: В режиме non-reload используется runpy.run_path() для выполнения скрипта, который не добавляет каталог скрипта в sys.path. Режим --reload запускается через subprocess.Popen, и подпроцесс наследует текущую рабочую директорию, поэтому sys.path[0] равен каталогу скрипта, и всё работает.

Влияющие версии: 2.5.0 - 2.5.2-dev.0

Исправленная версия: 2.5.2-dev.0

Содержание исправления: Перед вызовом runpy.run_path() вручную добавляется каталог скрипта в sys.path[0].

Дата исправления: 2026/06/27

Серьезность: 🟡 Средний

Тип: CLI


[BUG-021] Конструктор SQL-запроса отвергает допустимые выражения с подстановочными знаками и списками

Проблема: _validate_identifier() в SQLiteQueryBuilder применяется ко всем столбцам SELECT, используя строгий белый список регулярного выражения ^[a-zA-Z_][a-zA-Z0-9_]*$, что приводит к тому, что допустимые SQL-выражения ошибочно считаются небезопасными именами столбцов:

В частности, Select("*") используется Cron и другими модулями, что приводит к сбою загрузки модуля.

Причина: В версии 2.4.6 усилена защита от SQL-инъекций, и была введена _validate_identifier() с белым списком. Эта проверка применяется ко всем столбцам, но не различает чтение (SELECT/ORDER BY) и запись (INSERT/UPDATE). Столбцы SELECT могут содержать сложные SQL-выражения и не должны подвергаться строгой проверке идентификаторов.

Влияющие версии: 2.4.6 - 2.5.2-dev.1

Исправленная версия: 2.5.2-dev.2

Содержание исправления: Замена проверки идентификаторов для SELECT/ORDER BY с белого списка на черный список:

  1. Новый _validate_select_column() функция, проверяющая только опасные символы SQL (; ' " -- /* */ \x00 символы перевода строки)
  2. Разрешение любых допустимых SQL-выражений для столбцов (например, *, table.*, table.column, COUNT(*), col AS alias)
  3. Для INSERT/UPDATE столбцы по-прежнему подвергаются строгой проверке идентификаторов (только простые идентификаторы)

Дата исправления: 2026/06/29

Серьезность: 🔴 Серьезный

Тип: Хранилище


[BUG-022] Решение _resolve_account() не заполняет _accounts_data

Проблема: После рефакторинга системы конфигурации в 2.5.2, адаптеры, объявляющие AccountConfigClass, при вызове wait_reply, reply и других методов, требующих отправки сообщений, вызывают ошибку ValueError("не объявлен AccountConfigClass, невозможно решить аккаунт"). Даже если адаптер правильно настроен с информацией об аккаунтах, решение аккаунта по-прежнему терпит неудачу.

Причина: В 2.5.2-dev.5 _load_accounts() (отвечающий за чтение конфигурации + проверку + заполнение _accounts_data) был рефакторинг в _ensure_accounts_exist() (только генерация шаблона конфигурации), но _resolve_account() по-прежнему проверяет self._accounts_data is None. Поскольку _ensure_accounts_exist() больше не заполняет _accounts_data, это свойство всегда равно None, что приводит к тому, что _resolve_account() возвращает (None, None) заранее, и последующие вызовы _resolve_account получают None.

Цепочка корневых причин:

_load_accounts() удалена
  → __init__ не заполняет _accounts_data
    → _accounts_data всегда None
      → _resolve_account() проверяет _accounts_data is None → return (None, None)
        → методы, использующие _resolve_account (например, call_api), получают None
          → вызывается ошибка

Влияющие версии: 2.5.2-dev.5 - 2.5.2

Исправленная версия: 2.5.3

Содержание исправления: В BaseAdapter.__init__ после _ensure_accounts_exist() восстанавливается заполнение _accounts_data:

if self.AccountConfigClass is not None:
    self._ensure_accounts_exist()
    self._accounts_data = self.accounts  # восстановление заполнения, данные берутся из актуального свойства accounts

Логика _resolve_account() остается неизменной, полностью совместима с предыдущими версиями:

Дата исправления: 2026/07/07

Серьезность: 🔴 Серьезный

Тип: Адаптер / Система конфигурации


[BUG-023] Кэш адаптера не обновляется после изменения конфигурации аккаунта

Проблема: После изменения конфигурации аккаунта адаптера через панель управления (например, заполнение токена) адаптер по-прежнему использует устаревший кэш, и вызов методов, связанных с отправкой сообщений, вызывает ошибку не найден доступный аккаунт (account_id=default). Необходим перезапуск процесса, чтобы новая конфигурация вступила в силу.

Причина: _accounts_data читается из конфигурации только один раз при инициализации BaseAdapter, и не обновляется после. AdapterManager._run_adapter() и restart() не перечитывают конфигурацию аккаунта перед вызовом adapter.start(), что приводит к тому, что кэш и конфигурация расходятся.

Влияющие версии: 2.4.6 - 2.5.4

Исправленная версия: 2.5.4

Содержание исправления: При вызове adapter.start() перед AdapterManager._run_adapter() и restart() обновляется adapter._accounts_data = adapter.accounts, чтобы использовать новую конфигурацию.

Дата исправления: 2026/07/09

Серьезность: 🔴 Серьезный

Тип: Адаптер / Система конфигурации


[BUG-024] Запись больших числовых ID в storage приводит к OOM Kill

Проблема: Вызов storage.set() для записи вложенного ключа с большим числовым полем (например, QQ-группа 871684833) приводит к тому, что процесс убивается контейнером OOM Kill (код выхода -9), и служба полностью падает.

Причина: Рекурсивная реализация _set_nested_value при вложенных ключах с числовыми полями ошибочно интерпретирует их как индекс массива, вызывая current.extend([None] * (index - len(current) + 1)), что пытается выделить миллион элементов списка, мгновенно исчерпывая память.

Цепочка корневых причин:

Ключ содержит числовое поле (например, ID группы 871684833)
  → isdigit() ошибочно интерпретирует как индекс массива
    → extend([None] * (871684833 - len(current) + 1))
      → попытка выделить миллион элементов
        → исчерпание памяти → контейнер OOM Kill (код выхода -9)

Влияющие версии: 2.5.1 - 2.5.5

Исправленная версия: 2.5.5

Содержание исправления:

  1. При создании промежуточных уровней всегда используйте словарь, а не угадывайте тип контейнера по следующему числовому сегменту
  2. При установке конечного значения только если контейнер уже является списком и индекс меньше STORAGE_MAX_LIST_INDEX (10000) обрабатывайте индекс, иначе безопасно пропустите
  3. Преобразуйте рекурсивную реализацию в итеративную, устраняя потенциальную бесконечную рекурсию
  4. Добавьте константу STORAGE_MAX_LIST_INDEX в Core/constants.py для централизованного управления верхним пределом индексов

Дата исправления: 2026/07/10

Шаги воспроизведения:

# Запись вложенного ключа с большим числовым полем (например, ID группы QQ) вызывает OOM
await sdk.storage.aset("groups.871684833.name", "某群")
# → память процесса мгновенно возрастает, и он убивается OOM Kill

Регрессионное тестирование: tests/unit/test_unit_storage.py добавлены 4 теста на регрессию

Серьезность: 🔴 Серьезный

Тип: Хранилище


[BUG-025] Callback on_config_update не вызывается ядром

Проблема: on_config_update(old, new) callback определен в базовых классах (BaseModule / BaseAdapter), но ядро не связывает эти события с изменениями конфигурации. Фактически: при редактировании через панель управления изменения применяются, но при ручном редактировании config.toml или вызове setConfig() callback on_config_update не срабатывает.

Причина: ConfigManager при изменении конфигурации излучает события config.set / config.updated, но отсутствует логика подписки на эти события для вызова метода on_config_update соответствующих компонентов.

Цепочка корневых причин:

Ядро не подписывается на config.set / config.updated
  → события конфигурации не пересылаются
    → on_config_update не вызывается
      → ручное редактирование файла / код setConfig не вызывает callback горячего обновления

Влияющие версии: Все версии

Исправленная версия: 2.6.2

Содержание исправления: ModuleManager / AdapterManager регистрируют подписки на события config.set (путь перекрытия setConfig()) и config.updated (путь перекрытия ручного редактирования файла), соответствующие префиксы конфигурации сопоставляются, и вызывается соответствующий метод on_config_update, передавая безопасные объекты конфигурации. Также исправлено, что _flush_config() после записи файла не синхронизировал _config_mtime, что приводило к тому, что изменения, внесенные самим фреймворком, ошибочно воспринимались как внешние и запускали событие config.updated повторно.

Совместимость: Горячее обновление конфигурации теперь управляется ядром. Ранее логика, запускаемая через панель управления, удалена, и при обновлении панели управления необходимо синхронизировать, иначе будет дублирование (ядро и панель вызывают одинаково). Подпись и семантика метода on_config_update не изменились, подклассы не нуждаются в изменениях.

Дата исправления: 2026/07/23

Серьезность: 🟡 Средний

Тип: Система конфигурации


[BUG-026] Ошибка определения цели ответа для событий notice/request

Проблема: При вызове event.reply() в событии уведомления (например, group_member_increase) сообщение отправляется в личную переписку пользователя, который вызвал событие, а не в группу, в которой произошло событие. Аналогично для событий уведомления о друзьях.

Причина: infer_receive_type() возвращает detail_type как тип сессии. Для событий message это правильно (значения detail_type private/group — типы сессии), но для событий notice/request detail_type — это семантические подтипы (например, group_member_increase, friend_increase), а не типы сессии. В последующих convert_to_send_type() и get_id_field() в картах сопоставления отсутствует это значение, и по умолчанию возвращается "user" / "user_id", что приводит к неправильному определению цели ответа.

Цепочка корневых причин:

detail_type уведомления = "group_member_increase"
  → infer_receive_type() возвращает "group_member_increase"
    → convert_to_send_type("group_member_increase") не в карте → возвращается "user"
    → get_id_field("group_member_increase") не в карте → возвращается "user_id"
      → target_id = event["user_id"]  ← новый участник в личной переписке (а не в группе)

Влияющие версии: Все версии

Исправленная версия: 2.7.0-dev.3

Содержание исправления: infer_receive_type() добавляет проверку — detail_type возвращается только если он является известным типом сессии (стандартный или пользовательский тип); в противном случае по полю идентификатора (group_id / channel_id / user_id и т.д.) определяется правильный тип сессии.

Регрессионное тестирование: tests/unit/test_unit_session_type.py → TestNoticeRequestTypeInference (10 тестов)

Дата исправления: 2026/07/29

Серьезность: 🟢 Легкий

Тип: Система событий


[BUG-027] Очистка ограничения скорости маршрута использует фиксированный окно, что делает правило ограничения скорости на длинное окно неэффективным

Проблема: При настройке ограничения скорости маршрута на длинное окно (например, 100/hour, {"requests": 100, "window": 3600}) ограничение скорости оказывается неэффективным — фактически это примерно 100/minute (в час можно пропустить около 6000 запросов), и ограничение на уровне часа не оказывает никакого влияния.

Причина: _apply_rate_limit анализирует фактическое окно для каждого маршрута (максимум 3600 секунд), проверка per-request также использует это окно; однако фоновая задача очистки _cleanup_expired_rate_limits использует фиксированную константу DEFAULT_RATE_LIMIT_WINDOW_SECS (60 секунд) в качестве общего порога для всех маршрутов. Таким образом, временные метки, более старые, чем 60 секунд, удаляются, и в окне в час никогда не накапливается почти 100 записей, ограничение значительно снижается.

Цепочка корневых причин:

_apply_rate_limit анализирует window=3600 (100/hour)
  → per-request проверка использует это окно (правильно)
  → но _cleanup_expired_rate_limits использует фиксированное max_window=60s для очистки
    → временные метки до 60 секунд удаляются
      → в окне в час остаются только последние 1 минуты записей
        → 100/hour фактически снижается до ~100/minute (разрешение примерно в 60 раз)

Влияющие версии: 2.6.0-dev.0 - 2.7.0-dev.4

Исправленная версия: 2.7.0-dev.5

Содержание исправления: Добавлено _rate_limit_windows: dict[str, int] для записи фактического окна для каждого маршрута; при создании записи в _apply_rate_limit записывается окно; _cleanup_expired_rate_limits теперь очищает по окну маршрута (если нет, то по умолчанию); при удалении записей и stop() синхронно обновляются два словаря.

Дата исправления: 2026/07/31

Регрессионное тестирование: tests/unit/test_unit_router.py → TestRateLimit::test_cleanup_respects_per_route_window

Серьезность: 🔴 Серьезный

Тип: Маршрутизация


[BUG-029] Задача наблюдения за конфигурацией транслирует неполный TOML и тихо поглощает исключения

Проблема: Когда пользователь редактирует config.toml и сохраняет его на полпути (создавая временный файл с синтаксической ошибкой), фоновый поток наблюдения обнаруживает изменение mtime, перезагружает конфигурацию, но при неудачной загрузке по-прежнему излучает событие config.updated с пустой конфигурацией {}, что приводит к тому, что адаптеры/модули on_config_update получают пустую конфигурацию и ошибочно возвращаются к значениям по умолчанию. Кроме того, поток наблюдения тихо поглощает все исключения, что затрудняет диагностику сбоев наблюдателя.

Причина: Два недостатка:

  1. _load_config при синтаксической ошибке/ошибке доступа перезаписывает self._cache на {}, но фоновый цикл _watch_loop и _check_cache_validity всегда излучают _emit_config_updated(new_config={}), отправляя "пустой кэш, полученный из неудачной загрузки" как реальное изменение.
  2. _watch_loop с except Exception: pass не записывает никаких журналов.

Цепочка корневых причин:

Пользователь сохраняет на полпути → синтаксическая ошибка TOML
  → _load_config() перезаписывает _cache = {}
    → _watch_loop безусловно _emit_config_updated(new_config={})
      → адаптер/модуль on_config_update получает пустую конфигурацию
        → ошибочно полагает, что все параметры конфигурации были очищены, и возвращается к значениям по умолчанию

Влияющие версии: 2.6.2-dev.1 - 2.7.0-dev.4

Исправленная версия: 2.7.0-dev.5

Содержание исправления:

  1. _load_config теперь возвращает bool; при синтаксической ошибке/ошибке доступа и других ошибках self._cache не перезаписывается на {}, а сохраняется предыдущий действительный кэш (не записывается, только записываются диагностические журналы и возвращается False)
  2. _watch_loop и _check_cache_validity излучают config.updated только при возвращении True из _load_config()
  3. _watch_loop с except Exception теперь записывает на уровне warning (добавлен i18n ключ core.config.watcher_error, пять языков синхронизированы)

Дата исправления: 2026/07/31

Регрессионное тестирование: tests/unit/test_unit_config.py → test_malformed_toml_preserves_last_valid_cache, test_permission_denied_logs_clear_message (обновлено для проверки сохранения кэша и возвращения False)

Серьезность: 🟡 Средний

Тип: Система конфигурации


[BUG-030] Гонка наблюдателя за конфигурацией приводит к потере данных при задержанной записи setConfig

Проблема: При использовании config.setConfig(key, value) (по умолчанию immediate=False) после сохранения конфигурации в файл, конфигурация модуля не записывается в config.toml, а остается в следующем перезапуске. Запись в файл конфигурации теряется, а конфигурация, сгенерированная при запуске, сохраняется.

Причина: Два совпадающих недостатка:

  1. Логическая ошибка: _watch_loop безусловно _dirty_keys.clear() при _check_file_change() возвращает True. Но _check_file_change() использует только != для сравнения mtime, и запись конфигурации config.setConfig() также изменяет mtime — хотя _flush_config обновляет _config_mtime после записи, поток наблюдения может обнаружить разницу в mtime между записью файла и присвоением _config_mtime (а также на грубом масштабе файловой системы), и ошибочно интерпретировать это как "внешнее изменение", очищая все ключи для записи.
  2. Ошибки потоков: _watch_loop не захватывает _lock при операциях с _write_timer/_dirty_keys, что приводит к гонке данных с setConfig (захватывает _dirty_keys), _schedule_write (захватывает _write_timer).

Цепочка корневых причин:

МодульA setConfig(immediate=True) → flush записывает файл, mtime меняется
  → Пользовательский модуль setConfig(immediate=False) → входит в _dirty_keys, через 5s записывает файл
    → Наблюдательный цикл, _check_file_change замечает разницу в mtime, вызванную предыдущей записью
      → _dirty_keys.clear() → ключи для записи пользователя удаляются тихо
        → при перезапуске конфигурация отсутствует

Влияющие версии: 2.6.0 - 2.7.0

Исправленная версия: 2.7.1

Содержание исправления:

  1. Добавлено _last_self_write_mtime поле, _flush_config записывает его после записи; _check_file_change при сравнении mtime сначала сравнивает это значение, если совпадает, возвращает False
  2. _watch_loop весь захватывает _lock; при реальном внешнем изменении сохраняет _dirty_keys (слияние семантики), при следующей записи объединяет с внешними данными (ключи для записи имеют приоритет), а не clear()
  3. getConfig/_check_cache_validity пути не затрагиваются (перезагрузка не очищает ключи для записи)

Дата исправления: 2026/08/06

Регрессионное тестирование: tests/unit/test_unit_config.py → test_self_write_not_detected_as_external, test_external_change_preserves_dirty_keys, test_flush_merges_dirty_with_external

Серьезность: 🔴 Серьезный

Тип: Система конфигурации


[BUG-032] Задержанная запись конфигурации приводит к чтению "сразу после записи" старых значений

Проблема: После вызова config.setConfig() (по умолчанию immediate=False задержка около 5 секунд перед записью) для записи точечного ключа, немедленное чтение его родительского/предка (например, set_erispulse_section("scope.actions.MyModule", {...}) затем вызов get_erispulse_config()) возвращает старое значение, записанное в этом сценарии "чтение-запись-чтение", где значение ключа-потомка "исчезает", пока оно не будет записано. Сценарии горячего обновления конфигурации сектора "чтение-запись-чтение" затронуты (тестовый плагин 2.8.0 /t_section примеры выявили).

Причина: setConfig сохраняет точечный ключ в очередь для записи _dirty_keys в виде "плоской" структуры, только точечные запросы getConfig попадают в очередь для записи; деревообразные запросы (например, getConfig("ErisPulse.scope")) смотрят только в кэш дерева, не добавляя значения для записи — во время задержки записи (когда _flush_config объединяет значения для записи в кэш и очищает очередь) возникает разрыв между чтением и записью.

Влияющие версии: 2.6.0 - 2.8.0-dev.1

Исправленная версия: 2.8.0-dev.1

Содержание исправления: getConfig вводит семантику добавления значений для записи — ① точечное попадание в очередь для записи возвращает значение напрямую (старое поведение не изменилось); ② если ключ для записи является префиксом запроса, берется самый длинный префикс, в его значении поддерево разрешается оставшимися путями; ③ если ключ для записи является потомком запроса, строится поддерево для объединения ( _dirty_overlay ) с поддеревом кэша глубоким объединением ( _deep_merge , приоритет override, не изменяя исходный объект кэша). Без ключей для записи используется быстрый путь, без дополнительных накладных расходов.

Дата исправления: 2026/09/04

Регрессионное тестирование: tests/unit/test_unit_config.py → test_get_config_overlays_dirty_descendant, test_get_config_overlay_merges_with_cache_siblings, test_get_config_dirty_ancestor_query, test_get_config_dirty_exact_key_still_wins

Серьезность: 🟡 Средний

Тип: Система конфигурации


[BUG-033] Команда wait_reply блокирует ответ, уничтожаемый высокоприоритетным обработчиком

Проблема: Модуль вызывает wait_reply(), ожидая ответа пользователя, но если это сообщение было признано обработчиком с высоким приоритетом (mark_processed()), диспетчер команд в начале видит флаг _processed и немедленно возвращается, так что логика поиска ответа, находящаяся в конце _handle_message, никогда не выполняется — ждущая сторона не получает ответа и ждет таймаута, возвращая None. Типичная сцена: использование обработчиков сообщений с высоким приоритетом (запись/аудит/блокировка), роботы с диалоговой интеракцией случайно теряют.

Причина: Логика поиска ответа _check_pending_reply находится в конце _handle_message (после команды, если не найдена), а проверка _processed находится до нее — порядок проверки признания и логики поиска ответа нарушен. Диалоговое ожидание — это механизм продолжения сессии на уровне фреймворка, а не конкурентный обработчик, он не должен зависеть от признания других обработчиков.

Влияющие версии: 2.2.0-dev.0 - 2.8.0-dev.1

Исправленная версия: 2.8.0-dev.2

Содержание исправления: Логика поиска ответа перемещается в начало _handle_message (до проверки _processed, только для событий сообщения): сначала попытка завершить ожидающий диалог, если найдено, событие помечается как обработанное, и ниже проверки коротко завершаются; если не найдено, продолжается обычный процесс сопоставления команд. Также цепочка проверки делегирована новому менеджеру сессий взаимодействия ( Core/Event/interaction.py ), получившему возможность отмены и проверки прав доступа.

Дата исправления: 2026/09/08

Регрессионное тестирование: tests/unit/test_unit_interaction.py ( TestRegisterResolve попадание/не попадание/флаг признания), tests/unit/test_unit_event.py (весь цепной вызов wait_reply)

Серьезность: 🟡 Средний

Тип: Система событий / Командная система


[BUG-034] Запись в память с persist=False удаляется при последующей записи конфигурации

Проблема: scope.set_module(..., persist=False) запись во время выполнения изменяет только _data в памяти; но scope подписывается на события config.set / config.updated, любое изменение конфигурации (например, другой модуль записывает свои значения по умолчанию) вызывает перестроение дерева конфигурации, и все предыдущие записи во время выполнения теряются (восстановление к значению по умолчанию), без каких-либо записей в журнале. Зависимые от записи во время выполнении сценарии (панель управления "только во время выполнения", отключение модуля во время выполнения) ведут себя как до перезапуска.

Причина: Корневая цепочка: scope.set(..., persist=False) записывает только в _data ( Core/scope.py ) → любое setConfig излучает событие config.set → _on_config_updated безусловно _load_config() → _apply_tree() полностью заменяет _data = {...} → не записанные в файле значения по умолчанию теряются.

Влияющие версии: 2.8.0-dev.1 - 2.8.0-dev.2 Исправленная версия: 2.8.0-dev.2 Содержание исправления: Введено покрытие для записи во время выполнения _runtime_overrides (с тенью для удаления): persist=False записи записываются в покрытие, _apply_tree() перестраивает постоянное дерево, затем воспроизводит запись во время выполнения; persist=True запись/удаление очищает соответствующие записи покрытия (приоритет пользовательских значений по умолчанию); config.set по событию key точечно фильтрует, config.updated сравнивает новое и старое значение scope, перестраивает только если scope действительно изменился (включая избежание сброса несвязанных записей при очистке кэша LRU); добавлен unregister_by_owner() для очистки по владельцу при отмене модуля. Core.Event.overrides с persist=False также исправлены с архитектурой покрытия. Дата исправления: 2026/09/09

Шаги воспроизведения: ① scope.set_module("testplat", blocked=["TestB"], persist=False) → оценка False; ② любой модуль выполняет config.setConfig("HelpModule", {...}) → излучает событие scope перестроить; ③ scope.is_allowed("testplat", None, "TestB") возвращает True (ожидаемо все еще False). Связано: Issue #432 Регрессионное тестирование: tests/unit/test_unit_scope.py::TestRuntimeOverrideSurvival (не затрагивает запись, перестраивает и воспроизводит, тень удаления, очищает при persist=True, точечное обновление, очистка по владельцу) Серьезность: 🟡 Средний Тип: Система конфигурации / Рантайм


[BUG-035] Веб-интерфейс отображает опции select и словарные поля как [object Object]

Проблема: В веб-интерфейсе панели конфигурации опции выпадающего списка отображаются как [object Object] (например, динамически генерируемые варианты стиля цвета); необъявленные поля словаря (например, stalker_mode, knowledge_base и другие вложенные секции конфигурации) отображаются в текстовом поле как [object Object], что затрудняет просмотр и редактирование.

Причина: Две отдельные ошибки: ① парсер i18n _resolve_i18n_text восстанавливает только словарь с ключом i18n, метки опций с единственным default (без ключа i18n для динамического текста) передаются как есть, фронтенд esc(label) преобразует в строку, получая [object Object]; ② ветвь рендеринга панели Dashboard только для массива делает JSON textarea, словарь/таблица отображается в текстовом поле, преобразуясь в String(); кроме того, если модуль объявляет _schema_meta как обычное поле dataclass (без аннотации ClassVar), оно рассматривается как поле конфигурации, усугубляя путаницу.

Влияющие версии: 2.7.0 - 2.8.0-dev.2 Исправленная версия: 2.8.0-dev.2 Содержание исправления: ① _resolve_i18n_text поддерживает словарь с единственным default для преобразования в текст; ② пять мест — схема/шаблон/значение по умолчанию/заполнение/проверка — исключают поля с подчеркиванием в начале (ошибочное объявление безвредно); ③ панель Dashboard для опций select обрабатывает метку объекта (приоритет default), поля словаря/таблицы отображаются как JSON textarea (сохраняется путь сохранения как tp=object JSON.parse обратно, полный цикл чтения/записи); ④ если модуль объявляет _schema_meta как обычное поле dataclass (без ClassVar аннотации), оно рассматривается как поле конфигурации, усугубляя путаницу. Дата исправления: 2026/09/09

Регрессионное тестирование: tests/unit/test_unit_config.py::TestResolveI18nDefaultOnlyDict, TestSchemaUnderscoreFieldExclusion

Серьезность: 🟡 Средний Тип: Система конфигурации


[BUG-036] Запись конфигурации в многоконтекстном каталоге конфигурации с частыми сбоями (ENOENT)

Проблема: В сценариях развертывания Docker (несколько контейнеров, монтирующих один каталог конфигурации хоста), логи периодически показывают две последовательные ошибки Failed to write configuration file ... [Errno 2] No such file or directory: '...config.toml.tmp' -> '...config.toml'. Эта запись конфигурации теряется (старая конфигурация сохраняется полностью, без наблюдения потери конфигурации), функциональность не затронута, но предупреждения повторяются и мешают отладке, а конфигурация, ожидающая записи, должна ждать следующей записи, чтобы записаться.

Причина: Корневая цепочка: _flush_config / setConfigTemplate используют фиксированное имя временного файла config.toml.tmp для хранения новой информации, write() без fsync сразу rename(); два экземпляра ErisPulse, совместно использующие один каталог конфигурации, могут open("w") обрезать файл, который другой экземпляр пытается писать, приводя к тому, что первый rename получает файл, который уже был взят или обрезан другим, вызывая ошибку ENOENT (как показано в логах пользователя). В отчетах пользователей только сообщения об ошибке, но не сообщения об остановке, наблюдаются; если сценарий перекрестного доступа более экстремальный, rename может выдать пустой или частично записанный config.toml (не проявился в реальных средах). В одиночном экземпляре ext4 отложенная запись также имеет окно сбоя "атомичное rename до записи данных" (SIGKILL / отключение питания). _file_lock является процессным threading.RLock, не ограничивает перекрестный процесс/контейнер.

Влияющие версии: 2.2.0-dev.0 - 2.8.0

Исправленная версия: 2.8.1

Содержание исправления: Все операции записи конфигурации объединены в _atomic_write_text(): с помощью mkstemp в одном каталоге создается уникальный временный файл (устранение конфликта фиксированных имен, в многоконтекстном режиме сводится к последнему победителю, без ENOENT); после записи flush + fsync для принудительного сохранения данных (устранение окна "атомичного rename, но данные не записаны"); os.replace атомически заменяет целевой файл (POSIX/Windows атомичны, на диске всегда либо целая старая версия, либо целая новая версия); на POSIX дополнительно fsync каталога конфигурации. Три точки записи: _flush_config, setConfigTemplate, перенос конфигурации в корень каталога полностью переключены; логика очистки ошибок переработана с уникальным именем временного файла. Добавлено обнаружение многоконтекстного режима: при запуске advisory lock (POSIX flock / Windows msvcrt.locking) для уникального файла блокировки .erispulse_config.lock, если он занят, выводится предупреждение на i18n (не блокирует запуск), блокировка автоматически освобождается ОС при выходе из процесса, без "призрачных" блокировок.

Дата исправления: 2026/09/13

Шаги воспроизведения: ① два контейнера, монтирующие один каталог хоста config/, запускаются одновременно; ② один из экземпляров вызывает запись конфигурации (например, модуль регистрирует конфигурацию по умолчанию); ③ в логах появляются предупреждения о сбое записи с ошибкой ENOENT, эта запись конфигурации теряется (старая конфигурация сохраняется).

Связано: Отчеты пользователей (1Panel контейнер ×2)

Регрессионное тестирование: tests/unit/test_unit_config_atomic_write.py (полная запись содержимого / отсутствие временных файлов / запись сбоя сохраняет старый файл / одновременная запись двух экземпляров всегда приводит к законному файлу / создание файла блокировки / предупреждение при многоконтекстном режиме / атомичная запись при переносе)

Серьезность: 🟢 Легкий

Тип: Система конфигурации


[BUG-037] Команды с пробелами не могут быть запущены

Проблема: Регистрация подкоманды с пробелом (например, @command("admin add")) после регистрации нормально отображается в списке помощи, но при отправке /admin add бот не отвечает — ввод сопоставляется с командой admin и параметрами ["add"]; если команда admin также не зарегистрирована, ответа нет. Только при использовании точечного имени (например, admin.reload, все как один токен) можно избежать.

Причина: Корневая цепочка: CommandHandler.__call__ сохраняет любое имя команды (с пробелами) в плоский self.commands словарь → этап сопоставления _try_execute_command ищет только первый токен сообщения (cmd_name = parts[0]) → команды с несколькими токенами никогда не находятся в словаре ключей. Логика регистрации и сопоставления для имени команды не согласована, и нет предупреждения на этапе регистрации (тихая ошибка).

Влияющие версии: С момента введения системы команд - 2.8.0

Исправленная версия: 2.8.1

Содержание исправления: Этап сопоставления изменился на длиннейшее соответствие префикса: от самого длинного кандидата (например, " ".join(parts[:n]), n ограничено максимальным количеством токенов в именах команд или именах псевдонимов, кэшируемое _max_name_tokens) постепенно до более коротких, при попадании используется оставшиеся токены в качестве параметров, последующие области действия/ACL/перезапись/master/цепочка прав доступа применяются ко всему имени команды естественным образом. Сопутствующая семантика: при наличии родительской и дочерней команды, если дочерняя команда не зарегистрирована, вход попадает в родительскую команду (историческое поведение не меняется); если дочерняя команда не объявляет permission, она наследует ближайшую родительскую команду с объявленным permission (защита родительской команды защищает все ее дочерние команды); при регистрации только одной команды с одним токеном, первое попадание происходит, расходы на сопоставление не меняются. unregister / unregister_by_owner / полная очистка также поддерживают кэширование количества токенов.

Дата исправления: 2026/09/13

Шаги воспроизведения: ① в модуле @command("admin add") регистрируется; ② отправляется /admin add x; ③ до исправления нет ответа (или он принимается как параметры для /admin), после исправления admin add срабатывает, и get_command_args() возвращает ["x"].

Регрессионное тестирование: tests/unit/test_unit_command_subcommand.py (длиннейшее соответствие префикса / только дочерняя команда срабатывает / тройная вложенность / два режима чувствительности к регистру / псевдонимы с одним и несколькими токенами / событие нагрузки полного имени / хуки жизненного цикла полного имени / наследование прав доступа шесть примеров / ACL glob полного имени / мастер / отмена возврата и пересчет кэша)

Серьезность: 🟡 Средний

Тип: Система событий


[BUG-038] Запись во время выполнения persist=False записывается и сохраняется в файле, модуль после удаления восстанавливается из файла

Проблема: Запись во время выполнения с scope.set(path, value, persist=False) (документация заявляет, что не сохраняется в файле, исчезает после перезапуска процесса, очищается при удалении модуля) будет записана в файл конфигурации вместе с любой другой записью persist=True (значение по умолчанию, например, модуль set_module), и после удаления модуля, при перезагрузке конфигурации, эта запись будет восстановлена из файла и продолжит работать, оставаясь после перезапуска процесса — противоречие "запись во время выполнения не сохраняется" в документации, и трудно обнаружить.

Причина: Корневая цепочка: ScopeManager.set() сначала записывает значение в дерево конфигурации _data (запись во время выполнения также записывается напрямую в _data); для сохранения в файле хранения persist=True копирует полное дерево _data и передает его в update_erispulse_config для дифференциальной записи; delete(persist=True) аналогично: передает ссылку на узел _data в очередь задержки записи, и при задержке записи этот узел остается в очереди, пока не будет очищен.

Влияющие версии: 2.8.0-dev.2 - 2.9.0-dev.0

Исправленная версия: 2.9.0-dev.2

Содержание исправления: Введено дерево сохранения persisted_tree (обновленное дерево конфигурации при загрузке с диска и проверке) как зеркало истинного состояния на диске: set(persist=True) применяет только изменения, внесенные в это дерево, и передает их в update_erispulse_config для дифференциальной записи, запись во время выполнения никогда не попадает в сохраняемое содержимое; "чтение сразу после записи" теперь использует копию окончательного состояния памяти, а не перестраивает дерево конфигурации, чтобы не повредить дерево сохранения. delete(persist=True) аналогично: передает копию поддерева persisted_tree в слой сохранения; set_action заменяет все, удаляя старое дерево и вставляя новое, старые ключи не остаются в сохраняемом содержимом.

Дата исправления: 2026/09/27

Шаги воспроизведения: ① модуль вызывает scope.set("bots.p.debug_mode", {"blocked": ["X"]}, persist=False); ② вызывает любую операцию записи persist=True (например, другой модуль вызывает set_module); ③ открывает config/config.toml, видит, что debug_mode была записана (до исправления); ④ после удаления модуля (запись во время выполнения была отменена) и перезагрузки конфигурации, scope.get("bots.p.debug_mode") по-прежнему возвращает записанное значение.

Регрессионное тестирование: tests/unit/test_unit_scope.py::TestPersistBaseline (запись во время выполнения не записывается в файл при другой операции записи / после удаления модуля перезагрузка конфигурации не восстанавливает / удаление передает копию поддерева persisted_tree без записей во время выполнения / замена set_action не оставляет остаточных ключей / кэш размера конфигурации работает)

Серьезность: 🔴 Серьезный

Тип: Система конфигурации


[BUG-039] При совместном использовании полной записи и точечной записи возникают несогласованности чтения/записи (точечная перезапись теряется)

Проблема: В окне задержки записи, когда одновременно используются полная запись (например, setConfig("Mod", {...}), как BaseModule.cfg записывается обратно) и точечная запись (например, setConfig("Mod.key", v)), есть два варианта: вариант A (чтение пути) — getConfig("Mod") получает старую частичную запись в кэше, не видя более поздней точечной перезаписи (точечное чтение работает нормально); вариант B (запись путь, более серьезный) — если точечная запись происходит до полной записи (например, тестовый инструмент перезаписывает значение → модуль self.cfg = ... записывает обратно), при записи flush по порядку вставки применяются ключи, и полная запись заменяет этот раздел, точечная перезапись теряется на диске. В продакшене "горячая перезапись конфигурации + модуль записывает обратно во время выполнения" может вызвать эту проблему, независимо от тестовой среды (обнаружено тестовым отчетом ErisPulse-DailyCard).

Причина: getConfig первый (точное попадание в _dirty_keys) немедленно return self._dirty_keys[key], полностью обходя четвертую часть _dirty_overlay потомков; _flush_config применяет ключи _dirty_keys по порядку вставки, и если полная запись следует после точечной, точечное значение теряется при полной записи. Кроме того: getConfig возвращает (точное попадание) копию ссылки на объект _dirty_keys, и если вызывающая сторона изменяет возвращенный словарь, это напрямую изменяет состояние, ожидающее записи.

Влияющие версии: 2.6.0 - 2.9.0-dev.2

Исправленная версия: 2.9.0-dev.2

Содержание исправления: В окне записи с ключами хранения единство семантики — точечные (более конкретные) значения в _dirty_keys имеют приоритет над полными (более широкими) значениями, как в чтении, так и в записи: ① getConfig точное попадание в _dirty_keys все еще добавляет _dirty_overlay потомков (не словарь, возвращается копия поддерева, как в ③ + ④ краевые значения); ② _flush_config применяет ключи _dirty_keys по глубине сортировки (полные/родительские сначала, точечные/потомки позже, стабильная сортировка сохраняет порядок при одинаковой глубине); ③ getConfig возвращает (точное попадание, родительское поддерево, объединение с поддеревом) с помощью copy.deepcopy, чтобы изолировать копию, не утечка ссылки на внутренние объекты _dirty_keys; без ключей для записи, быстрый путь и поведение чистого чтения не меняются.

Дата исправления: 2026/09/28

Шаги воспроизведения: ① setConfig("FB.y", 1); ② setConfig("FB", {"z": 2}); ③ force_save() — до исправления на диске остается только [FB] z = 2 (y потеряно), после исправления {"y": 1, "z": 2}; вариант A: после шагов ①② без записи getConfig("FB") — до исправления не содержит y, после исправления видно.

Регрессионное тестирование: tests/unit/test_unit_config.py::TestSectionAndDottedDirtyConsistency (вариант A два порядка вставки / вариант B запись и порядок не зависят / три уровня смешанные живые / изоляция копий / родительское поддерево изоляция)

Серьезность: 🟡 Средний

Тип: Система конфигурации


[BUG-040] Командный конец сообщения с разделителями платформы теряется безвозвратно

Проблема: Когда платформа разбивает одно сообщение на несколько текстовых сегментов (например, /kick и параметры между упоминанием или сегментами ввода), сопоставление команды берет только первый текстовый сегмент, message разбивается на 艾特 @xxx 123 → 艾特 без параметров, команда с args= сообщает об отсутствии параметров, и нет никаких следов. Причина: В текстовом извлечении command.py цикл берет первый type=="text" сегмент и break после, остальные сегменты (включая все не текстовые сегменты) теряются. Влияющие версии: 2.6.0 (введение сопоставления команд) - 2.10.0 Исправленная версия: 2.10.1-dev.0 Содержание исправления: Командный текст теперь собирается путем последовательного объединения всех сегментов text с пробелами; упоминания/изображения между не мешают полному анализу команды; также улучшена устойчивость к поврежденным сегментам (если message является None/строкой/не dict сегментом, не приводит к сбою сопоставления). Регрессионное тестирование: tests/unit/test_unit_command_multiseg.py

Серьезность: 🟡 Средний Тип: Система событий

[BUG-041] После отказа команды с alt_message перезапуск всей цепочки: двойные ответы и двойное срабатывание побочных эффектов правил доступа

Проблема: Когда alt_message адаптера и текстовый сегмент сообщения не совпадают (например, alt содержит префикс упоминания), срабатывание команды отклоняется правилами scope/ACL/master/permission, и сопоставитель повторно запускает всю цепочку с alt_message — пользователь получает два сообщения "недостаточно прав", а пользовательские функции прав (часто с побочными эффектами, такими как проверка базы данных/подсчет) вызываются дважды. Причина: Отказ и "не найдена команда" используют один и тот же возвратное значение, что заставляет сопоставитель считать, что можно попробовать alt в качестве резервной; но событие было признано до отказа, повторный запуск только повторно выполняет повторную отправку и проверку прав. Влияющие версии: 2.9.0 (управление/цепочка правил сформировалась) - 2.10.0 Исправленная версия: 2.10.1-dev.0 Содержание исправления: Текстовое обработка теперь имеет три состояния: unmatched/consumed/rejected — после сопоставления зарегистрированной команды (независимо от отказа) не запускать alt; только если не найдена ни одна команда, сохраняется резервное alt. Регрессионное тестирование: tests/unit/test_unit_command_multiseg.py::test_denied_command_not_rerun_via_alt

Серьезность: 🟡 Средний Тип: Система событий

[BUG-042] Командный обработчик, зарегистрированный в wait_reply, потребляется той же самой командой

Проблема: При вызове wait_reply / регистрации ожидания в обработчике команды, ожидание потребляется той же самой командой, что инициировала её — первый цикл взаимодействия пропускает первый вопрос, "ответ пользователя" на самом деле является самой командой (ответ не по теме / данные загрязнены). Причина: _handle_message вызывает interaction.resolve дважды — в начале и в конце метода, что приводит к тому, что повторное разрешение происходит после выполнения команды, и команда потребляется в конце. Влияющие версии: 2.8.0 (введение системы сессий взаимодействия) - 2.10.0 Исправленная версия: 2.10.1-dev.0 Содержание исправления: Цепочка обработки сводится к одному interaction.resolve (в начале метода), удаляется второе вызование в конце; ожидание потребляется следующим сообщением. Регрессионное тестирование: tests/unit/test_unit_command_multiseg.py::test_wait_reply_not_consumed_by_command_trigger

Серьезность: 🟡 Средний Тип: Система событий

[BUG-043] select() с одним ключом и несколькими путями отменяет один путь, другой тихо отменяет

Проблема: При event.select() с нескольких ожиданий, приводящих к одному ключу сессии (один пользователь, разные шаблоны, несколько ожиданий сессии и т.д.), последующий путь отменяет предыдущий, select немедленно выбрасывает InteractionCancelled, а не возвращается timeout с (None, None), как заявлено в документации. Причина: Каждый вызов interaction.register имеет взаимоисключающую семантику для одного ключа, отправляя REASON_CONFLICT для отмены предыдущего; комментарий утверждает, что "несколько путей для одного пользователя не конфликтуют, так как копии изменяют только user_id", но копии изменяют только user_id, ключи для одного пользователя полностью одинаковы. Влияющие версии: 2.9.0 (введение select) - 2.10.0 Исправленная версия: 2.10.1-dev.0 Содержание исправления: select изменяется на агрегацию ожиданий — все ожидания объединяются в одно предложение с пользовательским matcher (ключи собираются, общая будущая), любое попадание возвращает соответствующий индекс; один ключ, несколько путей, естественно, существуют. Регрессионное тестирование: tests/unit/test_unit_command_multiseg.py::test_select_same_user_multi_branch

Серьезность: 🟡 Средний Тип: Система событий

[BUG-044] BaseConverter.at() генерирует устаревший "at" сегмент OB11, все проверки упоминания в фреймворке не распознают

Проблема: При использовании BaseConverter.at() для составления входящих событий, must_at_bot / has_mention() / on_at_message все слепы — упоминание бота в группе не работает. Причина: at() генерирует {"type": "at"} (формат OB11), в то время как все проверки упоминания и построение в фреймворке (MessageBuilder/декораторы) используют OB12 {"type": "mention"}, две системы существуют параллельно без предупреждений. Влияющие версии: Не ранее введения версии 2.5.0 - 2.10.0 Исправленная версия: 2.10.1-dev.0 Содержание исправления: at() теперь генерирует {"type": "mention", "data": {"user_id": ...}} (метод сохраняет псевдоним, добавлен mention псевдоним). Регрессионное тестирование: tests/unit/test_unit_command_multiseg.py::test_converter_at_produces_mention

Серьезность: 🟡 Средний Тип: Адаптер

[BUG-045] Без alt_message адаптер теряет ожидание, команды пересекаются

Проблема: Когда адаптер заполняет только message без alt_message, confirm / choose / collect ждут на ответ пользователя (например, "да") бесконечно; в ожидании зарегистрированные команды никогда не могут пройти; Event.get_text() возвращает пустую строку, что приводит к сбоям шаблонных примеров. Причина: Все эти пути берут alt_message напрямую, без возврата message — фреймворк "alt_message является необязательным полем адаптера" в контракте, только заполнение message является допустимым форматом. Влияющие версии: С момента введения интерактивных примитивов - 2.10.0 Исправленная версия: 2.10.1-dev.0 Содержание исправления: Единый подход extract_text для всех пяти точек получения/проверки значений, cmdpass проверка, Event.get_text()/get_message_text(). Регрессионное тестирование: tests/unit/test_unit_command_multiseg.py

Серьезность: 🟡 Средний Тип: Система событий

[BUG-046] Вложенные заголовки в шаблоне конфигурации смещаются в верхний уровень

Проблема: Вложенные dataclass объявленные в ConfigClass (вложенные поля dataclass) при генерации шаблона конфигурации в первый раз помещают заголовок подраздела [Nested] в верхний уровень, а не в компонентный раздел — адаптер не может получить значение cfg.嵌套字段. Причина: Генератор шаблона для вложенной dataclass рендерит относительный заголовок таблицы (например, [Nested]), а заголовки таблиц TOML имеют абсолютные пути с корня; setConfigTemplate перемещает разобранные объекты таблицы в компонентный раздел, config.full.example просто объединяет текст, обе траектории не переназначают относительный заголовок таблицы в путь компонента. Влияющие версии: 2.10.0 - 2.10.1 (вложенные dataclass шаблоны конфигурации с момента этого релиза) Исправленная версия: 2.10.2 Содержание исправления: setConfigTemplate точно переназначает заголовки таблицы (например, [Nested] → [组件名.Nested]) при анализе пути к таблице; config.full.example получает абсолютные заголовки таблиц, передавая _prefix. Регрессионное тестирование: tests/unit/test_unit_config.py::TestTomlclassEngineMigration::test_template_write_idempotent_and_nested_anchored

Серьезность: 🟡 Средний Тип: Система конфигурации / Адаптер


[BUG-047] Полный перечень служб модуля пропускает ленивые модули

Проблема: sdk.module.services() полный перечень модулей пропускает модули, которые не были проинициализированы (ленивые модули) — модуль объявил meta.services но не появляется в списке; точечный запрос services("Name") и топология get_topology() не затронуты. Причина: Ветвь полного перечня ModuleManager.services() без кэша контракта (не загруженные модули) после анализа контракта по-прежнему безусловно continue — намерение пропустить модули, которые не объявляют службы, на самом деле пропускает модули, которые не были загружены; загруженные модули, записавшие кэш контракта, не затронуты. Влияющие версии: 2.9.0 - 2.10.1 (модуль RPC и служба каталога с 2.9.0) Исправленная версия: 2.10.2 Содержание исправления: Удалить безусловный пропуск, использовать только "если результат пуст, пропустить"; модули, которые не были загружены, анализируют get_meta().services (то же, что и module.call() белый список), объявляющие службы, по-прежнему появляются в каталоге. Регрессионное тестирование: tests/unit/test_unit_module_rpc.py::TestServicesDirectory::test_lazy_declared_module_in_full_directory

Серьезность: 🟡 Средний Тип: Загрузка системы

[BUG-048] Запрос интерфейса хранилища сообщений теряет информацию об отправителе (sender и метаданные события)

Проблема: Все интерфейсы запроса хранилища сообщений ( get() / get_by_trace() / event.history() ) возвращают записи только с role/text/ts/event_id — столбец sender из хранилища был потерян, и метаданные события также не были сохранены в журнале, при повторном прослушивании сообщений в каталоге не различаются разные участники (AI ошибочно считает, что пользователь A — это предыдущий участник B); метаданные события, такие как никнейм, не сохраняются при повторном прослушивании, и при повторном анализе получается "нынешнее имя", а не "имя в то время". Причина: При добавлении sender в 2.8.0 комментарий указывал, что он используется для "восстановления источника сообщения", но Select и возвратная конструкция не включали его (пропущено реализация); отсутствие метаданных при сохранении — никогда не проектировалось; append() не имеет канала метаданных, и в структуре таблицы нет соответствующих столбцов. Влияющие версии: 2.8.0 - 2.11.0 (столбец sender с 2.8.0) Исправленная версия: 2.11.1-dev.0 Содержание исправления: Добавлен столбец meta (белый список: входящий никнейм/self_id/подтип/сообщение_id, исходящий контекст отправки и модуль; старая таблица автоматически мигрируется, при отсутствии ALTER в бэкенде тихо возвращается {}); append() получает параметр meta, проходящий через буфер памяти и массовую запись; get / recent / get_by_trace / event.history возвращают sender + meta (JSON повреждение возвращает {}). Связано: #467 (в той же серии #468 / #469 / #470) Регрессионное тестирование: tests/unit/test_unit_transcript_attribution.py::TestSenderMetaReadback / TestMetaMigration

Серьезность: 🟡 Средний Тип: Хранилище сообщений / Система хранения

[BUG-049] Автоматическое сообщение от бота при повторной отправке помечается как user, загрязняя журнал сообщений

Проблема: При включенной обратной связи сообщений на платформе/протоколе (OneBot v11 report-self-message, Telegram outgoing и т.д.), когда бот сам отправляет сообщение, журнал сообщений помечает его как role="user" — сообщение от бота и сообщение от реального пользователя не различаются, и下游ные модули обрабатывают сообщение от бота как сообщение от пользователя (AI отвечает на "сообщение пользователя"), что искажает статистику role. Причина: При автоматическом записи в Core/adapter.py для всех event_type == "message" безусловно помечается role="user", не потребляя self поле для определения обратной связи бота — в то же время последующая автоматическая обнаружение бота уже потребляет это поле (доступно на момент записи, но запись пропустила его). Влияющие версии: 2.8.0 - 2.11.0 (журнал сообщений автоматически записывается с 2.8.0) Исправленная версия: 2.11.1-dev.0 Содержание исправления: Перед записью проверяется self.user_id == user_id для определения обратной связи, в соответствии с настройкой ErisPulse.transcript.self_echo три режима: "bot" (по умолчанию) помечает role="bot" + sender=self_id + meta.self_echo=true; "skip" не записывает; "user" сохраняет прежнее поведение. Для платформ, где self отсутствует или не совпадает, каждый поле возвращается без изменений. Связано: #470 (зависит от #467, где метаданные meta хранят флаг self_echo) Регрессионное тестирование: tests/unit/test_unit_transcript_attribution.py::TestSelfEcho

Серьезность: 🟡 Средний Тип: Адаптер / Журнал сообщений