diff --git a/handlers/bind.py b/handlers/bind.py index 66eb7c4..84b1f9b 100644 --- a/handlers/bind.py +++ b/handlers/bind.py @@ -24,7 +24,6 @@ async def cmd_bind(update: Update, context: ContextTypes.DEFAULT_TYPE): f"📅 绑定时间: {existing['bound_at']}\n\n" f"如需重新绑定,先 /unbind" ) - asyncio.create_task(auto_delete(update.message)) asyncio.create_task(auto_delete(reply)) return @@ -35,7 +34,6 @@ async def cmd_bind(update: Update, context: ContextTypes.DEFAULT_TYPE): f"请点击以下链接完成绑定:\n{url}\n\n" "⏳ 链接 5 分钟内有效" ) - asyncio.create_task(auto_delete(update.message)) asyncio.create_task(auto_delete(reply, seconds=120)) @@ -48,7 +46,6 @@ async def cmd_unbind(update: Update, context: ContextTypes.DEFAULT_TYPE): await Action.create(action="UNBIND", user_id=user.id, username=user.username) else: reply = await update.message.reply_text("❌ 你没有绑定的账号") - asyncio.create_task(auto_delete(update.message)) asyncio.create_task(auto_delete(reply)) diff --git a/handlers/fun.py b/handlers/fun.py index 9ba63e6..3a11278 100644 --- a/handlers/fun.py +++ b/handlers/fun.py @@ -55,7 +55,6 @@ async def cmd_checkin(update: Update, context: ContextTypes.DEFAULT_TYPE): ) else: reply = await update.message.reply_text("❌ 你今天已经签到过了") - asyncio.create_task(auto_delete(update.message)) asyncio.create_task(auto_delete(reply)) @@ -69,7 +68,6 @@ async def cmd_trivia(update: Update, context: ContextTypes.DEFAULT_TYPE): context.user_data["trivia_answer"] = q["a"] context.user_data["trivia_options"] = q["opts"] reply = await update.message.reply_text(text) - asyncio.create_task(auto_delete(update.message)) asyncio.create_task(auto_delete(reply)) @@ -85,7 +83,6 @@ async def cmd_score(update: Update, context: ContextTypes.DEFAULT_TYPE): for s in scores: text += f"🎮 {s.game_type}: {s.score} 分 (胜率 {s.wins}/{s.total})\n" reply = await update.message.reply_text(text) - asyncio.create_task(auto_delete(update.message)) asyncio.create_task(auto_delete(reply)) @@ -101,7 +98,6 @@ async def cmd_rank(update: Update, context: ContextTypes.DEFAULT_TYPE): medal = medals[i] if i < 3 else f" {i+1}." text += f"{medal} @{s.username or s.user_id} — {s.score} 分\n" reply = await update.message.reply_text(text) - asyncio.create_task(auto_delete(update.message)) asyncio.create_task(auto_delete(reply)) diff --git a/handlers/knowledge.py b/handlers/knowledge.py index 24afa18..7aab7ab 100644 --- a/handlers/knowledge.py +++ b/handlers/knowledge.py @@ -1,5 +1,6 @@ """知识库问答 handler""" import asyncio +import html import logging import re @@ -26,10 +27,23 @@ def optimize_plain_answer(text: str) -> str: return "*回答*\n" + "\n".join(f"- {line}" if not line.startswith(("-", "•", "✅", "⚠️", "📚")) else line for line in lines) -def telegram_markdown(text: str) -> str: - """把常见 Markdown 调整为 Telegram legacy Markdown 更容易接受的形式。""" - text = re.sub(r"\*\*([^*]+)\*\*", r"*\1*", text) - text = re.sub(r"^#{1,3}\s+(.+)$", r"*\1*", text, flags=re.MULTILINE) +def markdown_to_html(text: str) -> str: + """把常见 Markdown 转成 Telegram HTML,规避 Markdown 解析器对 _/链接/长文本的误判。""" + code_blocks: list[str] = [] + + def stash_code_block(match: re.Match) -> str: + code = html.escape(match.group(1).strip()) + code_blocks.append(f"
{code}
") + return f"@@CODE_BLOCK_{len(code_blocks) - 1}@@" + + text = re.sub(r"```(?:\w+)?\n?(.*?)```", stash_code_block, text, flags=re.S) + text = html.escape(text) + text = re.sub(r"^#{1,3}\s+(.+)$", r"\1", text, flags=re.M) + text = re.sub(r"\*\*([^*\n]+)\*\*", r"\1", text) + text = re.sub(r"(?\1", text) + text = re.sub(r"`([^`\n]+)`", r"\1", text) + for i, block in enumerate(code_blocks): + text = text.replace(f"@@CODE_BLOCK_{i}@@", block) return text @@ -70,28 +84,21 @@ def split_answer(text: str, max_len: int = 3900, max_parts: int = 2) -> list[str async def safe_send(update: Update, context: ContextTypes.DEFAULT_TYPE, text: str, markdown: bool | None = None): - """优先回复原消息;Markdown 失败或原消息被删时自动降级。""" + """优先回复原消息;Markdown 内容转 HTML 发送,失败自动降级纯文本。""" chat_id = update.effective_chat.id - use_markdown = looks_like_markdown(text) if markdown is None else markdown - send_text = telegram_markdown(text) if use_markdown else text + use_markup = looks_like_markdown(text) if markdown is None else markdown + send_text = markdown_to_html(text) if use_markup else text - async def _send_plain(): + if use_markup: try: - return await update.message.reply_text(text) - except Exception: - return await context.bot.send_message(chat_id=chat_id, text=text) - - if use_markdown: - try: - return await update.message.reply_text(send_text, parse_mode="Markdown", disable_web_page_preview=True) + return await update.message.reply_text(send_text, parse_mode="HTML", disable_web_page_preview=True) except Exception as e: if "Message to be replied not found" in str(e): try: - return await context.bot.send_message(chat_id=chat_id, text=send_text, parse_mode="Markdown", disable_web_page_preview=True) + return await context.bot.send_message(chat_id=chat_id, text=send_text, parse_mode="HTML", disable_web_page_preview=True) except Exception: - return await context.bot.send_message(chat_id=chat_id, text=text) - logger.warning(f"Markdown 发送失败,降级普通文本: {e}") - return await _send_plain() + return await context.bot.send_message(chat_id=chat_id, text=text, disable_web_page_preview=True) + logger.warning(f"HTML 格式发送失败,降级普通文本: {e}") try: return await update.message.reply_text(text, disable_web_page_preview=True) @@ -110,13 +117,11 @@ async def cmd_ask(update: Update, context: ContextTypes.DEFAULT_TYPE): "例如: /ask 怎么安装收割机\n" f"文档地址: {config.SITE_URL}" ) - asyncio.create_task(auto_delete(update.message, seconds=180)) asyncio.create_task(auto_delete(reply, seconds=180)) return logger.info(f"📩 用户提问: [{update.message.from_user.username}] {query}") searching_msg = await safe_send(update, context, "🔍 正在搜索知识库并整理详细答案...") - asyncio.create_task(auto_delete(update.message, seconds=180)) try: from services.knowledge_base import ask_knowledge_base diff --git a/handlers/night_mode.py b/handlers/night_mode.py index fb82c79..8b3b977 100644 --- a/handlers/night_mode.py +++ b/handlers/night_mode.py @@ -25,7 +25,6 @@ async def cmd_night(update: Update, context: ContextTypes.DEFAULT_TYPE): "/night on 开启\n" "/night off 关闭" ) - asyncio.create_task(auto_delete(update.message)) return cmd = args[0].lower() @@ -38,7 +37,6 @@ async def cmd_night(update: Update, context: ContextTypes.DEFAULT_TYPE): else: reply = await update.message.reply_text("❓ 用法: /night on|off") - asyncio.create_task(auto_delete(update.message)) asyncio.create_task(auto_delete(reply))