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
+52 -2
View File
@@ -16,7 +16,8 @@ from nanobot.bus.events import OutboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.channels.base import BaseChannel
from nanobot.config.paths import get_runtime_subdir
from nanobot.config.schema import MochatConfig
from nanobot.config.schema import Base
from pydantic import Field
try:
import socketio
@@ -208,6 +209,49 @@ def parse_timestamp(value: Any) -> int | None:
return None
# ---------------------------------------------------------------------------
# Config classes
# ---------------------------------------------------------------------------
class MochatMentionConfig(Base):
"""Mochat mention behavior configuration."""
require_in_groups: bool = False
class MochatGroupRule(Base):
"""Mochat per-group mention requirement."""
require_mention: bool = False
class MochatConfig(Base):
"""Mochat channel configuration."""
enabled: bool = False
base_url: str = "https://mochat.io"
socket_url: str = ""
socket_path: str = "/socket.io"
socket_disable_msgpack: bool = False
socket_reconnect_delay_ms: int = 1000
socket_max_reconnect_delay_ms: int = 10000
socket_connect_timeout_ms: int = 10000
refresh_interval_ms: int = 30000
watch_timeout_ms: int = 25000
watch_limit: int = 100
retry_delay_ms: int = 500
max_retry_attempts: int = 0
claw_token: str = ""
agent_user_id: str = ""
sessions: list[str] = Field(default_factory=list)
panels: list[str] = Field(default_factory=list)
allow_from: list[str] = Field(default_factory=list)
mention: MochatMentionConfig = Field(default_factory=MochatMentionConfig)
groups: dict[str, MochatGroupRule] = Field(default_factory=dict)
reply_delay_mode: str = "non-mention"
reply_delay_ms: int = 120000
# ---------------------------------------------------------------------------
# Channel
# ---------------------------------------------------------------------------
@@ -218,7 +262,13 @@ class MochatChannel(BaseChannel):
name = "mochat"
display_name = "Mochat"
def __init__(self, config: MochatConfig, bus: MessageBus):
@classmethod
def default_config(cls) -> dict[str, Any]:
return MochatConfig().model_dump(by_alias=True)
def __init__(self, config: Any, bus: MessageBus):
if isinstance(config, dict):
config = MochatConfig.model_validate(config)
super().__init__(config, bus)
self.config: MochatConfig = config
self._http: httpx.AsyncClient | None = None