Rename to hkt.sh

This commit is contained in:
mango
2026-03-21 01:10:53 +08:00
parent 76a263d0f9
commit 8f1171fe99
6676 changed files with 1724268 additions and 0 deletions

776
projects/news-bot/bot.py Normal file
View File

@@ -0,0 +1,776 @@
"""
Telegram 财经快讯机器人 - 主入口
功能:自动抓取财经/科技快讯AI 评分过滤,推送到 Telegram
"""
import os
import time
import logging
import asyncio
from datetime import datetime
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
Application, CommandHandler, CallbackQueryHandler,
ContextTypes, MessageHandler, filters,
)
from telegram.constants import ParseMode
import sources
import scorer
import storage
import summarizer
# 日志配置
logging.basicConfig(
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
level=logging.INFO,
)
logger = logging.getLogger(__name__)
# 配置
BOT_TOKEN = os.environ.get("BOT_TOKEN", "")
ADMIN_ID = int(os.environ.get("ADMIN_ID", "165067365"))
# 分页状态缓存
_page_cache = {}
# ========== 工具函数 ==========
def _check_admin(user_id: int) -> bool:
"""检查是否为管理员"""
return user_id == ADMIN_ID
def _format_news_item(item: dict) -> str:
"""格式化单条新闻(支持 HTML 链接)"""
score = item.get("score", 0)
if score >= 9:
emoji = "🔥"
elif score >= 8:
emoji = ""
elif score >= 7:
emoji = "📌"
elif score >= 6:
emoji = "🔹"
else:
emoji = ""
src = item.get("source_name", "")
ts = item.get("time_str", "")
title = item.get("title", "")
url = item.get("url", "")
# 转义 HTML 特殊字符
import html as html_mod
title_safe = html_mod.escape(title)
if url:
title_display = f'<a href="{url}">{title_safe}</a>'
else:
title_display = title_safe
return f"{emoji} {title_display}\n 📡 {src} 🕐 {ts} 评分:{score}"
# ========== /start 命令 ==========
async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""欢迎消息 + 主菜单"""
if not _check_admin(update.effective_user.id):
await update.message.reply_text("⛔ 仅限管理员使用")
return
keyboard = [
[InlineKeyboardButton("📰 最新快讯", callback_data="news_0"),
InlineKeyboardButton("📊 新闻总结", callback_data="summary_menu")],
[InlineKeyboardButton("📡 订阅源管理", callback_data="sources_menu"),
InlineKeyboardButton("🔑 关键词管理", callback_data="keywords_menu")],
[InlineKeyboardButton("⚙️ 设置", callback_data="settings_menu")],
]
text = (
"👋 <b>欢迎使用财经快讯机器人</b>\n\n"
"🤖 自动抓取金十、华尔街见闻、36氪、新浪财经快讯\n"
"📊 AI 评分过滤,只推送相关内容\n"
"⏰ 定时总结,不错过重要新闻\n\n"
"请选择功能:"
)
await update.message.reply_text(
text, parse_mode=ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
# ========== /news 命令 ==========
async def cmd_news(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看最新快讯"""
if not _check_admin(update.effective_user.id):
return
await _show_news_page(update.message, 0)
async def _show_news_page(message_or_query, page: int):
"""显示新闻分页"""
settings = storage.get_settings()
news_list = storage.get_news(limit=100)
# 评分
scorer.score_and_filter(news_list, settings.get("keywords", []))
# 只显示评分 >= 4 的
news_list = [n for n in news_list if n.get("score", 0) >= 4]
news_list.sort(key=lambda x: x.get("timestamp", 0), reverse=True)
page_size = 10
total_pages = max(1, (len(news_list) + page_size - 1) // page_size)
page = max(0, min(page, total_pages - 1))
start = page * page_size
page_items = news_list[start:start + page_size]
if not page_items:
text = "📭 暂无快讯,等待抓取中..."
else:
lines = [f"📰 <b>最新快讯</b> (第{page+1}/{total_pages}页)\n"]
for item in page_items:
lines.append(_format_news_item(item))
text = "\n".join(lines)
# 分页按钮
buttons = []
if page > 0:
buttons.append(InlineKeyboardButton("⬅️ 上一页", callback_data=f"news_{page-1}"))
buttons.append(InlineKeyboardButton("🔄 刷新", callback_data=f"news_{page}"))
if page < total_pages - 1:
buttons.append(InlineKeyboardButton("➡️ 下一页", callback_data=f"news_{page+1}"))
keyboard = [buttons, [InlineKeyboardButton("🏠 主菜单", callback_data="main_menu")]]
if hasattr(message_or_query, "edit_message_text"):
try:
await message_or_query.edit_message_text(
text, parse_mode=ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
except Exception:
await message_or_query.edit_message_text(
text, reply_markup=InlineKeyboardMarkup(keyboard),
)
else:
await message_or_query.reply_text(
text, parse_mode=ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
# ========== /summary 命令 ==========
async def cmd_summary(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""手动触发新闻总结"""
if not _check_admin(update.effective_user.id):
return
await _show_summary_menu(update.message)
async def _show_summary_menu(message_or_query):
"""显示总结时间范围选择"""
keyboard = [
[InlineKeyboardButton("⏱ 最近1小时", callback_data="summary_最近1小时")],
[InlineKeyboardButton("🌅 上午", callback_data="summary_上午")],
[InlineKeyboardButton("📅 全天", callback_data="summary_全天")],
[InlineKeyboardButton("🏠 主菜单", callback_data="main_menu")],
]
text = "📊 <b>新闻总结</b>\n\n请选择时间范围:"
if hasattr(message_or_query, "edit_message_text"):
await message_or_query.edit_message_text(
text, parse_mode=ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
else:
await message_or_query.reply_text(
text, parse_mode=ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
# ========== /sources 命令 ==========
async def cmd_sources(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""查看/管理订阅源"""
if not _check_admin(update.effective_user.id):
return
await _show_sources_menu(update.message)
async def _show_sources_menu(message_or_query):
"""显示订阅源管理菜单"""
settings = storage.get_settings()
srcs = settings.get("sources", {})
source_info = {
"jin10": "金十数据",
"wallstreet": "华尔街见闻",
"kr36": "36氪",
"sina": "新浪财经",
}
keyboard = []
for key, name in source_info.items():
enabled = srcs.get(key, True)
status = "" if enabled else ""
keyboard.append([InlineKeyboardButton(
f"{status} {name}", callback_data=f"toggle_src_{key}"
)])
keyboard.append([InlineKeyboardButton("🏠 主菜单", callback_data="main_menu")])
text = "📡 <b>订阅源管理</b>\n\n点击切换开/关:"
if hasattr(message_or_query, "edit_message_text"):
await message_or_query.edit_message_text(
text, parse_mode=ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
else:
await message_or_query.reply_text(
text, parse_mode=ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
# ========== /keywords 命令 ==========
async def cmd_keywords(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""管理关注关键词"""
if not _check_admin(update.effective_user.id):
return
await _show_keywords_menu(update.message)
async def _show_keywords_menu(message_or_query):
"""显示关键词管理菜单"""
settings = storage.get_settings()
kw_list = settings.get("keywords", [])
# 每行显示3个关键词删除按钮
keyboard = []
row = []
for kw in kw_list[:30]:
row.append(InlineKeyboardButton(f"{kw}", callback_data=f"del_kw_{kw}"))
if len(row) == 3:
keyboard.append(row)
row = []
if row:
keyboard.append(row)
keyboard.append([InlineKeyboardButton(" 添加关键词", callback_data="add_kw_prompt")])
keyboard.append([InlineKeyboardButton("🏠 主菜单", callback_data="main_menu")])
text = f"🔑 <b>关键词管理</b>\n\n当前 {len(kw_list)} 个关键词\n(点击关键词可删除)"
if hasattr(message_or_query, "edit_message_text"):
await message_or_query.edit_message_text(
text, parse_mode=ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
else:
await message_or_query.reply_text(
text, parse_mode=ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
# ========== /settings 命令 ==========
async def cmd_settings(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""设置菜单"""
if not _check_admin(update.effective_user.id):
return
await _show_settings_menu(update.message)
async def _show_settings_menu(message_or_query):
"""显示设置菜单"""
settings = storage.get_settings()
push_on = settings.get("push_enabled", True)
min_score = settings.get("min_score", 6)
instant_score = settings.get("instant_score", 8)
interval = settings.get("batch_interval", 30)
push_txt = "✅ 推送已开" if push_on else "❌ 推送已关"
keyboard = [
[InlineKeyboardButton(push_txt, callback_data="toggle_push")],
[
InlineKeyboardButton(f"最低评分: {min_score} ", callback_data="score_min_down"),
InlineKeyboardButton(f"最低评分: {min_score} ", callback_data="score_min_up"),
],
[
InlineKeyboardButton(f"即时阈值: {instant_score} ", callback_data="score_instant_down"),
InlineKeyboardButton(f"即时阈值: {instant_score} ", callback_data="score_instant_up"),
],
[
InlineKeyboardButton(f"汇总间隔: {interval}min ", callback_data="interval_down"),
InlineKeyboardButton(f"汇总间隔: {interval}min ", callback_data="interval_up"),
],
[InlineKeyboardButton("🏠 主菜单", callback_data="main_menu")],
]
text = (
f"⚙️ <b>设置</b>\n\n"
f"推送状态: {'开启' if push_on else '关闭'}\n"
f"最低推送评分: {min_score}\n"
f"即时推送阈值: {instant_score}\n"
f"汇总推送间隔: {interval} 分钟"
)
if hasattr(message_or_query, "edit_message_text"):
await message_or_query.edit_message_text(
text, parse_mode=ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
else:
await message_or_query.reply_text(
text, parse_mode=ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
# ========== CallbackQuery 处理器 ==========
# 等待用户输入关键词的状态
_waiting_kw_add = set()
async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""统一处理所有按钮回调"""
query = update.callback_query
await query.answer()
data = query.data
user_id = query.from_user.id
if not _check_admin(user_id):
return
# ---------- 主菜单 ----------
if data == "main_menu":
keyboard = [
[InlineKeyboardButton("📰 最新快讯", callback_data="news_0"),
InlineKeyboardButton("📊 新闻总结", callback_data="summary_menu")],
[InlineKeyboardButton("📡 订阅源管理", callback_data="sources_menu"),
InlineKeyboardButton("🔑 关键词管理", callback_data="keywords_menu")],
[InlineKeyboardButton("⚙️ 设置", callback_data="settings_menu")],
]
await query.edit_message_text(
"🏠 <b>主菜单</b>\n\n请选择功能:",
parse_mode=ParseMode.HTML,
reply_markup=InlineKeyboardMarkup(keyboard),
)
# ---------- 新闻分页 ----------
elif data.startswith("news_"):
page = int(data.split("_", 1)[1])
await _show_news_page(query, page)
# ---------- 总结菜单 ----------
elif data == "summary_menu":
await _show_summary_menu(query)
elif data.startswith("summary_"):
period = data[len("summary_"):]
start_ts, end_ts = summarizer.get_period_range(period)
settings = storage.get_settings()
news_list = storage.get_news(limit=500, since_ts=start_ts)
news_list = [n for n in news_list if n.get("timestamp", 0) <= end_ts]
scorer.score_and_filter(news_list, settings.get("keywords", []))
news_list = [n for n in news_list if n.get("score", 0) >= settings.get("min_score", 6)]
text = summarizer.generate_summary(news_list, period)
back_btn = InlineKeyboardMarkup([[
InlineKeyboardButton("🔙 返回", callback_data="summary_menu"),
InlineKeyboardButton("🏠 主菜单", callback_data="main_menu"),
]])
try:
await query.edit_message_text(text, parse_mode=ParseMode.HTML, reply_markup=back_btn)
except Exception:
await query.edit_message_text(text, reply_markup=back_btn)
# ---------- 订阅源 ----------
elif data == "sources_menu":
await _show_sources_menu(query)
elif data.startswith("toggle_src_"):
src = data[len("toggle_src_"):]
new_state = storage.toggle_source(src)
state_txt = "开启" if new_state else "关闭"
await query.answer(f"{state_txt} {sources.SOURCE_NAMES.get(src, src)}", show_alert=False)
await _show_sources_menu(query)
# ---------- 关键词 ----------
elif data == "keywords_menu":
await _show_keywords_menu(query)
elif data.startswith("del_kw_"):
kw = data[len("del_kw_"):]
storage.remove_keyword(kw)
await query.answer(f"已删除关键词: {kw}", show_alert=False)
await _show_keywords_menu(query)
elif data == "add_kw_prompt":
_waiting_kw_add.add(user_id)
await query.edit_message_text(
"🔑 <b>添加关键词</b>\n\n请直接发送关键词文字(可空格分隔多个):",
parse_mode=ParseMode.HTML,
reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton("取消", callback_data="keywords_menu")
]]),
)
# ---------- 设置 ----------
elif data == "settings_menu":
await _show_settings_menu(query)
elif data == "toggle_push":
settings = storage.get_settings()
storage.update_settings({"push_enabled": not settings.get("push_enabled", True)})
await _show_settings_menu(query)
elif data == "score_min_up":
s = storage.get_settings()
storage.update_settings({"min_score": min(10, s.get("min_score", 6) + 1)})
await _show_settings_menu(query)
elif data == "score_min_down":
s = storage.get_settings()
storage.update_settings({"min_score": max(1, s.get("min_score", 6) - 1)})
await _show_settings_menu(query)
elif data == "score_instant_up":
s = storage.get_settings()
storage.update_settings({"instant_score": min(10, s.get("instant_score", 8) + 1)})
await _show_settings_menu(query)
elif data == "score_instant_down":
s = storage.get_settings()
storage.update_settings({"instant_score": max(1, s.get("instant_score", 8) - 1)})
await _show_settings_menu(query)
elif data == "interval_up":
s = storage.get_settings()
storage.update_settings({"batch_interval": min(120, s.get("batch_interval", 30) + 5)})
await _show_settings_menu(query)
elif data == "interval_down":
s = storage.get_settings()
storage.update_settings({"batch_interval": max(5, s.get("batch_interval", 30) - 5)})
await _show_settings_menu(query)
async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""处理普通文本消息(用于添加关键词)"""
user_id = update.effective_user.id
if not _check_admin(user_id):
return
if user_id not in _waiting_kw_add:
return
_waiting_kw_add.discard(user_id)
text = update.message.text.strip()
added = []
for kw in text.split():
if storage.add_keyword(kw):
added.append(kw)
if added:
await update.message.reply_text(
f"✅ 已添加关键词: {', '.join(added)}",
reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton("🔑 关键词管理", callback_data="keywords_menu")
]]),
)
else:
await update.message.reply_text("⚠️ 关键词已存在或无效")
# ========== 后台抓取任务 ==========
# 汇总队列评分6-7的新闻暂存
_batch_queue: list = []
_last_batch_push: float = 0.0
_pushed_in_memory: set = set() # 内存去重,防止并发重复推送
async def _fetch_and_process(app: Application):
"""核心抓取+处理循环"""
settings = storage.get_settings()
enabled = settings.get("sources", {})
push_on = settings.get("push_enabled", True)
min_score = settings.get("min_score", 6)
instant_score = settings.get("instant_score", 8)
logger.info("开始抓取新闻...")
try:
all_news = await sources.fetch_all(enabled)
except Exception as e:
logger.error(f"fetch_all 异常: {e}")
return
if not all_news:
return
# 评分
scorer.score_and_filter(all_news, settings.get("keywords", []))
# 去重(全局标题去重)
all_news = scorer.dedup_news(all_news)
# 过滤已推送(内存+文件双重去重)
pushed_ids = storage.get_pushed_ids() | _pushed_in_memory
new_items = [n for n in all_news if n["id"] not in pushed_ids and n.get("score", 0) >= min_score]
if not new_items:
return
# 保存到存储
storage.save_news(new_items)
if not push_on:
return
instant_items = [n for n in new_items if n.get("score", 0) >= instant_score]
batch_items = [n for n in new_items if min_score <= n.get("score", 0) < instant_score]
# 即时推送
for item in instant_items:
await _push_instant(app, item)
# 加入汇总队列(去重)
existing_ids = {n["id"] for n in _batch_queue}
for item in batch_items:
if item["id"] not in existing_ids:
_batch_queue.append(item)
existing_ids.add(item["id"])
# 标记已推送(即时+批量都标记,防止重复)
pushed = [n["id"] for n in instant_items + batch_items]
_pushed_in_memory.update(pushed)
storage.mark_pushed(pushed)
async def _push_instant(app: Application, item: dict):
"""立即推送单条重磅新闻"""
score = item.get("score", 0)
if score >= 9:
emoji = "🔥🔥🔥"
elif score >= 8:
emoji = "⚡ 重磅"
else:
emoji = "📌"
src = item.get("source_name", "")
title = item.get("title", "")
ts = item.get("time_str", "")
url = item.get("url", "")
import html as html_mod
title_safe = html_mod.escape(title)
if url:
title_display = f'<a href="{url}">{title_safe}</a>'
else:
title_display = f"<b>{title_safe}</b>"
text = f"{emoji}\n\n{title_display}\n\n📡 {src} 🕐 {ts} 评分:{score}"
try:
await app.bot.send_message(
chat_id=ADMIN_ID, text=text, parse_mode=ParseMode.HTML,
)
except Exception as e:
logger.error(f"即时推送失败: {e}")
try:
await app.bot.send_message(chat_id=ADMIN_ID, text=f"{emoji}\n\n{title}\n\n{src} {ts}")
except Exception as e2:
logger.error(f"即时推送备用失败: {e2}")
async def _push_batch(app: Application):
"""汇总推送每N分钟一次"""
global _batch_queue, _last_batch_push
if not _batch_queue:
return
settings = storage.get_settings()
interval = settings.get("batch_interval", 30) * 60
now = time.time()
if now - _last_batch_push < interval:
return
items = _batch_queue[:]
_batch_queue.clear()
_last_batch_push = now
# 标记已推送
storage.mark_pushed([n["id"] for n in items])
if not items:
return
import html as html_mod
from collections import defaultdict
# 按来源分组
source_map = defaultdict(list)
for item in sorted(items, key=lambda x: x.get("score", 0), reverse=True):
src = item.get("source_name", "未知")
source_map[src].append(item)
source_emoji = {
"金十数据": "💰", "华尔街见闻": "📈", "36氪": "🚀",
"新浪财经": "📊", "Google News": "🌐",
"Finviz": "📉", "TechCrunch": "💻",
}
lines = [f"🟡 <b>快讯汇总</b>{len(items)}条)"]
lines.append("")
for src, src_items in source_map.items():
emoji = source_emoji.get(src, "📰")
lines.append(f"{emoji} <b>{src}</b>{len(src_items)}")
for item in src_items[:10]:
title = item.get("title", "")
url = item.get("url", "")
title_safe = html_mod.escape(title)
if url:
lines.append(f" • <a href=\"{url}\">{title_safe}</a>")
else:
lines.append(f"{title_safe}")
if len(src_items) > 10:
lines.append(f" ... 还有{len(src_items) - 10}")
lines.append("")
lines.append(f"{datetime.now().strftime('%H:%M')}")
text = "\n".join(lines)
try:
await app.bot.send_message(
chat_id=ADMIN_ID, text=text, parse_mode=ParseMode.HTML,
)
except Exception as e:
logger.error(f"汇总推送失败: {e}")
try:
await app.bot.send_message(chat_id=ADMIN_ID, text="\n".join(lines[:15]))
except Exception as e2:
logger.error(f"汇总推送备用失败: {e2}")
async def _send_scheduled_summary(app: Application, period: str):
"""发送定时总结"""
settings = storage.get_settings()
start_ts, end_ts = summarizer.get_period_range(period)
news_list = storage.get_news(limit=500, since_ts=start_ts)
news_list = [n for n in news_list if n.get("timestamp", 0) <= end_ts]
scorer.score_and_filter(news_list, settings.get("keywords", []))
news_list = [n for n in news_list if n.get("score", 0) >= settings.get("min_score", 6)]
text = summarizer.generate_summary(news_list, period)
try:
await app.bot.send_message(
chat_id=ADMIN_ID, text=text, parse_mode=ParseMode.HTML,
)
except Exception as e:
logger.error(f"定时总结推送失败: {e}")
try:
await app.bot.send_message(chat_id=ADMIN_ID, text=text)
except Exception:
pass
# ========== 调度器 ==========
class Scheduler:
"""轻量调度器,基于 asyncio"""
def __init__(self, app: Application):
self.app = app
self._tasks: list[asyncio.Task] = []
def start(self):
self._tasks.append(asyncio.create_task(self._fast_loop()))
self._tasks.append(asyncio.create_task(self._slow_loop()))
self._tasks.append(asyncio.create_task(self._batch_loop()))
self._tasks.append(asyncio.create_task(self._summary_loop()))
logger.info("调度器已启动")
async def _fast_loop(self):
"""金十/华尔街见闻 每2分钟抓取"""
while True:
try:
await _fetch_and_process(self.app)
except Exception as e:
logger.error(f"fast_loop 异常: {e}")
await asyncio.sleep(120)
async def _slow_loop(self):
"""36氪/新浪 每5分钟额外触发fast_loop已包含全部这里可扩展"""
await asyncio.sleep(60) # 错开启动时间
while True:
try:
await _fetch_and_process(self.app)
except Exception as e:
logger.error(f"slow_loop 异常: {e}")
await asyncio.sleep(300)
async def _batch_loop(self):
"""每分钟检查是否需要汇总推送"""
await asyncio.sleep(30)
while True:
try:
await _push_batch(self.app)
except Exception as e:
logger.error(f"batch_loop 异常: {e}")
await asyncio.sleep(60)
async def _summary_loop(self):
"""定时总结 08:00 / 11:30 / 20:00"""
schedule = [
(8, 0, "昨晚到今早"),
(11, 30, "上午"),
(20, 0, "全天"),
]
triggered = set()
while True:
now = datetime.now()
key = f"{now.date()}"
for hour, minute, period in schedule:
tid = f"{key}_{hour}_{minute}"
if tid not in triggered:
# 在目标时间 ±2 分钟内触发
target = now.replace(hour=hour, minute=minute, second=0)
diff = abs((now - target).total_seconds())
if diff <= 120:
triggered.add(tid)
try:
await _send_scheduled_summary(self.app, period)
except Exception as e:
logger.error(f"定时总结异常: {e}")
# 每分钟检查一次
await asyncio.sleep(60)
def stop(self):
for t in self._tasks:
t.cancel()
# ========== 主函数 ==========
def main():
if not BOT_TOKEN:
logger.error("请设置 BOT_TOKEN 环境变量")
return
logger.info(f"机器人启动ADMIN_ID={ADMIN_ID}")
app = Application.builder().token(BOT_TOKEN).build()
# 注册命令
app.add_handler(CommandHandler("start", cmd_start))
app.add_handler(CommandHandler("news", cmd_news))
app.add_handler(CommandHandler("summary", cmd_summary))
app.add_handler(CommandHandler("sources", cmd_sources))
app.add_handler(CommandHandler("keywords", cmd_keywords))
app.add_handler(CommandHandler("settings", cmd_settings))
# 注册回调
app.add_handler(CallbackQueryHandler(handle_callback))
# 注册文本消息(用于添加关键词)
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text))
# 启动调度器(在 post_init 中启动,确保 event loop 已就绪)
scheduler = Scheduler(app)
async def post_init(application: Application):
scheduler.start()
logger.info("调度器已在 post_init 中启动")
app.post_init = post_init
logger.info("开始 polling...")
app.run_polling(drop_pending_updates=True)
if __name__ == "__main__":
main()