refactor: auto-discover channels via pkgutil, eliminate hardcoded registry
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
"""Base channel interface for chat platforms."""
|
"""Base channel interface for chat platforms."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -18,6 +21,8 @@ class BaseChannel(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name: str = "base"
|
name: str = "base"
|
||||||
|
display_name: str = "Base"
|
||||||
|
transcription_api_key: str = ""
|
||||||
|
|
||||||
def __init__(self, config: Any, bus: MessageBus):
|
def __init__(self, config: Any, bus: MessageBus):
|
||||||
"""
|
"""
|
||||||
@@ -31,6 +36,19 @@ class BaseChannel(ABC):
|
|||||||
self.bus = bus
|
self.bus = bus
|
||||||
self._running = False
|
self._running = False
|
||||||
|
|
||||||
|
async def transcribe_audio(self, file_path: str | Path) -> str:
|
||||||
|
"""Transcribe an audio file via Groq Whisper. Returns empty string on failure."""
|
||||||
|
if not self.transcription_api_key:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
from nanobot.providers.transcription import GroqTranscriptionProvider
|
||||||
|
|
||||||
|
provider = GroqTranscriptionProvider(api_key=self.transcription_api_key)
|
||||||
|
return await provider.transcribe(file_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("{}: audio transcription failed: {}", self.name, e)
|
||||||
|
return ""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ class DingTalkChannel(BaseChannel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "dingtalk"
|
name = "dingtalk"
|
||||||
|
display_name = "DingTalk"
|
||||||
_IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
|
_IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
|
||||||
_AUDIO_EXTS = {".amr", ".mp3", ".wav", ".ogg", ".m4a", ".aac"}
|
_AUDIO_EXTS = {".amr", ".mp3", ".wav", ".ogg", ".m4a", ".aac"}
|
||||||
_VIDEO_EXTS = {".mp4", ".mov", ".avi", ".mkv", ".webm"}
|
_VIDEO_EXTS = {".mp4", ".mov", ".avi", ".mkv", ".webm"}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class DiscordChannel(BaseChannel):
|
|||||||
"""Discord channel using Gateway websocket."""
|
"""Discord channel using Gateway websocket."""
|
||||||
|
|
||||||
name = "discord"
|
name = "discord"
|
||||||
|
display_name = "Discord"
|
||||||
|
|
||||||
def __init__(self, config: DiscordConfig, bus: MessageBus):
|
def __init__(self, config: DiscordConfig, bus: MessageBus):
|
||||||
super().__init__(config, bus)
|
super().__init__(config, bus)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class EmailChannel(BaseChannel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "email"
|
name = "email"
|
||||||
|
display_name = "Email"
|
||||||
_IMAP_MONTHS = (
|
_IMAP_MONTHS = (
|
||||||
"Jan",
|
"Jan",
|
||||||
"Feb",
|
"Feb",
|
||||||
|
|||||||
@@ -244,11 +244,11 @@ class FeishuChannel(BaseChannel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "feishu"
|
name = "feishu"
|
||||||
|
display_name = "Feishu"
|
||||||
|
|
||||||
def __init__(self, config: FeishuConfig, bus: MessageBus, groq_api_key: str = ""):
|
def __init__(self, config: FeishuConfig, bus: MessageBus):
|
||||||
super().__init__(config, bus)
|
super().__init__(config, bus)
|
||||||
self.config: FeishuConfig = config
|
self.config: FeishuConfig = config
|
||||||
self.groq_api_key = groq_api_key
|
|
||||||
self._client: Any = None
|
self._client: Any = None
|
||||||
self._ws_client: Any = None
|
self._ws_client: Any = None
|
||||||
self._ws_thread: threading.Thread | None = None
|
self._ws_thread: threading.Thread | None = None
|
||||||
@@ -928,16 +928,10 @@ class FeishuChannel(BaseChannel):
|
|||||||
if file_path:
|
if file_path:
|
||||||
media_paths.append(file_path)
|
media_paths.append(file_path)
|
||||||
|
|
||||||
# Transcribe audio using Groq Whisper
|
if msg_type == "audio" and file_path:
|
||||||
if msg_type == "audio" and file_path and self.groq_api_key:
|
transcription = await self.transcribe_audio(file_path)
|
||||||
try:
|
if transcription:
|
||||||
from nanobot.providers.transcription import GroqTranscriptionProvider
|
content_text = f"[transcription: {transcription}]"
|
||||||
transcriber = GroqTranscriptionProvider(api_key=self.groq_api_key)
|
|
||||||
transcription = await transcriber.transcribe(file_path)
|
|
||||||
if transcription:
|
|
||||||
content_text = f"[transcription: {transcription}]"
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning("Failed to transcribe audio: {}", e)
|
|
||||||
|
|
||||||
content_parts.append(content_text)
|
content_parts.append(content_text)
|
||||||
|
|
||||||
|
|||||||
@@ -31,135 +31,23 @@ class ChannelManager:
|
|||||||
self._init_channels()
|
self._init_channels()
|
||||||
|
|
||||||
def _init_channels(self) -> None:
|
def _init_channels(self) -> None:
|
||||||
"""Initialize channels based on config."""
|
"""Initialize channels discovered via pkgutil scan."""
|
||||||
|
from nanobot.channels.registry import discover_channel_names, load_channel_class
|
||||||
|
|
||||||
# Telegram channel
|
groq_key = self.config.providers.groq.api_key
|
||||||
if self.config.channels.telegram.enabled:
|
|
||||||
|
for modname in discover_channel_names():
|
||||||
|
section = getattr(self.config.channels, modname, None)
|
||||||
|
if not section or not getattr(section, "enabled", False):
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
from nanobot.channels.telegram import TelegramChannel
|
cls = load_channel_class(modname)
|
||||||
self.channels["telegram"] = TelegramChannel(
|
channel = cls(section, self.bus)
|
||||||
self.config.channels.telegram,
|
channel.transcription_api_key = groq_key
|
||||||
self.bus,
|
self.channels[modname] = channel
|
||||||
groq_api_key=self.config.providers.groq.api_key,
|
logger.info("{} channel enabled", cls.display_name)
|
||||||
)
|
|
||||||
logger.info("Telegram channel enabled")
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
logger.warning("Telegram channel not available: {}", e)
|
logger.warning("{} channel not available: {}", modname, e)
|
||||||
|
|
||||||
# WhatsApp channel
|
|
||||||
if self.config.channels.whatsapp.enabled:
|
|
||||||
try:
|
|
||||||
from nanobot.channels.whatsapp import WhatsAppChannel
|
|
||||||
self.channels["whatsapp"] = WhatsAppChannel(
|
|
||||||
self.config.channels.whatsapp, self.bus
|
|
||||||
)
|
|
||||||
logger.info("WhatsApp channel enabled")
|
|
||||||
except ImportError as e:
|
|
||||||
logger.warning("WhatsApp channel not available: {}", e)
|
|
||||||
|
|
||||||
# Discord channel
|
|
||||||
if self.config.channels.discord.enabled:
|
|
||||||
try:
|
|
||||||
from nanobot.channels.discord import DiscordChannel
|
|
||||||
self.channels["discord"] = DiscordChannel(
|
|
||||||
self.config.channels.discord, self.bus
|
|
||||||
)
|
|
||||||
logger.info("Discord channel enabled")
|
|
||||||
except ImportError as e:
|
|
||||||
logger.warning("Discord channel not available: {}", e)
|
|
||||||
|
|
||||||
# Feishu channel
|
|
||||||
if self.config.channels.feishu.enabled:
|
|
||||||
try:
|
|
||||||
from nanobot.channels.feishu import FeishuChannel
|
|
||||||
self.channels["feishu"] = FeishuChannel(
|
|
||||||
self.config.channels.feishu, self.bus,
|
|
||||||
groq_api_key=self.config.providers.groq.api_key,
|
|
||||||
)
|
|
||||||
logger.info("Feishu channel enabled")
|
|
||||||
except ImportError as e:
|
|
||||||
logger.warning("Feishu channel not available: {}", e)
|
|
||||||
|
|
||||||
# Mochat channel
|
|
||||||
if self.config.channels.mochat.enabled:
|
|
||||||
try:
|
|
||||||
from nanobot.channels.mochat import MochatChannel
|
|
||||||
|
|
||||||
self.channels["mochat"] = MochatChannel(
|
|
||||||
self.config.channels.mochat, self.bus
|
|
||||||
)
|
|
||||||
logger.info("Mochat channel enabled")
|
|
||||||
except ImportError as e:
|
|
||||||
logger.warning("Mochat channel not available: {}", e)
|
|
||||||
|
|
||||||
# DingTalk channel
|
|
||||||
if self.config.channels.dingtalk.enabled:
|
|
||||||
try:
|
|
||||||
from nanobot.channels.dingtalk import DingTalkChannel
|
|
||||||
self.channels["dingtalk"] = DingTalkChannel(
|
|
||||||
self.config.channels.dingtalk, self.bus
|
|
||||||
)
|
|
||||||
logger.info("DingTalk channel enabled")
|
|
||||||
except ImportError as e:
|
|
||||||
logger.warning("DingTalk channel not available: {}", e)
|
|
||||||
|
|
||||||
# Email channel
|
|
||||||
if self.config.channels.email.enabled:
|
|
||||||
try:
|
|
||||||
from nanobot.channels.email import EmailChannel
|
|
||||||
self.channels["email"] = EmailChannel(
|
|
||||||
self.config.channels.email, self.bus
|
|
||||||
)
|
|
||||||
logger.info("Email channel enabled")
|
|
||||||
except ImportError as e:
|
|
||||||
logger.warning("Email channel not available: {}", e)
|
|
||||||
|
|
||||||
# Slack channel
|
|
||||||
if self.config.channels.slack.enabled:
|
|
||||||
try:
|
|
||||||
from nanobot.channels.slack import SlackChannel
|
|
||||||
self.channels["slack"] = SlackChannel(
|
|
||||||
self.config.channels.slack, self.bus
|
|
||||||
)
|
|
||||||
logger.info("Slack channel enabled")
|
|
||||||
except ImportError as e:
|
|
||||||
logger.warning("Slack channel not available: {}", e)
|
|
||||||
|
|
||||||
# QQ channel
|
|
||||||
if self.config.channels.qq.enabled:
|
|
||||||
try:
|
|
||||||
from nanobot.channels.qq import QQChannel
|
|
||||||
self.channels["qq"] = QQChannel(
|
|
||||||
self.config.channels.qq,
|
|
||||||
self.bus,
|
|
||||||
)
|
|
||||||
logger.info("QQ channel enabled")
|
|
||||||
except ImportError as e:
|
|
||||||
logger.warning("QQ channel not available: {}", e)
|
|
||||||
|
|
||||||
# Matrix channel
|
|
||||||
if self.config.channels.matrix.enabled:
|
|
||||||
try:
|
|
||||||
from nanobot.channels.matrix import MatrixChannel
|
|
||||||
self.channels["matrix"] = MatrixChannel(
|
|
||||||
self.config.channels.matrix,
|
|
||||||
self.bus,
|
|
||||||
)
|
|
||||||
logger.info("Matrix channel enabled")
|
|
||||||
except ImportError as e:
|
|
||||||
logger.warning("Matrix channel not available: {}", e)
|
|
||||||
|
|
||||||
# WeCom channel
|
|
||||||
if self.config.channels.wecom.enabled:
|
|
||||||
try:
|
|
||||||
from nanobot.channels.wecom import WecomChannel
|
|
||||||
self.channels["wecom"] = WecomChannel(
|
|
||||||
self.config.channels.wecom,
|
|
||||||
self.bus,
|
|
||||||
)
|
|
||||||
logger.info("WeCom channel enabled")
|
|
||||||
except ImportError as e:
|
|
||||||
logger.warning("WeCom channel not available: {}", e)
|
|
||||||
|
|
||||||
self._validate_allow_from()
|
self._validate_allow_from()
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ except ImportError as e:
|
|||||||
) from e
|
) from e
|
||||||
|
|
||||||
from nanobot.bus.events import OutboundMessage
|
from nanobot.bus.events import OutboundMessage
|
||||||
|
from nanobot.bus.queue import MessageBus
|
||||||
from nanobot.channels.base import BaseChannel
|
from nanobot.channels.base import BaseChannel
|
||||||
from nanobot.config.paths import get_data_dir, get_media_dir
|
from nanobot.config.paths import get_data_dir, get_media_dir
|
||||||
from nanobot.utils.helpers import safe_filename
|
from nanobot.utils.helpers import safe_filename
|
||||||
@@ -146,15 +147,15 @@ class MatrixChannel(BaseChannel):
|
|||||||
"""Matrix (Element) channel using long-polling sync."""
|
"""Matrix (Element) channel using long-polling sync."""
|
||||||
|
|
||||||
name = "matrix"
|
name = "matrix"
|
||||||
|
display_name = "Matrix"
|
||||||
|
|
||||||
def __init__(self, config: Any, bus, *, restrict_to_workspace: bool = False,
|
def __init__(self, config: Any, bus: MessageBus):
|
||||||
workspace: Path | None = None):
|
|
||||||
super().__init__(config, bus)
|
super().__init__(config, bus)
|
||||||
self.client: AsyncClient | None = None
|
self.client: AsyncClient | None = None
|
||||||
self._sync_task: asyncio.Task | None = None
|
self._sync_task: asyncio.Task | None = None
|
||||||
self._typing_tasks: dict[str, asyncio.Task] = {}
|
self._typing_tasks: dict[str, asyncio.Task] = {}
|
||||||
self._restrict_to_workspace = restrict_to_workspace
|
self._restrict_to_workspace = False
|
||||||
self._workspace = workspace.expanduser().resolve() if workspace else None
|
self._workspace: Path | None = None
|
||||||
self._server_upload_limit_bytes: int | None = None
|
self._server_upload_limit_bytes: int | None = None
|
||||||
self._server_upload_limit_checked = False
|
self._server_upload_limit_checked = False
|
||||||
|
|
||||||
@@ -677,7 +678,14 @@ class MatrixChannel(BaseChannel):
|
|||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
if isinstance(body := getattr(event, "body", None), str) and body.strip():
|
if isinstance(body := getattr(event, "body", None), str) and body.strip():
|
||||||
parts.append(body.strip())
|
parts.append(body.strip())
|
||||||
if marker:
|
|
||||||
|
if attachment and attachment.get("type") == "audio":
|
||||||
|
transcription = await self.transcribe_audio(attachment["path"])
|
||||||
|
if transcription:
|
||||||
|
parts.append(f"[transcription: {transcription}]")
|
||||||
|
else:
|
||||||
|
parts.append(marker)
|
||||||
|
elif marker:
|
||||||
parts.append(marker)
|
parts.append(marker)
|
||||||
|
|
||||||
await self._start_typing_keepalive(room.room_id)
|
await self._start_typing_keepalive(room.room_id)
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ class MochatChannel(BaseChannel):
|
|||||||
"""Mochat channel using socket.io with fallback polling workers."""
|
"""Mochat channel using socket.io with fallback polling workers."""
|
||||||
|
|
||||||
name = "mochat"
|
name = "mochat"
|
||||||
|
display_name = "Mochat"
|
||||||
|
|
||||||
def __init__(self, config: MochatConfig, bus: MessageBus):
|
def __init__(self, config: MochatConfig, bus: MessageBus):
|
||||||
super().__init__(config, bus)
|
super().__init__(config, bus)
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class QQChannel(BaseChannel):
|
|||||||
"""QQ channel using botpy SDK with WebSocket connection."""
|
"""QQ channel using botpy SDK with WebSocket connection."""
|
||||||
|
|
||||||
name = "qq"
|
name = "qq"
|
||||||
|
display_name = "QQ"
|
||||||
|
|
||||||
def __init__(self, config: QQConfig, bus: MessageBus):
|
def __init__(self, config: QQConfig, bus: MessageBus):
|
||||||
super().__init__(config, bus)
|
super().__init__(config, bus)
|
||||||
|
|||||||
35
nanobot/channels/registry.py
Normal file
35
nanobot/channels/registry.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"""Auto-discovery for channel modules — no hardcoded registry."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import pkgutil
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from nanobot.channels.base import BaseChannel
|
||||||
|
|
||||||
|
_INTERNAL = frozenset({"base", "manager", "registry"})
|
||||||
|
|
||||||
|
|
||||||
|
def discover_channel_names() -> list[str]:
|
||||||
|
"""Return all channel module names by scanning the package (zero imports)."""
|
||||||
|
import nanobot.channels as pkg
|
||||||
|
|
||||||
|
return [
|
||||||
|
name
|
||||||
|
for _, name, ispkg in pkgutil.iter_modules(pkg.__path__)
|
||||||
|
if name not in _INTERNAL and not ispkg
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def load_channel_class(module_name: str) -> type[BaseChannel]:
|
||||||
|
"""Import *module_name* and return the first BaseChannel subclass found."""
|
||||||
|
from nanobot.channels.base import BaseChannel as _Base
|
||||||
|
|
||||||
|
mod = importlib.import_module(f"nanobot.channels.{module_name}")
|
||||||
|
for attr in dir(mod):
|
||||||
|
obj = getattr(mod, attr)
|
||||||
|
if isinstance(obj, type) and issubclass(obj, _Base) and obj is not _Base:
|
||||||
|
return obj
|
||||||
|
raise ImportError(f"No BaseChannel subclass in nanobot.channels.{module_name}")
|
||||||
@@ -21,6 +21,7 @@ class SlackChannel(BaseChannel):
|
|||||||
"""Slack channel using Socket Mode."""
|
"""Slack channel using Socket Mode."""
|
||||||
|
|
||||||
name = "slack"
|
name = "slack"
|
||||||
|
display_name = "Slack"
|
||||||
|
|
||||||
def __init__(self, config: SlackConfig, bus: MessageBus):
|
def __init__(self, config: SlackConfig, bus: MessageBus):
|
||||||
super().__init__(config, bus)
|
super().__init__(config, bus)
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ class TelegramChannel(BaseChannel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "telegram"
|
name = "telegram"
|
||||||
|
display_name = "Telegram"
|
||||||
|
|
||||||
# Commands registered with Telegram's command menu
|
# Commands registered with Telegram's command menu
|
||||||
BOT_COMMANDS = [
|
BOT_COMMANDS = [
|
||||||
@@ -164,15 +165,9 @@ class TelegramChannel(BaseChannel):
|
|||||||
BotCommand("help", "Show available commands"),
|
BotCommand("help", "Show available commands"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, config: TelegramConfig, bus: MessageBus):
|
||||||
self,
|
|
||||||
config: TelegramConfig,
|
|
||||||
bus: MessageBus,
|
|
||||||
groq_api_key: str = "",
|
|
||||||
):
|
|
||||||
super().__init__(config, bus)
|
super().__init__(config, bus)
|
||||||
self.config: TelegramConfig = config
|
self.config: TelegramConfig = config
|
||||||
self.groq_api_key = groq_api_key
|
|
||||||
self._app: Application | None = None
|
self._app: Application | None = None
|
||||||
self._chat_ids: dict[str, int] = {} # Map sender_id to chat_id for replies
|
self._chat_ids: dict[str, int] = {} # Map sender_id to chat_id for replies
|
||||||
self._typing_tasks: dict[str, asyncio.Task] = {} # chat_id -> typing loop task
|
self._typing_tasks: dict[str, asyncio.Task] = {} # chat_id -> typing loop task
|
||||||
@@ -615,11 +610,8 @@ class TelegramChannel(BaseChannel):
|
|||||||
|
|
||||||
media_paths.append(str(file_path))
|
media_paths.append(str(file_path))
|
||||||
|
|
||||||
# Handle voice transcription
|
if media_type in ("voice", "audio"):
|
||||||
if media_type == "voice" or media_type == "audio":
|
transcription = await self.transcribe_audio(file_path)
|
||||||
from nanobot.providers.transcription import GroqTranscriptionProvider
|
|
||||||
transcriber = GroqTranscriptionProvider(api_key=self.groq_api_key)
|
|
||||||
transcription = await transcriber.transcribe(file_path)
|
|
||||||
if transcription:
|
if transcription:
|
||||||
logger.info("Transcribed {}: {}...", media_type, transcription[:50])
|
logger.info("Transcribed {}: {}...", media_type, transcription[:50])
|
||||||
content_parts.append(f"[transcription: {transcription}]")
|
content_parts.append(f"[transcription: {transcription}]")
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class WecomChannel(BaseChannel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "wecom"
|
name = "wecom"
|
||||||
|
display_name = "WeCom"
|
||||||
|
|
||||||
def __init__(self, config: WecomConfig, bus: MessageBus):
|
def __init__(self, config: WecomConfig, bus: MessageBus):
|
||||||
super().__init__(config, bus)
|
super().__init__(config, bus)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class WhatsAppChannel(BaseChannel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "whatsapp"
|
name = "whatsapp"
|
||||||
|
display_name = "WhatsApp"
|
||||||
|
|
||||||
def __init__(self, config: WhatsAppConfig, bus: MessageBus):
|
def __init__(self, config: WhatsAppConfig, bus: MessageBus):
|
||||||
super().__init__(config, bus)
|
super().__init__(config, bus)
|
||||||
|
|||||||
@@ -683,6 +683,7 @@ app.add_typer(channels_app, name="channels")
|
|||||||
@channels_app.command("status")
|
@channels_app.command("status")
|
||||||
def channels_status():
|
def channels_status():
|
||||||
"""Show channel status."""
|
"""Show channel status."""
|
||||||
|
from nanobot.channels.registry import discover_channel_names, load_channel_class
|
||||||
from nanobot.config.loader import load_config
|
from nanobot.config.loader import load_config
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
@@ -690,85 +691,19 @@ def channels_status():
|
|||||||
table = Table(title="Channel Status")
|
table = Table(title="Channel Status")
|
||||||
table.add_column("Channel", style="cyan")
|
table.add_column("Channel", style="cyan")
|
||||||
table.add_column("Enabled", style="green")
|
table.add_column("Enabled", style="green")
|
||||||
table.add_column("Configuration", style="yellow")
|
|
||||||
|
|
||||||
# WhatsApp
|
for modname in sorted(discover_channel_names()):
|
||||||
wa = config.channels.whatsapp
|
section = getattr(config.channels, modname, None)
|
||||||
table.add_row(
|
enabled = section and getattr(section, "enabled", False)
|
||||||
"WhatsApp",
|
try:
|
||||||
"✓" if wa.enabled else "✗",
|
cls = load_channel_class(modname)
|
||||||
wa.bridge_url
|
display = cls.display_name
|
||||||
)
|
except ImportError:
|
||||||
|
display = modname.title()
|
||||||
dc = config.channels.discord
|
table.add_row(
|
||||||
table.add_row(
|
display,
|
||||||
"Discord",
|
"[green]\u2713[/green]" if enabled else "[dim]\u2717[/dim]",
|
||||||
"✓" if dc.enabled else "✗",
|
)
|
||||||
dc.gateway_url
|
|
||||||
)
|
|
||||||
|
|
||||||
# Feishu
|
|
||||||
fs = config.channels.feishu
|
|
||||||
fs_config = f"app_id: {fs.app_id[:10]}..." if fs.app_id else "[dim]not configured[/dim]"
|
|
||||||
table.add_row(
|
|
||||||
"Feishu",
|
|
||||||
"✓" if fs.enabled else "✗",
|
|
||||||
fs_config
|
|
||||||
)
|
|
||||||
|
|
||||||
# Mochat
|
|
||||||
mc = config.channels.mochat
|
|
||||||
mc_base = mc.base_url or "[dim]not configured[/dim]"
|
|
||||||
table.add_row(
|
|
||||||
"Mochat",
|
|
||||||
"✓" if mc.enabled else "✗",
|
|
||||||
mc_base
|
|
||||||
)
|
|
||||||
|
|
||||||
# Telegram
|
|
||||||
tg = config.channels.telegram
|
|
||||||
tg_config = f"token: {tg.token[:10]}..." if tg.token else "[dim]not configured[/dim]"
|
|
||||||
table.add_row(
|
|
||||||
"Telegram",
|
|
||||||
"✓" if tg.enabled else "✗",
|
|
||||||
tg_config
|
|
||||||
)
|
|
||||||
|
|
||||||
# Slack
|
|
||||||
slack = config.channels.slack
|
|
||||||
slack_config = "socket" if slack.app_token and slack.bot_token else "[dim]not configured[/dim]"
|
|
||||||
table.add_row(
|
|
||||||
"Slack",
|
|
||||||
"✓" if slack.enabled else "✗",
|
|
||||||
slack_config
|
|
||||||
)
|
|
||||||
|
|
||||||
# DingTalk
|
|
||||||
dt = config.channels.dingtalk
|
|
||||||
dt_config = f"client_id: {dt.client_id[:10]}..." if dt.client_id else "[dim]not configured[/dim]"
|
|
||||||
table.add_row(
|
|
||||||
"DingTalk",
|
|
||||||
"✓" if dt.enabled else "✗",
|
|
||||||
dt_config
|
|
||||||
)
|
|
||||||
|
|
||||||
# QQ
|
|
||||||
qq = config.channels.qq
|
|
||||||
qq_config = f"app_id: {qq.app_id[:10]}..." if qq.app_id else "[dim]not configured[/dim]"
|
|
||||||
table.add_row(
|
|
||||||
"QQ",
|
|
||||||
"✓" if qq.enabled else "✗",
|
|
||||||
qq_config
|
|
||||||
)
|
|
||||||
|
|
||||||
# Email
|
|
||||||
em = config.channels.email
|
|
||||||
em_config = em.imap_host if em.imap_host else "[dim]not configured[/dim]"
|
|
||||||
table.add_row(
|
|
||||||
"Email",
|
|
||||||
"✓" if em.enabled else "✗",
|
|
||||||
em_config
|
|
||||||
)
|
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user