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

1""" 

2AgentOS Channels — 消息路由引擎。 

3 

4负责: 

5 1. 根据 webhook URL 路径匹配目标渠道适配器 

6 2. 维持渠道适配器实例注册表 

7 3. 提供统一的 send_message 接口(自动路由到正确渠道) 

8""" 

9 

10from __future__ import annotations 

11 

12import time 

13 

14from agentos.channels.base import BaseChannelAdapter, ReplyResult 

15from agentos.channels.message import ChannelMessage, ChannelType, ConversationContext 

16 

17 

18class ChannelRouter: 

19 """多渠道路由器 — 注册/查找/分发。 

20 

21 每个渠道适配器以 webhook_path 或 channel_type 注册。 

22 收到消息时按 path 匹配 > channel_type 匹配 > 默认适配器的顺序查找。 

23 """ 

24 

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 

30 

31 # ── 注册 ── 

32 

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 

42 

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) 

52 

53 # ── 查找 ── 

54 

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 

64 

65 def get(self, channel: ChannelType) -> BaseChannelAdapter | None: 

66 """按渠道类型获取适配器。""" 

67 return self._by_channel.get(channel) 

68 

69 # ── 会话管理 ── 

70 

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 

80 

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 

91 

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 ) 

102 

103 # ── 统一发送 ── 

104 

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) 

113 

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 

128 

129 # ── 状态 ── 

130 

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 ] 

141 

142 @property 

143 def active_count(self) -> int: 

144 return sum(1 for a in self._by_channel.values() if a.config.enabled)