コンポーネント登録、依存解決、ロード/ティアダウン、SharedData連携の実装仕様
共通規約は .github/copilot-instructions.md を参照してください。
このプロジェクトでは、Bot本体以外の機能(翻訳・TTS・STT・キャッシュなど)はすべてコンポーネントとして実装し、 Bot起動時にロード、Bot終了時にティアダウンします。
Bot.setup_hook() が呼ばれる。SharedData.async_init() で共有マネージャー(Trans/TTS/STT など)を初期化する。ComponentBase.component_registry の依存関係を検証する。
RuntimeError。RuntimeError。add_component() し、各コンポーネントの component_load() が実行される。Bot.close() が呼ばれる。remove_component() する。component_teardown() が実行される。Bot.close() は複数回呼ばれる可能性があるため、_closed フラグで二重実行を防ぐ。ComponentBase を継承する(定義: src/core/components/base.py)。__init_subclass__() で component_registry に自動登録される。depends: ClassVar[list[str]] で宣言する。depends には クラス名文字列(例: "TranslationServiceComponent")を指定する。component_registry の型は dict[str, ComponentDescriptor]。
ComponentDescriptor は (component, depends, is_removable) のNamedTuple。is_removable=True のコンポーネントは src/core/components/removable/ 配下に定義されたコンポーネントで、Bot運転中に動的にアタッチ/デタッチできる。例:
from __future__ import annotations
from typing import ClassVar
from core.components.base import ComponentBase
from utils.logger_utils import LoggerUtils
logger = LoggerUtils.get_logger(__name__)
class MyComponent(ComponentBase):
depends: ClassVar[list[str]] = ["OtherComponent"]
async def component_load(self) -> None:
logger.debug("'%s' component loaded", self.__class__.__name__)
async def component_teardown(self) -> None:
logger.debug("'%s' component unloaded", self.__class__.__name__)
depends に従って自動解決される。depends で表現する。self.config は多段プロパティアクセスになるため、設定欠落時は AttributeError をまとめて捕捉する。
getattr() を段階的に使うより、try-except で囲うほうが実装と整合しやすい。
async def component_load(self) -> None:
try:
stt_enabled: bool = self.config.STT.ENABLED
if isinstance(stt_enabled, bool) and stt_enabled:
await self.stt_manager.async_init(on_result=self._on_stt_result)
logger.debug("'%s' component loaded", self.__class__.__name__)
return
logger.info(
"STT service is disabled by configuration; '%s' component loaded without initializing STT",
self.__class__.__name__,
)
except AttributeError as err:
logger.warning("STT service initialization skipped due to missing configuration: %s", err)
クリーンアップ対象が存在しないケースは AttributeError を許容する。
from contextlib import suppress
async def component_teardown(self) -> None:
with suppress(AttributeError):
await self.stt_manager.close()
logger.debug("'%s' component unloaded", self.__class__.__name__)
LoggerUtils.get_logger(__name__) を使用する。component_load() / component_teardown() の成功ログは debug で残す。info、設定欠落などの異常系は warning 以上を使う。self.shared 経由で参照する。self.config / self.trans_manager / self.tts_manager / self.stt_manager などのプロパティを優先して使う。Bot.setup_hook() 側で行われる前提のため、コンポーネント側では利用時の例外処理を行う。component_load() 失敗時に他コンポーネントへ不要な波及がないこと。component_teardown() が逆順で実行され、二重終了でも破綻しないこと。AttributeError ハンドリングで安全にスキップできること。ComponentBase 継承になっているか。depends が正しいクラス名で定義されているか。component_load() と component_teardown() の双方が実装されているか。