Skip to content

Commit 5dbd3fb

Browse files
committed
feat: Implement bot mention detection and improve message parsing in Telegram adapter
1 parent d694925 commit 5dbd3fb

File tree

6 files changed

+90
-15
lines changed

6 files changed

+90
-15
lines changed

framework/im/message.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ def to_plain(self):
111111
def __repr__(self):
112112
return f"ImageMessage(url={self.url}, path={self.path}, format={self.format})"
113113

114-
115114
# 定义@消息元素
115+
# :deprecated
116116
class AtElement(MessageElement):
117117

118118
def __init__(self, user_id: str, nickname: str = ""):
@@ -128,6 +128,19 @@ def to_plain(self):
128128
def __repr__(self):
129129
return f"AtElement(user_id={self.user_id}, nickname={self.nickname})"
130130

131+
# 定义@消息元素
132+
class MentionElement(MessageElement):
133+
def __init__(self, target: ChatSender):
134+
self.target = target
135+
136+
def to_dict(self):
137+
return {"type": "mention", "data": {"target": self.target}}
138+
139+
def to_plain(self):
140+
return f"@{self.target.display_name or self.target.user_id}"
141+
142+
def __repr__(self):
143+
return f"MentionElement(target={self.target})"
131144

132145
# 定义回复消息元素
133146
class ReplyElement(MessageElement):

