Files
PolyWeather/src/bot/handlers/basic.py
T
2569718930@qq.com 2183c0399d 移除错误定价信号和市场聚焦摘要功能
保留关键市场警报推送(4 条触发规则不变),
移除 mispricing 信号分类/推送、focus digest 评分/聚合/定时推送、
/markets 手动查询等全套逻辑,净删除 1049 行。
2026-05-07 21:42:06 +08:00

268 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from typing import Any
from typing import Callable
from src.bot.command_parser import extract_command_name
from src.bot.io_layer import BotIOLayer
from src.bot.observability import CommandTrace
from src.bot.runtime_coordinator import RuntimeStatus, render_runtime_status_html
_BASIC_COMMANDS = {"start", "help", "id", "top", "diag", "bind", "unbind"}
_BASIC_COMMANDS = {"start", "help", "id", "top", "diag", "bind", "unbind", "markets"}
class BasicCommandHandler:
def __init__(
self,
bot: Any,
io_layer: BotIOLayer,
runtime_status_provider: Callable[[], RuntimeStatus],
config: dict | None = None,
):
self.bot = bot
self.io_layer = io_layer
self.runtime_status_provider = runtime_status_provider
self.config = config or {}
def register(self) -> None:
@self.bot.message_handler(commands=["start", "help"])
def _start_help(message):
self._dispatch(message)
@self.bot.message_handler(commands=["id"])
def _id(message):
self._dispatch(message)
@self.bot.message_handler(commands=["top"])
def _top(message):
self._dispatch(message)
@self.bot.message_handler(commands=["diag"])
def _diag(message):
self._dispatch(message)
@self.bot.message_handler(commands=["bind"])
def _bind(message):
self._dispatch(message)
@self.bot.message_handler(commands=["unbind"])
def _unbind(message):
self._dispatch(message)
@self.bot.message_handler(commands=["markets"])
def _markets(message):
self._dispatch(message)
@self.bot.message_handler(
content_types=["text"],
func=lambda message: extract_command_name(
getattr(message, "text", None),
getattr(message, "entities", None),
)
in _BASIC_COMMANDS,
)
def _basic_text(message):
self._dispatch(message)
def _dispatch(self, message: Any) -> None:
command = extract_command_name(
getattr(message, "text", None),
getattr(message, "entities", None),
)
if command not in _BASIC_COMMANDS:
return
if getattr(message, "_pw_basic_handled", False):
return
setattr(message, "_pw_basic_handled", True)
setattr(message, "_pw_command_handled", True)
if command in {"start", "help"}:
self.handle_start_help(message)
return
if command == "id":
self.handle_id(message)
return
if command == "top":
self.handle_top(message)
return
if command == "diag":
self.handle_diag(message)
return
if command == "bind":
self.handle_bind(message)
return
if command == "unbind":
self.handle_unbind(message)
return
if command == "markets":
self.handle_markets(message)
return
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()
def handle_bind(self, message: Any) -> None:
trace = CommandTrace("/bind", message)
try:
parts = (message.text or "").split(maxsplit=2)
if len(parts) < 2:
self.bot.reply_to(
message,
(
"❌ 用法:\n"
"<code>/bind &lt;supabase_user_id&gt; [email]</code>\n\n"
"示例:\n"
"<code>/bind 11111111-2222-3333-4444-555555555555 user@example.com</code>"
),
parse_mode="HTML",
)
trace.set_status("bad_request", "missing_supabase_user_id")
return
supabase_user_id = str(parts[1] or "").strip()
if len(supabase_user_id) < 8:
self.bot.reply_to(message, "❌ supabase_user_id 格式不正确。")
trace.set_status("bad_request", "invalid_supabase_user_id")
return
supabase_email = str(parts[2] or "").strip() if len(parts) >= 3 else ""
user = message.from_user
self.io_layer.db.upsert_user(user.id, self.io_layer.display_name(user))
result = self.io_layer.db.bind_supabase_identity(
telegram_id=user.id,
supabase_user_id=supabase_user_id,
supabase_email=supabase_email,
)
if not bool(result.get("ok")):
reason = str(result.get("reason") or "bind_failed")
if reason == "telegram_already_bound_other":
current_uid = str(result.get("current_supabase_user_id") or "")
self.bot.reply_to(
message,
(
"❌ 当前 Telegram 已绑定其他网页账号。\n"
f"当前绑定: <code>{current_uid}</code>\n\n"
"请先执行 <code>/unbind</code> 再绑定新账号。"
),
parse_mode="HTML",
)
trace.set_status("conflict", "telegram_already_bound_other")
return
if reason == "supabase_already_bound_other":
owner = str(result.get("owner_telegram_id") or "")
self.bot.reply_to(
message,
(
"❌ 该网页账号已绑定到其他 Telegram。\n"
f"绑定中的 Telegram ID: <code>{owner}</code>\n\n"
"如需迁移,请先在原 Telegram 账号执行 <code>/unbind</code>。"
),
parse_mode="HTML",
)
trace.set_status("conflict", "supabase_already_bound_other")
return
self.bot.reply_to(message, "❌ 绑定失败,请稍后重试。")
trace.set_status("error", reason)
return
if str(result.get("reason") or "") == "already_bound_same":
self.bot.reply_to(
message,
(
"✅ 已是当前绑定账号,无需重复绑定。\n"
f"supabase_user_id: <code>{supabase_user_id}</code>"
),
parse_mode="HTML",
)
trace.set_status("ok", "already_bound_same")
return
self.bot.reply_to(
message,
(
"✅ 账号绑定完成。\n"
f"supabase_user_id: <code>{supabase_user_id}</code>"
),
parse_mode="HTML",
)
trace.set_status("ok")
finally:
trace.emit()
def handle_unbind(self, message: Any) -> None:
trace = CommandTrace("/unbind", message)
try:
user = message.from_user
self.io_layer.db.upsert_user(user.id, self.io_layer.display_name(user))
result = self.io_layer.db.unbind_supabase_identity(user.id)
if str(result.get("reason") or "") == "not_bound":
self.bot.reply_to(
message,
"️ 当前 Telegram 尚未绑定网页账号。",
)
trace.set_status("ok", "not_bound")
return
self.bot.reply_to(
message,
"✅ 已解除当前 Telegram 与网页账号的绑定。",
)
trace.set_status("ok", "unbound")
finally:
trace.emit()
def handle_markets(self, message: Any) -> None:
trace = CommandTrace("/markets", message)
try:
chat_type = str(getattr(getattr(message, "chat", None), "type", "") or "").strip().lower()
if chat_type and chat_type != "private":
self.bot.reply_to(
message,
"️ `/markets` 仅支持私聊机器人查询。",
parse_mode="Markdown",
)
trace.set_status("blocked", f"unsupported_chat_type:{chat_type}")
return
self.bot.reply_to(
message,
"️ 市场概览 (Focus Digest) 功能已移除。\n频道继续接收关键市场警报推送;如需查看当前市场状态,请访问 https://polyweather-pro.vercel.app/",
disable_web_page_preview=True,
)
trace.set_status("ok", "removed")
finally:
trace.emit()