chore: sync current WechatHookBot workspace
This commit is contained in:
3
plugins/SpeechLeaderboard/__init__.py
Normal file
3
plugins/SpeechLeaderboard/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .main import SpeechLeaderboard
|
||||
|
||||
__all__ = ["SpeechLeaderboard"]
|
||||
44
plugins/SpeechLeaderboard/config.toml
Normal file
44
plugins/SpeechLeaderboard/config.toml
Normal file
@@ -0,0 +1,44 @@
|
||||
# 发言榜插件配置
|
||||
|
||||
[behavior]
|
||||
# 是否启用插件
|
||||
enabled = true
|
||||
# 管理员 wxid 列表(为空则使用 main_config.toml 的 Bot.admins)
|
||||
admins = []
|
||||
# 测试指令(仅管理员可用)
|
||||
test_keywords = ["/发言榜"]
|
||||
# 启用的群聊列表(为空则对所有群生效)
|
||||
enabled_groups = []
|
||||
# 禁用的群聊列表
|
||||
disabled_groups = []
|
||||
# 昨日发言人数不足该值则不触发
|
||||
min_speakers = 5
|
||||
# 排行榜展示人数
|
||||
top_n = 20
|
||||
# 每多少句发言 +1 积分
|
||||
messages_per_point = 5
|
||||
# 龙王额外奖励积分
|
||||
dragon_bonus = 5
|
||||
# 群消息间隔(秒)
|
||||
group_interval = 1.5
|
||||
# 是否排除机器人自己的发言
|
||||
exclude_bot = true
|
||||
|
||||
[schedule]
|
||||
# 是否启用定时任务
|
||||
enabled = true
|
||||
# 每天 09:30 触发
|
||||
hour = 9
|
||||
minute = 30
|
||||
|
||||
[render]
|
||||
# 是否使用 HTML 渲染(推荐)
|
||||
use_html = true
|
||||
# 是否发送后清理图片文件
|
||||
cleanup_image = true
|
||||
# HTML 渲染超时(秒)
|
||||
render_timeout = 20
|
||||
# 背景来源: local / api
|
||||
bg_source = "local"
|
||||
# 背景 API(bg_source=api 时生效)
|
||||
bg_api_url = ""
|
||||
541
plugins/SpeechLeaderboard/main.py
Normal file
541
plugins/SpeechLeaderboard/main.py
Normal file
@@ -0,0 +1,541 @@
|
||||
"""
|
||||
发言榜插件
|
||||
|
||||
每天早上自动统计昨日群聊发言排行榜,并发放积分奖励。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import tomllib
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import pymysql
|
||||
import aiosqlite
|
||||
from loguru import logger
|
||||
|
||||
from utils.plugin_base import PluginBase
|
||||
from utils.decorators import schedule, on_text_message
|
||||
|
||||
# HTML 渲染器(延迟导入)
|
||||
HTML_RENDERER_AVAILABLE = False
|
||||
try:
|
||||
from plugins.SignInPlugin.html_renderer import HtmlRenderer
|
||||
HTML_RENDERER_AVAILABLE = True
|
||||
except Exception:
|
||||
logger.warning("SpeechLeaderboard: HTML 渲染器导入失败,将无法生成图片")
|
||||
|
||||
|
||||
class SpeechLeaderboard(PluginBase):
|
||||
"""发言榜插件"""
|
||||
|
||||
description = "发言榜 - 每日统计群聊发言排行并发放积分"
|
||||
author = "Assistant"
|
||||
version = "1.0.0"
|
||||
dependencies = ["MessageLogger", "SignInPlugin"]
|
||||
load_priority = 55
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.config = None
|
||||
self.db_config = None
|
||||
self.member_sync_db_path = None
|
||||
self.html_renderer = None
|
||||
self.templates_dir = Path(__file__).parent / "templates"
|
||||
self.temp_dir = Path(__file__).parent / "temp"
|
||||
self.images_dir = Path(__file__).parent / "images"
|
||||
|
||||
async def async_init(self):
|
||||
config_path = Path(__file__).parent / "config.toml"
|
||||
with open(config_path, "rb") as f:
|
||||
self.config = tomllib.load(f)
|
||||
|
||||
self.templates_dir.mkdir(exist_ok=True)
|
||||
self.temp_dir.mkdir(exist_ok=True)
|
||||
self.images_dir.mkdir(exist_ok=True)
|
||||
|
||||
render_cfg = self.config.get("render", {})
|
||||
use_html = bool(render_cfg.get("use_html", True))
|
||||
if use_html and HTML_RENDERER_AVAILABLE:
|
||||
self.html_renderer = HtmlRenderer(
|
||||
template_dir=self.templates_dir,
|
||||
output_dir=self.temp_dir,
|
||||
images_dir=self.images_dir,
|
||||
bg_source=render_cfg.get("bg_source", "local"),
|
||||
bg_api_url=render_cfg.get("bg_api_url", ""),
|
||||
)
|
||||
logger.info("SpeechLeaderboard: HTML 渲染已启用")
|
||||
else:
|
||||
self.html_renderer = None
|
||||
logger.warning("SpeechLeaderboard: HTML 渲染不可用,将使用文本")
|
||||
|
||||
self._resolve_member_sync_db_path()
|
||||
self._load_message_logger_config()
|
||||
logger.success("SpeechLeaderboard 插件初始化完成")
|
||||
|
||||
def _load_message_logger_config(self):
|
||||
try:
|
||||
config_path = Path(__file__).parent.parent / "MessageLogger" / "config.toml"
|
||||
if not config_path.exists():
|
||||
return
|
||||
with open(config_path, "rb") as f:
|
||||
cfg = tomllib.load(f)
|
||||
self.db_config = cfg.get("database") or {}
|
||||
except Exception as e:
|
||||
logger.warning(f"SpeechLeaderboard: 读取 MessageLogger 配置失败: {e}")
|
||||
|
||||
def _get_admins(self) -> List[str]:
|
||||
admins = self.config.get("behavior", {}).get("admins", []) if self.config else []
|
||||
admins = [a for a in admins if isinstance(a, str) and a]
|
||||
if admins:
|
||||
return admins
|
||||
try:
|
||||
main_config_path = Path(__file__).parent.parent.parent / "main_config.toml"
|
||||
with open(main_config_path, "rb") as f:
|
||||
cfg = tomllib.load(f)
|
||||
bot_cfg = cfg.get("Bot", {})
|
||||
admins = bot_cfg.get("admins", []) or []
|
||||
return [a for a in admins if isinstance(a, str) and a]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _resolve_member_sync_db_path(self):
|
||||
plugins_dir = Path(__file__).parent.parent
|
||||
config_path = plugins_dir / "MemberSync" / "config.toml"
|
||||
if not config_path.exists():
|
||||
return
|
||||
try:
|
||||
with open(config_path, "rb") as f:
|
||||
cfg = tomllib.load(f)
|
||||
db_rel = str(cfg.get("database", {}).get("db_path", "")).strip()
|
||||
if not db_rel:
|
||||
return
|
||||
db_path = plugins_dir / "MemberSync" / db_rel
|
||||
if db_path.exists():
|
||||
self.member_sync_db_path = db_path
|
||||
except Exception as e:
|
||||
logger.debug(f"SpeechLeaderboard: 解析 MemberSync 配置失败: {e}")
|
||||
|
||||
def _get_db_connection(self):
|
||||
msg_logger = self.get_plugin("MessageLogger")
|
||||
if msg_logger and hasattr(msg_logger, "get_db_connection"):
|
||||
try:
|
||||
return msg_logger.get_db_connection()
|
||||
except Exception:
|
||||
pass
|
||||
if not self.db_config:
|
||||
return None
|
||||
return pymysql.connect(
|
||||
host=self.db_config.get("host"),
|
||||
port=self.db_config.get("port"),
|
||||
user=self.db_config.get("user"),
|
||||
password=self.db_config.get("password"),
|
||||
database=self.db_config.get("database"),
|
||||
charset=self.db_config.get("charset", "utf8mb4"),
|
||||
autocommit=True,
|
||||
)
|
||||
|
||||
def _get_bot_wxid(self) -> str:
|
||||
try:
|
||||
main_config_path = Path(__file__).parent.parent.parent / "main_config.toml"
|
||||
with open(main_config_path, "rb") as f:
|
||||
cfg = tomllib.load(f)
|
||||
bot_cfg = cfg.get("Bot", {})
|
||||
return str(bot_cfg.get("wxid", "")).strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _extract_chatroom_wxid(self, chatroom_entry) -> str:
|
||||
if isinstance(chatroom_entry, str):
|
||||
return chatroom_entry
|
||||
if not isinstance(chatroom_entry, dict):
|
||||
return ""
|
||||
contact = chatroom_entry.get("contact", chatroom_entry)
|
||||
username = contact.get("userName", "")
|
||||
if isinstance(username, dict):
|
||||
return username.get("String", "")
|
||||
return str(username) if username else ""
|
||||
|
||||
async def _get_groups_from_member_sync(self) -> List[str]:
|
||||
if not self.member_sync_db_path or not self.member_sync_db_path.exists():
|
||||
return []
|
||||
try:
|
||||
async with aiosqlite.connect(self.member_sync_db_path) as db:
|
||||
cursor = await db.execute(
|
||||
"SELECT DISTINCT chatroom_wxid FROM group_members WHERE chatroom_wxid != ''"
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [row[0] for row in rows if row and row[0]]
|
||||
except Exception as e:
|
||||
logger.debug(f"SpeechLeaderboard: 查询 MemberSync 群列表失败: {e}")
|
||||
return []
|
||||
|
||||
async def _get_groups_from_messages(self) -> List[str]:
|
||||
conn = self._get_db_connection()
|
||||
if not conn:
|
||||
return []
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT DISTINCT group_id FROM messages WHERE is_group = 1 AND group_id IS NOT NULL AND group_id != ''"
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
return [row[0] for row in rows if row and row[0]]
|
||||
except Exception as e:
|
||||
logger.debug(f"SpeechLeaderboard: 查询消息表群列表失败: {e}")
|
||||
return []
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
async def _get_target_groups(self, bot) -> List[str]:
|
||||
enabled_groups = self.config.get("behavior", {}).get("enabled_groups", [])
|
||||
disabled_groups = set(self.config.get("behavior", {}).get("disabled_groups", []))
|
||||
|
||||
groups = [g for g in enabled_groups if isinstance(g, str) and g]
|
||||
if not groups:
|
||||
groups = await self._get_groups_from_member_sync()
|
||||
if not groups:
|
||||
groups = await self._get_groups_from_messages()
|
||||
if not groups and bot:
|
||||
try:
|
||||
chatrooms = await bot.get_chatroom_list(force_refresh=True)
|
||||
for item in chatrooms:
|
||||
wxid = self._extract_chatroom_wxid(item)
|
||||
if wxid and wxid.endswith("@chatroom"):
|
||||
groups.append(wxid)
|
||||
except Exception as e:
|
||||
logger.debug(f"SpeechLeaderboard: 获取群聊列表失败: {e}")
|
||||
|
||||
groups = [g for g in groups if g and g not in disabled_groups]
|
||||
groups = list(dict.fromkeys(groups))
|
||||
return groups
|
||||
|
||||
def _get_yesterday_range(self) -> Tuple[datetime, datetime, str]:
|
||||
today = datetime.now().date()
|
||||
yesterday = today - timedelta(days=1)
|
||||
start_dt = datetime.combine(yesterday, datetime.min.time())
|
||||
end_dt = datetime.combine(today, datetime.min.time())
|
||||
date_label = yesterday.strftime("%Y年%m月%d日")
|
||||
return start_dt, end_dt, date_label
|
||||
|
||||
async def _fetch_group_stats(self, group_id: str, start_dt: datetime, end_dt: datetime) -> List[Dict]:
|
||||
conn = self._get_db_connection()
|
||||
if not conn:
|
||||
return []
|
||||
|
||||
exclude_bot = bool(self.config.get("behavior", {}).get("exclude_bot", True))
|
||||
bot_wxid = self._get_bot_wxid() if exclude_bot else ""
|
||||
|
||||
try:
|
||||
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||
sql = (
|
||||
"SELECT sender_wxid, MAX(nickname) AS nickname, MAX(avatar_url) AS avatar_url, "
|
||||
"COUNT(*) AS msg_count "
|
||||
"FROM messages "
|
||||
"WHERE is_group = 1 AND group_id = %s AND create_time >= %s AND create_time < %s "
|
||||
"AND sender_wxid IS NOT NULL AND sender_wxid != ''"
|
||||
)
|
||||
params = [group_id, start_dt, end_dt]
|
||||
if bot_wxid:
|
||||
sql += " AND sender_wxid != %s"
|
||||
params.append(bot_wxid)
|
||||
sql += " GROUP BY sender_wxid"
|
||||
cursor.execute(sql, params)
|
||||
rows = cursor.fetchall()
|
||||
return rows or []
|
||||
except Exception as e:
|
||||
logger.error(f"SpeechLeaderboard: 查询群 {group_id} 发言统计失败: {e}")
|
||||
return []
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _calc_points(self, msg_count: int, rank: int) -> int:
|
||||
per = int(self.config.get("behavior", {}).get("messages_per_point", 5))
|
||||
bonus = int(self.config.get("behavior", {}).get("dragon_bonus", 5))
|
||||
base_points = msg_count // max(1, per)
|
||||
if rank == 1:
|
||||
return base_points + bonus
|
||||
return base_points
|
||||
|
||||
def _build_html(self, data: Dict) -> str:
|
||||
leaderboard = data.get("leaderboard", [])
|
||||
total_speakers = data.get("total_speakers", 0)
|
||||
total_messages = data.get("total_messages", 0)
|
||||
date_label = data.get("date", "")
|
||||
|
||||
rows_html = ""
|
||||
medals = ["🐉", "🥈", "🥉"]
|
||||
|
||||
for i, user in enumerate(leaderboard):
|
||||
rank = i + 1
|
||||
nickname = user.get("nickname") or "未知用户"
|
||||
msg_count = user.get("msg_count", 0)
|
||||
points = user.get("points", 0)
|
||||
avatar_url = user.get("avatar_url", "")
|
||||
|
||||
if len(nickname) > 10:
|
||||
nickname = nickname[:9] + "…"
|
||||
|
||||
if rank <= 3:
|
||||
rank_html = f'<span class="medal">{medals[rank-1]}</span>'
|
||||
row_class = f"top{rank}"
|
||||
else:
|
||||
rank_html = f'<span class="rank-num">{rank}</span>'
|
||||
row_class = ""
|
||||
|
||||
if avatar_url:
|
||||
avatar_html = f'<img class="row-avatar" src="{avatar_url}" alt="">'
|
||||
else:
|
||||
avatar_html = '<div class="row-avatar-placeholder">👤</div>'
|
||||
|
||||
title_tag = "<span class=\"tag\">龙王</span>" if rank == 1 else ""
|
||||
|
||||
rows_html += f'''
|
||||
<div class="row {row_class}">
|
||||
<div class="rank">{rank_html}</div>
|
||||
{avatar_html}
|
||||
<div class="info">
|
||||
<div class="name">{nickname} {title_tag}</div>
|
||||
<div class="stats">发言 {msg_count} | +{points} 分</div>
|
||||
</div>
|
||||
<div class="count">{msg_count}</div>
|
||||
</div>'''
|
||||
|
||||
return f'''<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: "Microsoft YaHei", sans-serif; }}
|
||||
.container {{ width: 540px; background: #fff; }}
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%);
|
||||
padding: 22px 20px; text-align: center; color: #4a2c2c;
|
||||
}}
|
||||
.header h1 {{ font-size: 26px; margin-bottom: 6px; }}
|
||||
.header p {{ font-size: 13px; color: #6b4a4a; }}
|
||||
.summary {{
|
||||
display: flex; justify-content: space-between;
|
||||
padding: 12px 18px; font-size: 12px; color: #666;
|
||||
background: #faf7f7; border-bottom: 1px solid #f0e9e9;
|
||||
}}
|
||||
.list {{ padding: 10px 15px; }}
|
||||
.row {{
|
||||
display: flex; align-items: center;
|
||||
padding: 12px 10px; border-bottom: 1px solid #f0f0f0;
|
||||
}}
|
||||
.row.top1 {{ background: linear-gradient(90deg, #fff3d6 0%, #fff 100%); }}
|
||||
.row.top2 {{ background: linear-gradient(90deg, #f5f5f5 0%, #fff 100%); }}
|
||||
.row.top3 {{ background: linear-gradient(90deg, #fff5f0 0%, #fff 100%); }}
|
||||
.rank {{ width: 40px; text-align: center; }}
|
||||
.medal {{ font-size: 22px; }}
|
||||
.rank-num {{ font-size: 16px; font-weight: bold; color: #999; }}
|
||||
.row-avatar, .row-avatar-placeholder {{
|
||||
width: 45px; height: 45px; border-radius: 50%;
|
||||
margin: 0 12px; object-fit: cover;
|
||||
}}
|
||||
.row-avatar-placeholder {{
|
||||
background: #e0e0e0; display: flex;
|
||||
align-items: center; justify-content: center; font-size: 20px;
|
||||
}}
|
||||
.info {{ flex: 1; }}
|
||||
.name {{ font-size: 16px; font-weight: 500; color: #333; }}
|
||||
.tag {{
|
||||
display: inline-block; margin-left: 6px; padding: 2px 6px;
|
||||
font-size: 11px; color: #fff; background: #ff6b6b; border-radius: 10px;
|
||||
}}
|
||||
.stats {{ font-size: 12px; color: #999; margin-top: 2px; }}
|
||||
.count {{
|
||||
font-size: 18px; font-weight: bold; color: #ff6b6b; min-width: 60px; text-align: right;
|
||||
}}
|
||||
.footer {{
|
||||
text-align: center; padding: 12px;
|
||||
color: #999; font-size: 12px; border-top: 1px solid #f0f0f0;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" id="card">
|
||||
<div class="header">
|
||||
<h1>📣 昨日发言榜</h1>
|
||||
<p>{date_label}</p>
|
||||
</div>
|
||||
<div class="summary">
|
||||
<span>发言人数:{total_speakers}</span>
|
||||
<span>总消息:{total_messages}</span>
|
||||
<span>龙王奖励:+{self.config.get("behavior", {}).get("dragon_bonus", 5)}分</span>
|
||||
</div>
|
||||
<div class="list">{rows_html}</div>
|
||||
<div class="footer">每日统计 | WechatHookBot</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
async def _render_image(self, data: Dict) -> str:
|
||||
if not self.html_renderer:
|
||||
return ""
|
||||
html = self._build_html(data)
|
||||
try:
|
||||
timeout = int(self.config.get("render", {}).get("render_timeout", 20))
|
||||
return await asyncio.wait_for(
|
||||
self.html_renderer._render_html(html, "speech_leaderboard"),
|
||||
timeout=timeout,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("SpeechLeaderboard: HTML 渲染超时")
|
||||
except Exception as e:
|
||||
logger.error(f"SpeechLeaderboard: HTML 渲染失败: {e}")
|
||||
return ""
|
||||
|
||||
async def _send_text_fallback(self, bot, group_id: str, data: Dict):
|
||||
lines = ["📣 昨日发言榜", f"日期:{data.get('date', '')}", "─" * 14]
|
||||
for i, user in enumerate(data.get("leaderboard", []), 1):
|
||||
nickname = user.get("nickname") or "未知用户"
|
||||
msg_count = user.get("msg_count", 0)
|
||||
points = user.get("points", 0)
|
||||
flag = "(龙王)" if i == 1 else ""
|
||||
lines.append(f"{i}. {nickname} {flag} - {msg_count}句 +{points}分")
|
||||
lines.append(f"发言人数:{data.get('total_speakers', 0)} | 总消息:{data.get('total_messages', 0)}")
|
||||
await bot.send_text(group_id, "\n".join(lines))
|
||||
|
||||
async def _award_points(self, stats: List[Dict], date_key: str):
|
||||
signin = self.get_plugin("SignInPlugin")
|
||||
if not signin:
|
||||
logger.warning("SpeechLeaderboard: 未找到 SignInPlugin,无法发放积分")
|
||||
return
|
||||
|
||||
sorted_stats = sorted(stats, key=lambda x: x.get("msg_count", 0), reverse=True)
|
||||
for i, row in enumerate(sorted_stats, 1):
|
||||
wxid = row.get("sender_wxid", "")
|
||||
if not wxid:
|
||||
continue
|
||||
nickname = row.get("nickname") or ""
|
||||
msg_count = int(row.get("msg_count", 0))
|
||||
points = self._calc_points(msg_count, i)
|
||||
if points <= 0:
|
||||
continue
|
||||
|
||||
try:
|
||||
signin.create_or_update_user(wxid, nickname)
|
||||
desc = f"昨日发言 {msg_count} 句,奖励 {points} 积分"
|
||||
if i == 1:
|
||||
desc = f"龙王奖励:昨日发言 {msg_count} 句,奖励 {points} 积分"
|
||||
signin.add_points(
|
||||
wxid,
|
||||
points,
|
||||
change_type="speech_rank",
|
||||
description=desc,
|
||||
related_id=date_key,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"SpeechLeaderboard: 发放积分失败 {wxid}: {e}")
|
||||
|
||||
async def _process_group(self, bot, group_id: str, start_dt: datetime, end_dt: datetime, date_label: str, *, award_points: bool = True):
|
||||
stats = await self._fetch_group_stats(group_id, start_dt, end_dt)
|
||||
min_speakers = int(self.config.get("behavior", {}).get("min_speakers", 5))
|
||||
if len(stats) < min_speakers:
|
||||
logger.info(f"SpeechLeaderboard: 群 {group_id} 昨日发言人数不足 ({len(stats)} < {min_speakers})")
|
||||
return
|
||||
|
||||
stats_sorted = sorted(stats, key=lambda x: x.get("msg_count", 0), reverse=True)
|
||||
total_messages = sum(int(r.get("msg_count", 0)) for r in stats_sorted)
|
||||
top_n = int(self.config.get("behavior", {}).get("top_n", 20))
|
||||
top_list = []
|
||||
for i, row in enumerate(stats_sorted[:top_n], 1):
|
||||
msg_count = int(row.get("msg_count", 0))
|
||||
top_list.append({
|
||||
"rank": i,
|
||||
"sender_wxid": row.get("sender_wxid"),
|
||||
"nickname": row.get("nickname") or "未知用户",
|
||||
"avatar_url": row.get("avatar_url") or "",
|
||||
"msg_count": msg_count,
|
||||
"points": self._calc_points(msg_count, i),
|
||||
})
|
||||
|
||||
date_key = (start_dt.date()).strftime("%Y-%m-%d")
|
||||
if award_points:
|
||||
await self._award_points(stats_sorted, date_key)
|
||||
|
||||
data = {
|
||||
"date": date_label,
|
||||
"total_speakers": len(stats_sorted),
|
||||
"total_messages": total_messages,
|
||||
"leaderboard": top_list,
|
||||
}
|
||||
|
||||
image_path = await self._render_image(data)
|
||||
if image_path:
|
||||
await bot.send_image(group_id, image_path)
|
||||
if self.config.get("render", {}).get("cleanup_image", True):
|
||||
try:
|
||||
Path(image_path).unlink()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
await self._send_text_fallback(bot, group_id, data)
|
||||
|
||||
@schedule("cron", hour=9, minute=30)
|
||||
async def scheduled_report(self, bot=None):
|
||||
if not self.config or not self.config.get("behavior", {}).get("enabled", True):
|
||||
return
|
||||
if not self.config.get("schedule", {}).get("enabled", True):
|
||||
return
|
||||
|
||||
if not bot:
|
||||
bot = self.get_bot()
|
||||
if not bot:
|
||||
logger.error("SpeechLeaderboard: 无法获取 bot 实例")
|
||||
return
|
||||
|
||||
start_dt, end_dt, date_label = self._get_yesterday_range()
|
||||
groups = await self._get_target_groups(bot)
|
||||
if not groups:
|
||||
logger.warning("SpeechLeaderboard: 未找到可统计的群聊")
|
||||
return
|
||||
|
||||
logger.info(f"SpeechLeaderboard: 开始统计昨日发言榜,群数={len(groups)}")
|
||||
group_interval = float(self.config.get("behavior", {}).get("group_interval", 1.5))
|
||||
|
||||
for group_id in groups:
|
||||
try:
|
||||
await self._process_group(bot, group_id, start_dt, end_dt, date_label)
|
||||
await asyncio.sleep(group_interval)
|
||||
except Exception as e:
|
||||
logger.error(f"SpeechLeaderboard: 群 {group_id} 处理失败: {e}")
|
||||
|
||||
logger.info("SpeechLeaderboard: 昨日发言榜任务完成")
|
||||
|
||||
@on_text_message(priority=50)
|
||||
async def handle_manual_report(self, bot, message: dict):
|
||||
if not self.config or not self.config.get("behavior", {}).get("enabled", True):
|
||||
return
|
||||
|
||||
content = str(message.get("Content", "")).strip()
|
||||
keywords = self.config.get("behavior", {}).get("test_keywords", ["/发言榜"])
|
||||
if content not in keywords:
|
||||
return True
|
||||
|
||||
is_group = bool(message.get("IsGroup", False))
|
||||
from_wxid = message.get("FromWxid", "")
|
||||
sender_wxid = message.get("SenderWxid", "")
|
||||
|
||||
if not is_group:
|
||||
await bot.send_text(from_wxid, "❌ 请在群聊中使用该指令")
|
||||
return False
|
||||
|
||||
admins = self._get_admins()
|
||||
operator_wxid = sender_wxid if is_group else from_wxid
|
||||
if admins and operator_wxid not in admins:
|
||||
await bot.send_text(from_wxid, "❌ 权限不足,只有管理员可用")
|
||||
return False
|
||||
|
||||
await bot.send_text(from_wxid, "📣 正在生成昨日发言榜,请稍候...")
|
||||
|
||||
start_dt, end_dt, date_label = self._get_yesterday_range()
|
||||
try:
|
||||
await self._process_group(bot, from_wxid, start_dt, end_dt, date_label, award_points=False)
|
||||
except Exception as e:
|
||||
logger.error(f"SpeechLeaderboard: 手动生成失败: {e}")
|
||||
await bot.send_text(from_wxid, f"❌ 生成失败: {e}")
|
||||
return False
|
||||
Reference in New Issue
Block a user