Files
w 432f4f6a59 功能(前后端): 多智能体、渠道与配置系统全面升级
后端: 重构 agent context/loop/subagent/workflow,增强 agent team API、会话级运行时配置、渠道 handler/manager,以及 QQ、飞书、钉钉、企业微信等渠道能力;同步调整 send_media、spawn、workflow_tool、filesystem 等工具模块,移除 image_uploader。

前端: 重构聊天窗口、消息项、会话面板、工具调用卡片和设置中心;新增团队配置、会话配置、工作流面板、预设选择、虚拟滚动、提示框等组件与 composables;更新 agentTeams/store/types、国际化文案、主题样式与 frontend dist 构建产物。
2026-03-19 00:58:08 +08:00

185 lines
5.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""频道基类模块
定义所有聊天频道的抽象接口和消息数据结构。
每个具体频道(Telegram、飞书、钉钉等)都应继承 BaseChannel 并实现其抽象方法。
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
from loguru import logger
@dataclass
class InboundMessage:
"""入站消息 - 从聊天平台接收"""
channel: str
sender_id: str
chat_id: str
content: str
media: List[str] = None
metadata: Dict[str, Any] = None
def __post_init__(self):
if self.media is None:
self.media = []
if self.metadata is None:
self.metadata = {}
@dataclass
class OutboundMessage:
"""出站消息 - 发送到聊天平台"""
channel: str
chat_id: str
content: str
media: Optional[List[str]] = None
metadata: Dict[str, Any] = None
def __post_init__(self):
if self.media is None:
self.media = []
if self.metadata is None:
self.metadata = {}
class BaseChannel(ABC):
"""频道抽象基类
所有聊天频道的公共接口,提供:
- 生命周期管理(start / stop)
- 消息收发(send / _handle_message)
- 权限校验(is_allowed)
- 连接测试(test_connection)
"""
name: str = "base"
def __init__(self, config: Any):
self.config = config
self._running = False
self._message_callback = None
self._account_id = str(getattr(config, "account_id", "default") or "default")
self._instance_key = f"{self.name}:{self._account_id}"
# ------------------------------------------------------------------
# 抽象方法 - 子类必须实现
# ------------------------------------------------------------------
@abstractmethod
async def start(self) -> None:
"""启动频道,开始监听消息。"""
@abstractmethod
async def stop(self) -> None:
"""停止频道,释放资源。"""
@abstractmethod
async def send(self, msg: OutboundMessage) -> None:
"""通过此频道发送消息。"""
@abstractmethod
async def test_connection(self) -> Dict[str, Any]:
"""测试频道连接,返回 ``{success: bool, message: str}``。"""
# ------------------------------------------------------------------
# 消息回调
# ------------------------------------------------------------------
def set_message_callback(self, callback) -> None:
"""设置入站消息回调(由 ChannelManager 调用)。"""
self._message_callback = callback
# ------------------------------------------------------------------
# 权限校验
# ------------------------------------------------------------------
def is_allowed(self, sender_id: str) -> bool:
"""检查发送者是否在白名单中。空白名单表示允许所有人。"""
raw_allow_list = getattr(self.config, "allow_from", [])
allow_list = [
str(item).strip()
for item in (raw_allow_list or [])
if str(item).strip()
]
if not allow_list:
return True
sender_str = str(sender_id)
if sender_str in allow_list:
return True
# 支持 "id|username" 复合格式
if "|" in sender_str:
for part in sender_str.split("|"):
if part and part in allow_list:
return True
return False
# ------------------------------------------------------------------
# 入站消息处理
# ------------------------------------------------------------------
async def _handle_message(
self,
sender_id: str,
chat_id: str,
content: str,
media: Optional[List[str]] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> None:
"""处理入站消息:校验权限后转发到回调。"""
if not self.is_allowed(sender_id):
logger.warning(
f"Access denied for {sender_id} on {self.name}, "
f"add to allow_from to grant access"
)
return
msg = InboundMessage(
channel=self.name,
sender_id=str(sender_id),
chat_id=str(chat_id),
content=content,
media=media or [],
metadata={
**(metadata or {}),
"account_id": str((metadata or {}).get("account_id") or self._account_id),
"instance_key": str((metadata or {}).get("instance_key") or self._instance_key),
},
)
if self._message_callback:
await self._message_callback(msg)
else:
logger.warning(f"No message callback set for channel {self.name}")
# ------------------------------------------------------------------
# 属性
# ------------------------------------------------------------------
@property
def is_running(self) -> bool:
return self._running
@property
def display_name(self) -> str:
configured_name = str(getattr(self.config, "display_name", "") or "").strip()
if configured_name:
return configured_name
if self._account_id == "default":
return self.name.capitalize()
return f"{self.name.capitalize()} [{self._account_id}]"
@property
def account_id(self) -> str:
return self._account_id
@property
def instance_key(self) -> str:
return self._instance_key