Skip to content

Commit 1fa02af

Browse files
committed
feat: add /newchat command and topic precheck for groups
- Add /newchat local command to create new Topic and send first message - Add topic precheck when bot joins group (is_forum + can_manage_topics) - Send detailed setup instructions if precheck fails - Consumer uses message_data text/thread_id (allows handler override)
1 parent 328de52 commit 1fa02af

3 files changed

Lines changed: 134 additions & 6 deletions

File tree

agent-sdk-client/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ commands = [
88
[local_commands]
99
# Local-only commands handled by the client
1010
help = "Hello World"
11+
newchat = "创建新对话"
1112

1213
[security]
1314
# User IDs allowed to add bot to groups and send private messages.

agent-sdk-client/consumer.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ async def process_message(message_data: dict) -> None:
108108
'error_message': 'Failed to get response from Agent Server'
109109
}
110110

111+
# Use message_data fields for SQS message (allows handler to override text/thread_id)
112+
user_message = message_data.get('text') or message.text
113+
thread_id = message_data.get('thread_id') or message.message_thread_id
114+
111115
# Call Agent Server
112116
try:
113117
async with httpx.AsyncClient(timeout=600.0) as client:
@@ -118,9 +122,9 @@ async def process_message(message_data: dict) -> None:
118122
'Content-Type': 'application/json',
119123
},
120124
json={
121-
'user_message': message.text,
125+
'user_message': user_message,
122126
'chat_id': str(message.chat_id),
123-
'thread_id': str(message.message_thread_id) if message.message_thread_id else None,
127+
'thread_id': str(thread_id) if thread_id else None,
124128
},
125129
)
126130
response.raise_for_status()
@@ -131,7 +135,7 @@ async def process_message(message_data: dict) -> None:
131135
await bot.send_message(
132136
chat_id=message.chat_id,
133137
text="Request timed out.",
134-
message_thread_id=message.message_thread_id,
138+
message_thread_id=thread_id,
135139
)
136140
raise # Re-raise to trigger SQS retry for transient errors
137141

@@ -142,7 +146,7 @@ async def process_message(message_data: dict) -> None:
142146
await bot.send_message(
143147
chat_id=message.chat_id,
144148
text=error_text,
145-
message_thread_id=message.message_thread_id,
149+
message_thread_id=thread_id,
146150
)
147151
except Exception as send_error:
148152
logger.error(f"Failed to send error message to Telegram: {send_error}")
@@ -163,7 +167,7 @@ async def process_message(message_data: dict) -> None:
163167
chat_id=message.chat_id,
164168
text=text,
165169
parse_mode=ParseMode.MARKDOWN_V2,
166-
message_thread_id=message.message_thread_id,
170+
message_thread_id=thread_id,
167171
reply_to_message_id=message.message_id,
168172
)
169173
except BadRequest as e:
@@ -173,7 +177,7 @@ async def process_message(message_data: dict) -> None:
173177
chat_id=message.chat_id,
174178
text=safe_text,
175179
parse_mode=ParseMode.MARKDOWN_V2,
176-
message_thread_id=message.message_thread_id,
180+
message_thread_id=thread_id,
177181
reply_to_message_id=message.message_id,
178182
)
179183
else:

agent-sdk-client/handler.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,93 @@ def _handle_local_command(bot: Bot, message, config: Config, cmd: str) -> bool:
149149
return True
150150

151151

