feat(horse_racing): 实现赛马消息更新替换与自动撤回

重构消息发送逻辑,引入消息类型区分和自动撤回机制。赛马进度更新现在会替换前一条更新消息,避免消息刷屏;比赛结果发送前自动撤回最后一条进度更新,提升聊天体验。同时支持配置不同消息类型的自动撤回时间。

- 新增 MessageService.send_with_recall 方法统一处理消息发送和撤回
- 添加 recall_previous_of_type 方法用于撤回特定类型的上一条消息
- 修改 _send_to_scope 函数支持消息类型参数
- 更新测试代码以适配新的消息发送接口
This commit is contained in:
2026-04-07 20:17:00 +08:00
parent 889cfc799b
commit 9895256064
3 changed files with 63 additions and 26 deletions

View File

@@ -260,18 +260,14 @@ async def settle_race(room: Room) -> RaceResult | None:
return result return result
async def _send_to_scope(bot: Bot, scope: str, message: str): async def _send_to_scope(bot: Bot, scope: str, message: str, message_type: str = "race_update"):
"""Send message to group or private chat based on scope.""" """Send message to group or private chat based on scope."""
try: try:
outbound_message: str | Message = message outbound_message: str | Message = message
if config.RACE_RENDER_AS_IMAGE: if config.RACE_RENDER_AS_IMAGE:
outbound_message = _build_race_image_message(message) outbound_message = _build_race_image_message(message)
await bot.send_msg(
message_type="group" if scope.startswith("group_") else "private", await message_service.send_with_recall(bot, scope, message_type, outbound_message)
group_id=int(scope.split("_", 1)[1]) if scope.startswith("group_") else None,
user_id=int(scope.split("_", 1)[1]) if scope.startswith("test_") else None,
message=outbound_message,
)
except Exception: except Exception:
pass pass
@@ -281,7 +277,7 @@ async def run_race_with_settlement(bot: Bot, room: Room, scope: str):
room.state = RoomState.RUNNING room.state = RoomState.RUNNING
horse_list = "\n".join(f" {_format_horse_label(h)}" for h in _get_horses_in_order(room)) horse_list = "\n".join(f" {_format_horse_label(h)}" for h in _get_horses_in_order(room))
await _send_to_scope(bot, scope, f"比赛开始!\n{horse_list}") await _send_to_scope(bot, scope, f"比赛开始!\n{horse_list}", "race_update")
# Race loop with progress updates # Race loop with progress updates
try: try:
@@ -290,7 +286,7 @@ async def run_race_with_settlement(bot: Bot, room: Room, scope: str):
finished = race_engine.tick(room) finished = race_engine.tick(room)
progress = race_engine.format_progress(room) progress = race_engine.format_progress(room)
await _send_to_scope(bot, scope, progress) await _send_to_scope(bot, scope, progress, "race_update")
if finished: if finished:
champion = race_engine.determine_champion(finished) champion = race_engine.determine_champion(finished)
@@ -327,7 +323,9 @@ async def run_race_with_settlement(bot: Bot, room: Room, scope: str):
if result: if result:
result_lines.extend(await _format_point_change_lines(room, result.point_changes, result.point_change_summaries, name_map)) result_lines.extend(await _format_point_change_lines(room, result.point_changes, result.point_change_summaries, name_map))
await _send_to_scope(bot, scope, "\n".join(result_lines)) # Before sending result, we can recall the last update
await message_service.recall_previous_of_type(bot, scope, "race_update")
await _send_to_scope(bot, scope, "\n".join(result_lines), "race_result")
# Cleanup # Cleanup
race_engine.stop_race(scope) race_engine.stop_race(scope)

View File