framework/im/sender.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class ChatSender:
1717
chat_type: ChatType
1818
group_id: Optional[str] = None
1919
raw_metadata: Dict[str, Any] = None
20-
20+
2121
@classmethod
2222
def from_group_chat(
2323
cls,
@@ -61,3 +61,13 @@ def __str__(self) -> str:
6161
return f"{self.group_id}:{self.user_id}"
6262
else:
6363
return f"c2c:{self.user_id}"
64+
65+
def __eq__(self, other: Any) -> bool:
66+
if isinstance(other, ChatSender):
67+
return self.user_id == other.user_id and \
68+
self.chat_type == other.chat_type and \
69+
self.group_id == other.group_id
70+
return False
71+
72+
def __hash__(self) -> int:
73+
return hash((self.user_id, self.chat_type, self.group_id))

framework/workflow/core/dispatch/registry.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from .models.dispatch_rules import CombinedDispatchRule, RuleGroup, SimpleDispatchRule
1111
from .rules.base import DispatchRule
12-
from .rules.message_rules import KeywordMatchRule, PrefixMatchRule, RegexMatchRule
12+
from .rules.message_rules import BotMentionMatchRule, KeywordMatchRule, PrefixMatchRule, RegexMatchRule
1313
from .rules.sender_rules import ChatSenderMatchRule, ChatSenderMismatchRule
1414
from .rules.system_rules import FallbackMatchRule, RandomChanceMatchRule
1515

@@ -176,6 +176,7 @@ def save_rules(self, rules_dir: Optional[str] = None):
176176
DispatchRule.register_rule_type(RegexMatchRule)
177177
DispatchRule.register_rule_type(PrefixMatchRule)
178178
DispatchRule.register_rule_type(KeywordMatchRule)
179+
DispatchRule.register_rule_type(BotMentionMatchRule)
179180
DispatchRule.register_rule_type(RandomChanceMatchRule)
180181
DispatchRule.register_rule_type(ChatSenderMatchRule)
181182
DispatchRule.register_rule_type(ChatSenderMismatchRule)

framework/workflow/core/dispatch/rules/message_rules.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pydantic import Field
55

66
from framework.im.message import IMMessage
7+
from framework.im.sender import ChatSender
78
from framework.workflow.core.workflow.registry import WorkflowRegistry
89

910
from .base import DispatchRule, RuleConfig
@@ -77,4 +78,23 @@ def get_config(self) -> KeywordRuleConfig:
7778

7879
@classmethod
7980
def from_config(cls, config: KeywordRuleConfig, workflow_registry: WorkflowRegistry, workflow_id: str) -> "KeywordMatchRule":
80-
return cls(config.keywords, workflow_registry, workflow_id)
81+
return cls(config.keywords, workflow_registry, workflow_id)
82+
83+
class BotMentionMatchRule(DispatchRule):
84+
"""根据机器人被提及匹配的规则"""
85+
config_class = RuleConfig
86+
type_name = "bot_mention"
87+
88+
def __init__(self, workflow_registry: WorkflowRegistry, workflow_id: str):
89+
super().__init__(workflow_registry, workflow_id)
90+
91+
def match(self, message: IMMessage) -> bool:
92+
bot_sender = ChatSender.get_bot_sender()
93+
return any(element.type == "mention" and element.target == bot_sender for element in message.message_elements)
94+
95+
def get_config(self) -> RuleConfig:
96+
return RuleConfig()
97+
98+
@classmethod
99+
def from_config(cls, config: RuleConfig, workflow_registry: WorkflowRegistry, workflow_id: str) -> "BotMentionMatchRule":
100+
return cls(workflow_registry, workflow_id)

plugins/im_telegram_adapter/adapter.py

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
import telegramify_markdown
66
from pydantic import BaseModel, ConfigDict, Field
7-
from telegram import Update, User
7+
from telegram import Bot, Update, User
88
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
99

1010
from framework.im.adapter import EditStateAdapter, IMAdapter, UserProfileAdapter
11-
from framework.im.message import ImageMessage, IMMessage, TextMessage, VoiceMessage
11+
from framework.im.message import ImageMessage, IMMessage, MentionElement, TextMessage, VoiceMessage
1212
from framework.im.profile import Gender, UserProfile
1313
from framework.im.sender import ChatSender, ChatType
1414
from framework.logger import get_logger
@@ -46,17 +46,17 @@ class TelegramAdapter(IMAdapter, UserProfileAdapter, EditStateAdapter):
4646
def __init__(self, config: TelegramConfig):
4747
self.config = config
4848
self.application = Application.builder().token(config.token).build()
49-
49+
self.bot = Bot(token=config.token)
5050
# 注册命令处理器和消息处理器
51-
self.application.add_handler(CommandHandler("start", self.start))
51+
self.application.add_handler(CommandHandler("start", self.command_start))
5252
self.application.add_handler(
5353
MessageHandler(
5454
filters.TEXT | filters.VOICE | filters.PHOTO, self.handle_message
5555
)
5656
)
5757
self.logger = get_logger("Telegram-Adapter")
5858

59-
async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
59+
async def command_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
6060
"""处理 /start 命令"""
6161
await update.message.reply_text("Welcome! I am ready to receive your messages.")
6262

@@ -94,11 +94,41 @@ def convert_to_message(self, raw_message: Update) -> IMMessage:
9494

9595
message_elements = []
9696
raw_message_dict = raw_message.message.to_dict()
97-
9897
# 处理文本消息
9998
if raw_message.message.text:
100-
text_element = TextMessage(text=raw_message.message.text)
101-
message_elements.append(text_element)
99+
text = raw_message.message.text
100+
offset = 0
101+
for entity in raw_message.message.entities or []:
102+
if entity.type in ("mention", "text_mention"):
103+
# Extract mention text
104+
mention_text = text[entity.offset:entity.offset + entity.length]
105+
106+
# Add preceding text as TextMessage
107+
if entity.offset > offset:
108+
message_elements.append(TextMessage(text=text[offset:entity.offset]))
109+
110+
# Create ChatSender for MentionElement
111+
if entity.type == "text_mention" and entity.user:
112+
if entity.user.id == self.me.id:
113+
mention_element = MentionElement(target=ChatSender.get_bot_sender())
114+
else:
115+
mention_element = MentionElement(target=ChatSender.from_c2c_chat(user_id=entity.user.id, display_name=mention_text))
116+
elif entity.type == "mention":
117+
# 这里需要从 adapter 实例中获取 bot 的 username
118+
if mention_text == f'@{self.me.username}':
119+
mention_element = MentionElement(target=ChatSender.get_bot_sender())
120+
else:
121+
mention_element = MentionElement(target=ChatSender.from_c2c_chat(user_id=f'unknown_id:{mention_text}', display_name=mention_text))
122+
else:
123+
# Fallback in case of unknown entity type
124+
mention_element = TextMessage(text=mention_text) # Or handle as needed
125+
message_elements.append(mention_element)
126+
127+
offset = entity.offset + entity.length
128+
129+
# Add remaining text as TextMessage
130+
if offset < len(text):
131+
message_elements.append(TextMessage(text=text[offset:]))
102132

103133
# 处理语音消息
104134
if raw_message.message.voice:
@@ -115,13 +145,14 @@ def convert_to_message(self, raw_message: Update) -> IMMessage:
115145
photo_url = photo_file.file_path
116146
photo_element = ImageMessage(url=photo_url)
117147
message_elements.append(photo_element)
118-
148+
119149
# 创建 Message 对象
120150
message = IMMessage(
121151
sender=sender,
122152
message_elements=message_elements,
123153
raw_message=raw_message_dict,
124154
)
155+
print(message)
125156
return message
126157

127158
async def send_message(self, message: IMMessage, recipient: ChatSender):
@@ -171,6 +202,7 @@ async def start(self):
171202
"""启动 Bot"""
172203
await self.application.initialize()
173204
await self.application.start()
205+
self.me = await self.bot.get_me()
174206
await self.application.updater.start_polling(drop_pending_updates=True)
175207

176208
async def stop(self):

tests/workflow_executor/test_workflow_basic.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11

2-
from framework.workflow.core.block import Block
3-
from framework.workflow.core.block.input_output import Input, Output
2+
from framework.workflow.core.block import Block, Input, Output
43
from framework.workflow.core.workflow import Wire, Workflow
54

65
# Define test blocks

0 commit comments

Comments
 (0)