152+
async def _check_forum_requirements(bot: Bot, chat_id: int) -> tuple[bool, str]:
153+
"""检查群组 Topic 功能要求。
154+
155+
Returns:
156+
(is_ok, error_message) - 如果满足要求返回 (True, ""),否则返回 (False, 错误提示)
157+
"""
158+
try:
159+
chat = await bot.get_chat(chat_id)
160+
if not chat.is_forum:
161+
return False, (
162+
"⚠️ 群组未开启 Topics 功能\n\n"
163+
"请按以下步骤开启:\n"
164+
"1. 打开群组设置\n"
165+
"2. 点击「Topics」\n"
166+
"3. 开启 Topics 功能\n"
167+
"4. 重新添加 Bot"
168+
)
169+
170+
me = await bot.get_me()
171+
member = await bot.get_chat_member(chat_id, me.id)
172+
if not getattr(member, 'can_manage_topics', False):
173+
return False, (
174+
"⚠️ Bot 缺少「管理 Topics」权限\n\n"
175+
"请按以下步骤授权:\n"
176+
"1. 打开群组设置 > 管理员\n"
177+
"2. 选择此 Bot\n"
178+
"3. 开启「Manage Topics」权限"
179+
)
180+
return True, ""
181+
except Exception as e:
182+
logger.warning(f"Failed to check forum requirements: {e}")
183+
return False, f"检查权限失败: {str(e)[:100]}"
184+
185+
186+
async def _handle_newchat_command(
187+
bot: Bot, message, body: dict, config: Config, sqs, prompts: str
188+
) -> bool:
189+
"""处理 /newchat - 创建 Topic 并发送消息到 SQS。
190+
191+
Args:
192+
bot: Telegram Bot 实例
193+
message: Telegram Message 对象
194+
body: 原始 webhook body (用于构造 SQS 消息)
195+
config: 配置对象
196+
sqs: SQS 客户端
197+
prompts: 用户输入的消息内容
198+
199+
Returns:
200+
True 如果成功,False 如果失败
201+
"""
202+
from datetime import datetime
203+
204+
chat_id = message.chat_id
205+
topic_name = f"Chat {datetime.now().strftime('%m/%d %H:%M')}"
206+
207+
try:
208+
forum_topic = await bot.create_forum_topic(chat_id=chat_id, name=topic_name)
209+
new_thread_id = forum_topic.message_thread_id
210+
211+
# 使用标准 SQS 消息格式,覆盖 text 和 thread_id
212+
message_body = {
213+
'telegram_update': body,
214+
'chat_id': chat_id,
215+
'message_id': message.message_id,
216+
'text': prompts,
217+
'thread_id': new_thread_id,
218+
}
219+
220+
success = _send_to_sqs_safe(sqs, config.queue_url, message_body)
221+
if not success:
222+
await bot.send_message(
223+
chat_id=chat_id,
224+
text="发送消息失败,请重试",
225+
message_thread_id=new_thread_id,
226+
)
227+
return success
228+
229+
except Exception as e:
230+
logger.warning(f"Failed to create forum topic: {e}")
231+
await bot.send_message(
232+
chat_id=chat_id,
233+
text=f"创建 Topic 失败: {str(e)[:100]}",
234+
message_thread_id=message.message_thread_id,
235+
)
236+
return False
237+
238+
152239
def lambda_handler(event: dict, context: Any) -> dict:
153240
"""Lambda entry point - Producer.
154241
@@ -182,6 +269,23 @@ def lambda_handler(event: dict, context: Any) -> dict:
182269
extra={'chat_id': chat_id, 'inviter_id': inviter_id},
183270
)
184271
_send_metric('SecurityBlock.UnauthorizedGroup')
272+
else:
273+
# 授权群组的 Topic 预检
274+
member_update = update.my_chat_member
275+
old_status = member_update.old_chat_member.status
276+
new_status = member_update.new_chat_member.status
277+
278+
if old_status in ('left', 'kicked') and new_status in (
279+
'member',
280+
'administrator',
281+
):
282+
chat_id = member_update.chat.id
283+
is_ok, error_msg = asyncio.run(
284+
_check_forum_requirements(bot, chat_id)
285+
)
286+
if not is_ok:
287+
asyncio.run(bot.send_message(chat_id=chat_id, text=error_msg))
288+
_send_metric('TopicPrecheck.Failed')
185289
return {'statusCode': 200}
186290

187291
message = update.message or update.edited_message
@@ -201,6 +305,25 @@ def lambda_handler(event: dict, context: Any) -> dict:
201305
return {'statusCode': 200}
202306

203307
cmd = config.get_command(message.text)
308+
309+
# /newchat 特殊处理 - 创建 Topic 后发 SQS
310+
if cmd == '/newchat':
311+
# 提取 prompts:移除命令部分(包括可能的 @bot 后缀)
312+
parts = message.text.strip().split(maxsplit=1)
313+
prompts = parts[1] if len(parts) > 1 else ''
314+
if not prompts:
315+
bot.send_message(
316+
chat_id=message.chat_id,
317+
text="用法: /newchat <消息内容>",
318+
message_thread_id=message.message_thread_id,
319+
)
320+
return {'statusCode': 200}
321+
322+
sqs = _get_sqs_client()
323+
asyncio.run(_handle_newchat_command(bot, message, body, config, sqs, prompts))
324+
return {'statusCode': 200}
325+
326+
# 其他 local command 正常处理
204327
if cmd and config.is_local_command(cmd):
205328
_handle_local_command(bot, message, config, cmd)
206329
return {'statusCode': 200}

0 commit comments

Comments
 (0)