@@ -1,6 +1,6 @@
import asyncio import asyncio
from typing import Optional, Callable from typing import Optional, Any
from datetime import datetime, timedelta from nonebot.adapters.onebot.v11 import Bot, Message
from .config import Config from .config import Config
@@ -9,25 +9,45 @@ class MessageService:
def __init__(self, config: Config): def __init__(self, config: Config):
self.config = config self.config = config
self.pending_recalls: dict[str, list[asyncio.Task]] = {} self.pending_recalls: dict[str, list[asyncio.Task]] = {}
self.last_messages: dict[str, dict[str, str]] = {} # scope -> {message_type -> message_id}
async def send_with_recall( async def send_with_recall(
self, self,
bot: Bot,
scope: str, scope: str,
message_type: str, message_type: str,
send_func: Callable, message: str | Message,
*args,
**kwargs,
) -> Optional[str]: ) -> Optional[str]:
"""Send message and schedule recall if configured.""" """Send message and schedule recall if configured.
If it's a 'race_update', recall the previous one first."""
try: try:
message_id = await send_func(*args, **kwargs) # For race_update, recall the previous one in the same scope
if message_type == "race_update":
await self.recall_previous_of_type(bot, scope, "race_update")
# Send the message
is_group = scope.startswith("group_")
result = await bot.send_msg(
message_type="group" if is_group else "private",
group_id=int(scope.split("_", 1)[1]) if is_group else None,
user_id=int(scope.split("_", 1)[1]) if not is_group else None,
message=message,
)
message_id = result.get("message_id") if isinstance(result, dict) else None
if not message_id: if not message_id:
return None return None
recall_time = self.config.MESSAGE_RECALL.get(message_type, 0) # Track the last message of this type
if recall_time > 0: if scope not in self.last_messages:
self.last_messages[scope] = {}
self.last_messages[scope][message_type] = message_id
# Schedule auto-recall if configured
recall_delay = self.config.MESSAGE_RECALL.get(message_type, 0)
if recall_delay > 0:
task = asyncio.create_task( task = asyncio.create_task(
self._schedule_recall(scope, message_id, recall_time, send_func) self._schedule_recall(bot, scope, message_id, recall_delay)
) )
if scope not in self.pending_recalls: if scope not in self.pending_recalls:
self.pending_recalls[scope] = [] self.pending_recalls[scope] = []
@@ -37,27 +57,40 @@ class MessageService:
except Exception as e: except Exception as e:
return None return None
async def recall_previous_of_type(self, bot: Bot, scope: str, message_type: str):
"""Recall the previous message of a specific type in a scope."""
if scope in self.last_messages and message_type in self.last_messages[scope]:
old_message_id = self.last_messages[scope][message_type]
try:
await bot.delete_msg(message_id=old_message_id)
except Exception:
pass
del self.last_messages[scope][message_type]
async def _schedule_recall( async def _schedule_recall(
self, self,
bot: Bot,
scope: str, scope: str,
message_id: str, message_id: str,
delay: int, delay: int,
recall_func: Callable,
): ):
"""Schedule message recall.""" """Schedule message recall after a delay."""
try: try:
await asyncio.sleep(delay) await asyncio.sleep(delay)
await recall_func(message_id) await bot.delete_msg(message_id=message_id)
except Exception: except Exception:
pass pass
def clear_pending_recalls(self, scope: str): def clear_pending_recalls(self, scope: str):
"""Cancel all pending recall tasks for a scope.""" """Cancel all pending recall tasks for a scope and clear last messages."""
if scope in self.pending_recalls: if scope in self.pending_recalls:
for task in self.pending_recalls[scope]: for task in self.pending_recalls[scope]:
if not task.done(): if not task.done():
task.cancel() task.cancel()
del self.pending_recalls[scope] del self.pending_recalls[scope]
if scope in self.last_messages:
del self.last_messages[scope]
def clear_all_recalls(self): def clear_all_recalls(self):
"""Cancel all pending recall tasks.""" """Cancel all pending recall tasks."""

View File

@@ -195,6 +195,12 @@ class _NoopMessageService:
def clear_pending_recalls(self, scope: str): def clear_pending_recalls(self, scope: str):
return return
async def send_with_recall(self, bot, scope, message_type, message):
return "fake_msg_id"
async def recall_previous_of_type(self, bot, scope, message_type):
return
@test_simulate_race_cmd.handle() @test_simulate_race_cmd.handle()
async def handle_test_simulate_race(bot: Bot, event: Event): async def handle_test_simulate_race(bot: Bot, event: Event):
@@ -240,7 +246,7 @@ async def handle_test_simulate_race(bot: Bot, event: Event):
progress_count = 0 progress_count = 0
max_progress = 30 max_progress = 30
async def _test_send_to_scope(_bot: Bot, _scope: str, message: str): async def _test_send_to_scope(_bot: Bot, _scope: str, message: str, *args, **kwargs):
nonlocal progress_count nonlocal progress_count
await fake_bot.send_msg(message_type="private", user_id=event.user_id, message=message) await fake_bot.send_msg(message_type="private", user_id=event.user_id, message=message)
if not stream_progress: if not stream_progress:
@@ -250,7 +256,7 @@ async def handle_test_simulate_race(bot: Bot, event: Event):
progress_count += 1 progress_count += 1
if progress_count > max_progress: if progress_count > max_progress:
return return
await original_send_to_scope(bot, scope, message) await original_send_to_scope(bot, scope, message, *args, **kwargs)
commands_mod._send_to_scope = _test_send_to_scope commands_mod._send_to_scope = _test_send_to_scope