feat: Implement new bot architecture including handlers, services, analysis modules, and comprehensive tests.

This commit is contained in:
2569718930@qq.com
2026-03-12 11:44:52 +08:00
parent c582015163
commit f4fea03f35
28 changed files with 1525 additions and 445 deletions
+2
View File
@@ -0,0 +1,2 @@
"""Bot command handlers."""
+20
View File
@@ -0,0 +1,20 @@
from __future__ import annotations
from typing import Any
from src.bot.io_layer import BotIOLayer
class ActivityHandler:
def __init__(self, bot: Any, io_layer: BotIOLayer):
self.bot = bot
self.io_layer = io_layer
def register(self) -> None:
@self.bot.message_handler(func=lambda message: True, content_types=["text"])
def _activity(message):
self.handle(message)
def handle(self, message: Any) -> None:
self.io_layer.track_group_text_activity(message)
+75
View File
@@ -0,0 +1,75 @@
from __future__ import annotations
from typing import Any
from typing import Callable
from src.bot.io_layer import BotIOLayer
from src.bot.observability import CommandTrace
from src.bot.runtime_coordinator import RuntimeStatus, render_runtime_status_html
class BasicCommandHandler:
def __init__(
self,
bot: Any,
io_layer: BotIOLayer,
runtime_status_provider: Callable[[], RuntimeStatus],
):
self.bot = bot
self.io_layer = io_layer
self.runtime_status_provider = runtime_status_provider
def register(self) -> None:
@self.bot.message_handler(commands=["start", "help"])
def _start_help(message):
self.handle_start_help(message)
@self.bot.message_handler(commands=["id"])
def _id(message):
self.handle_id(message)
@self.bot.message_handler(commands=["top"])
def _top(message):
self.handle_top(message)
@self.bot.message_handler(commands=["diag"])
def _diag(message):
self.handle_diag(message)
def handle_start_help(self, message: Any) -> None:
trace = CommandTrace("/start", message)
try:
self.bot.reply_to(message, self.io_layer.build_welcome_text(), parse_mode="HTML")
trace.set_status("ok")
finally:
trace.emit()
def handle_id(self, message: Any) -> None:
trace = CommandTrace("/id", message)
try:
self.bot.reply_to(
message,
f"🎯 当前聊天的 Chat ID 是: <code>{message.chat.id}</code>",
parse_mode="HTML",
)
trace.set_status("ok")
finally:
trace.emit()
def handle_top(self, message: Any) -> None:
trace = CommandTrace("/top", message)
try:
rank_text = self.io_layer.build_points_rank_text(message.from_user)
self.bot.send_message(message.chat.id, rank_text, parse_mode="HTML")
trace.set_status("ok")
finally:
trace.emit()
def handle_diag(self, message: Any) -> None:
trace = CommandTrace("/diag", message)
try:
status = self.runtime_status_provider()
self.bot.reply_to(message, render_runtime_status_html(status), parse_mode="HTML")
trace.set_status("ok")
finally:
trace.emit()
+76
View File
@@ -0,0 +1,76 @@
from __future__ import annotations
from typing import Any
from loguru import logger
from src.bot.command_guard import CommandGuard
from src.bot.observability import CommandTrace
from src.bot.services.city_command_service import CityCommandService
from src.bot.settings import CITY_QUERY_COST
class CityCommandHandler:
def __init__(
self,
bot: Any,
guard: CommandGuard,
city_service: CityCommandService,
):
self.bot = bot
self.guard = guard
self.city_service = city_service
def register(self) -> None:
@self.bot.message_handler(commands=["city"])
def _city(message):
self.handle(message)
def handle(self, message: Any) -> None:
trace = CommandTrace("/city", message)
try:
parts = (message.text or "").split(maxsplit=1)
if len(parts) < 2:
trace.set_status("bad_request", "missing_city")
self.bot.reply_to(
message,
"❌ 请输入城市名称\n\n用法: <code>/city chicago</code>",
parse_mode="HTML",
)
return
city_input = parts[1].strip().lower()
resolved = self.city_service.resolve_city(city_input)
if not resolved.ok:
city_list = ", ".join(resolved.supported_cities or [])
trace.set_status("bad_request", "city_not_supported")
self.bot.reply_to(
message,
f"❌ 未找到城市: <b>{city_input}</b>\n\n支持的城市: {city_list}",
parse_mode="HTML",
)
return
city_name = str(resolved.city_name)
if not self.guard.ensure_access_and_points(message, CITY_QUERY_COST, "/city"):
trace.set_status("blocked", "guard_rejected")
return
self.bot.send_message(
message.chat.id, f"🔍 正在查询 {city_name.title()} 的天气数据..."
)
report_result = self.city_service.build_report(city_name, CITY_QUERY_COST)
if not report_result.ok:
trace.set_status("failed", report_result.error or "city_report_failed")
self.bot.reply_to(message, f"❌ 查询失败: {report_result.error}")
return
self.bot.send_message(message.chat.id, str(report_result.report), parse_mode="HTML")
trace.set_status("ok", city_name)
except Exception as exc:
trace.set_status("failed", "unexpected_error")
logger.exception("查询 /city 失败")
self.bot.reply_to(message, f"❌ 查询失败: {exc}")
finally:
trace.emit()
+71
View File
@@ -0,0 +1,71 @@
from __future__ import annotations
from typing import Any
from loguru import logger
from src.bot.command_guard import CommandGuard
from src.bot.observability import CommandTrace
from src.bot.services.deb_command_service import DebCommandService
from src.bot.settings import DEB_QUERY_COST
class DebCommandHandler:
def __init__(
self,
bot: Any,
guard: CommandGuard,
deb_service: DebCommandService,
):
self.bot = bot
self.guard = guard
self.deb_service = deb_service
def register(self) -> None:
@self.bot.message_handler(commands=["deb"])
def _deb(message):
self.handle(message)
def handle(self, message: Any) -> None:
trace = CommandTrace("/deb", message)
try:
parts = (message.text or "").split(maxsplit=1)
if len(parts) < 2:
trace.set_status("bad_request", "missing_city")
self.bot.reply_to(
message,
"❌ 用法: <code>/deb ankara</code>",
parse_mode="HTML",
)
return
city_input = parts[1].strip().lower()
city_name = self.deb_service.resolve_city(city_input)
if not self.deb_service.has_history(city_name):
trace.set_status("bad_request", "history_missing")
self.bot.reply_to(
message,
f"❌ 暂无 {city_name} 的历史数据。",
parse_mode="HTML",
)
return
if not self.guard.ensure_access_and_points(message, DEB_QUERY_COST, "/deb"):
trace.set_status("blocked", "guard_rejected")
return
report_result = self.deb_service.build_report(city_name, DEB_QUERY_COST)
if not report_result.ok:
trace.set_status("failed", report_result.error or "deb_report_failed")
self.bot.reply_to(message, f"❌ 查询失败: {report_result.error}")
return
self.bot.reply_to(message, str(report_result.report), parse_mode="HTML")
trace.set_status("ok", city_name)
except Exception as exc:
trace.set_status("failed", "unexpected_error")
logger.exception("查询 /deb 失败")
self.bot.reply_to(message, f"❌ 查询失败: {exc}")
finally:
trace.emit()