feat: channel plugin architecture with decoupled configs

- Add plugin discovery via Python entry_points (group: nanobot.channels)
- Move 11 channel Config classes from schema.py into their own channel modules
- ChannelsConfig now only keeps send_progress + send_tool_hints (extra=allow)
- Each built-in channel parses dict->Pydantic in __init__, zero internal changes
- All channels implement default_config() for onboard auto-population
- nanobot onboard injects defaults for all discovered channels (built-in + plugins)
- Add nanobot plugins list CLI command
- Add Channel Plugin Guide (docs/CHANNEL_PLUGIN_GUIDE.md)
- Fully backward compatible: existing config.json and sessions work as-is
- 340 tests pass, zero regressions
This commit is contained in:
Xubin Ren
2026-03-14 16:13:38 +08:00
committed by Xubin Ren
parent 58389766a7
commit dbdb43faff
26 changed files with 923 additions and 266 deletions
+18 -2
View File
@@ -11,11 +11,12 @@ from urllib.parse import unquote, urlparse
import httpx
from loguru import logger
from pydantic import Field
from nanobot.bus.events import OutboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.channels.base import BaseChannel
from nanobot.config.schema import DingTalkConfig
from nanobot.config.schema import Base
try:
from dingtalk_stream import (
@@ -102,6 +103,15 @@ class NanobotDingTalkHandler(CallbackHandler):
return AckMessage.STATUS_OK, "Error"
class DingTalkConfig(Base):
"""DingTalk channel configuration using Stream mode."""
enabled: bool = False
client_id: str = ""
client_secret: str = ""
allow_from: list[str] = Field(default_factory=list)
class DingTalkChannel(BaseChannel):
"""
DingTalk channel using Stream Mode.
@@ -119,7 +129,13 @@ class DingTalkChannel(BaseChannel):
_AUDIO_EXTS = {".amr", ".mp3", ".wav", ".ogg", ".m4a", ".aac"}
_VIDEO_EXTS = {".mp4", ".mov", ".avi", ".mkv", ".webm"}
def __init__(self, config: DingTalkConfig, bus: MessageBus):
@classmethod
def default_config(cls) -> dict[str, Any]:
return DingTalkConfig().model_dump(by_alias=True)
def __init__(self, config: Any, bus: MessageBus):
if isinstance(config, dict):
config = DingTalkConfig.model_validate(config)
super().__init__(config, bus)
self.config: DingTalkConfig = config
self._client: Any = None