Coverage for agentos/channels/router.py: 0%
67 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 19:15 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 19:15 +0800
1"""
2AgentOS Channels — 消息路由引擎。
4负责:
5 1. 根据 webhook URL 路径匹配目标渠道适配器
6 2. 维持渠道适配器实例注册表
7 3. 提供统一的 send_message 接口(自动路由到正确渠道)
8"""
10from __future__ import annotations
12import time
14from agentos.channels.base import BaseChannelAdapter, ReplyResult
15from agentos.channels.message import ChannelMessage, ChannelType, ConversationContext
18class ChannelRouter:
19 """多渠道路由器 — 注册/查找/分发。
21 每个渠道适配器以 webhook_path 或 channel_type 注册。
22 收到消息时按 path 匹配 > channel_type 匹配 > 默认适配器的顺序查找。
23 """
25 def __init__(self):
26 self._adapters: dict[str, BaseChannelAdapter] = {} # webhook_path → adapter
27 self._by_channel: dict[ChannelType, BaseChannelAdapter] = {}
28 self._default: BaseChannelAdapter | None = None
29 self._contexts: dict[str, ConversationContext] = {} # session_id → context
31 # ── 注册 ──
33 def register(self, adapter: BaseChannelAdapter, webhook_path: str = "") -> None:
34 """注册一个渠道适配器。"""
35 channel = adapter.channel_type
36 self._by_channel[channel] = adapter
37 if webhook_path:
38 self._adapters[webhook_path] = adapter
39 adapter.config.webhook_path = webhook_path
40 if len(self._adapters) == 1:
41 self._default = adapter
43 def unregister(self, channel: ChannelType) -> None:
44 """注销渠道适配器。"""
45 adapter = self._by_channel.pop(channel, None)
46 if adapter:
47 paths_to_remove = [p for p, a in self._adapters.items() if a == adapter]
48 for p in paths_to_remove:
49 del self._adapters[p]
50 if self._default == adapter:
51 self._default = next(iter(self._adapters.values()), None)
53 # ── 查找 ──
55 def find(
56 self, webhook_path: str = "", channel: ChannelType | None = None
57 ) -> BaseChannelAdapter | None:
58 """按路径或渠道类型查找适配器。"""
59 if webhook_path and webhook_path in self._adapters:
60 return self._adapters[webhook_path]
61 if channel and channel in self._by_channel:
62 return self._by_channel[channel]
63 return self._default
65 def get(self, channel: ChannelType) -> BaseChannelAdapter | None:
66 """按渠道类型获取适配器。"""
67 return self._by_channel.get(channel)
69 # ── 会话管理 ──
71 def get_context(self, msg: ChannelMessage) -> ConversationContext:
72 """获取或创建会话上下文。"""
73 sid = msg.session_id or msg.sender_id
74 if sid in self._contexts:
75 ctx = self._contexts[sid]
76 ctx.history.append({"role": "user", "content": msg.content, "timestamp": msg.timestamp})
77 if len(ctx.history) > 50:
78 ctx.history = ctx.history[-50:]
79 return ctx
81 ctx = ConversationContext(
82 channel=msg.channel,
83 user_id=msg.sender_id,
84 session_id=sid,
85 channel_config={"conversation_id": msg.conversation_id},
86 metadata={"first_message_at": time.time()},
87 )
88 ctx.history.append({"role": "user", "content": msg.content, "timestamp": msg.timestamp})
89 self._contexts[sid] = ctx
90 return ctx
92 def update_context(self, session_id: str, reply_text: str) -> None:
93 """将 Agent 回复记录到会话上下文。"""
94 if session_id in self._contexts:
95 self._contexts[session_id].history.append(
96 {
97 "role": "assistant",
98 "content": reply_text,
99 "timestamp": time.time(),
100 }
101 )
103 # ── 统一发送 ──
105 async def send(
106 self, channel: ChannelType, user_id: str, content: str, msg_type: str = "text"
107 ) -> ReplyResult:
108 """统一发送接口 — 自动路由到目标渠道适配器。"""
109 adapter = self._by_channel.get(channel)
110 if not adapter:
111 return ReplyResult(success=False, error=f"No adapter for {channel.value}")
112 return await adapter.send_message(user_id, content, msg_type)
114 async def broadcast(
115 self, content: str, msg_type: str = "text", exclude: ChannelType | None = None
116 ) -> list[ReplyResult]:
117 """向所有已注册渠道广播消息。"""
118 results = []
119 for channel, adapter in self._by_channel.items():
120 if channel == exclude:
121 continue
122 results.append(
123 ReplyResult(
124 success=False, error=f"broadcast to {channel.value} requires explicit user_id"
125 )
126 )
127 return results
129 # ── 状态 ──
131 @property
132 def active_channels(self) -> list[dict]:
133 return [
134 {
135 "channel": a.channel_type.value,
136 "webhook_path": a.config.webhook_path,
137 "enabled": a.config.enabled,
138 }
139 for a in self._by_channel.values()
140 ]
142 @property
143 def active_count(self) -> int:
144 return sum(1 for a in self._by_channel.values() if a.config.enabled)