扩展 value_rank 周报能力并新增周报命令
- 新增命令 身价周报,并接入配置与帮助文案 - 新增每周定时动作 value_rank_weekly_report_push(周一09:35)自动推送周报 - 周报内容包含:综合排行Top5、上升榜Top5、下行榜Top5(对比7天前) - 扩展 ValueRankDB:新增按日期读取快照分数字典能力,支持周报对比计算 - 调度执行中支持周报推送并补充重算保障,确保周报数据为当天最新
This commit is contained in:
@@ -1,14 +1,15 @@
|
|||||||
[ValueRank]
|
[ValueRank]
|
||||||
enable = true
|
enable = true
|
||||||
command = ["我的身价", "身价排行", "社交热度榜", "搭子榜", "身价说明", "重算身价"]
|
command = ["我的身价", "身价排行", "社交热度榜", "搭子榜", "身价周报", "身价说明", "重算身价"]
|
||||||
command-format = """
|
command-format = """
|
||||||
📊 身价系统命令:
|
📊 身价系统命令:
|
||||||
1. 我的身价
|
1. 我的身价
|
||||||
2. 身价排行 [名次]
|
2. 身价排行 [名次]
|
||||||
3. 社交热度榜 [名次]
|
3. 社交热度榜 [名次]
|
||||||
4. 搭子榜 [名次]
|
4. 搭子榜 [名次]
|
||||||
5. 身价说明
|
5. 身价周报
|
||||||
6. 重算身价(管理员)
|
6. 身价说明
|
||||||
|
7. 重算身价(管理员)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 统计窗口(天)
|
# 统计窗口(天)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -251,6 +251,23 @@ class ValueRankDB(BaseDBOperator):
|
|||||||
"""
|
"""
|
||||||
return self.execute_query(sql, (group_id, social_window_days, limit)) or []
|
return self.execute_query(sql, (group_id, social_window_days, limit)) or []
|
||||||
|
|
||||||
|
def get_snapshot_score_map(self, stat_date: str, group_id: str) -> Dict[str, float]:
|
||||||
|
"""读取某天群内所有用户分数字典:{user_id: score}。"""
|
||||||
|
sql = """
|
||||||
|
SELECT user_id, score
|
||||||
|
FROM t_value_rank_snapshot
|
||||||
|
WHERE stat_date = %s
|
||||||
|
AND group_id = %s
|
||||||
|
"""
|
||||||
|
rows = self.execute_query(sql, (stat_date, group_id)) or []
|
||||||
|
result: Dict[str, float] = {}
|
||||||
|
for row in rows:
|
||||||
|
user_id = str(row.get("user_id") or "").strip()
|
||||||
|
if not user_id:
|
||||||
|
continue
|
||||||
|
result[user_id] = float(row.get("score") or 0.0)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class ValueRankPlugin(MessagePluginInterface):
|
class ValueRankPlugin(MessagePluginInterface):
|
||||||
"""群成员身价排行插件。
|
"""群成员身价排行插件。
|
||||||
@@ -303,7 +320,7 @@ class ValueRankPlugin(MessagePluginInterface):
|
|||||||
|
|
||||||
# 配置默认值:即使未配置 config.toml,也能以保守参数运行。
|
# 配置默认值:即使未配置 config.toml,也能以保守参数运行。
|
||||||
self.enable = True
|
self.enable = True
|
||||||
self._commands = ["我的身价", "身价排行", "社交热度榜", "搭子榜", "身价说明", "重算身价"]
|
self._commands = ["我的身价", "身价排行", "社交热度榜", "搭子榜", "身价周报", "身价说明", "重算身价"]
|
||||||
self.command_format = "我的身价 | 身价排行 [名次] | 身价说明 | 重算身价"
|
self.command_format = "我的身价 | 身价排行 [名次] | 身价说明 | 重算身价"
|
||||||
|
|
||||||
self.message_window_days = 7
|
self.message_window_days = 7
|
||||||
@@ -416,6 +433,11 @@ class ValueRankPlugin(MessagePluginInterface):
|
|||||||
await bot.send_text_message(roomid, text, sender)
|
await bot.send_text_message(roomid, text, sender)
|
||||||
return True, "查询成功"
|
return True, "查询成功"
|
||||||
|
|
||||||
|
if command == "身价周报":
|
||||||
|
text = await self._build_weekly_report_text(roomid)
|
||||||
|
await bot.send_text_message(roomid, text, sender)
|
||||||
|
return True, "查询成功"
|
||||||
|
|
||||||
if command == "身价说明":
|
if command == "身价说明":
|
||||||
await bot.send_text_message(roomid, self._build_explain_text(), sender)
|
await bot.send_text_message(roomid, self._build_explain_text(), sender)
|
||||||
return True, "查询成功"
|
return True, "查询成功"
|
||||||
@@ -435,7 +457,7 @@ class ValueRankPlugin(MessagePluginInterface):
|
|||||||
return True, "命令错误"
|
return True, "命令错误"
|
||||||
|
|
||||||
def get_schedule_actions(self) -> List[Dict[str, Any]]:
|
def get_schedule_actions(self) -> List[Dict[str, Any]]:
|
||||||
"""声明可调度动作:每日凌晨全量重算。"""
|
"""声明可调度动作:每日重算 + 每周周报。"""
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"action_key": "value_rank_daily_recompute",
|
"action_key": "value_rank_daily_recompute",
|
||||||
@@ -447,15 +469,26 @@ class ValueRankPlugin(MessagePluginInterface):
|
|||||||
"target_config": {},
|
"target_config": {},
|
||||||
"payload": {},
|
"payload": {},
|
||||||
"default_enabled": True,
|
"default_enabled": True,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"action_key": "value_rank_weekly_report_push",
|
||||||
|
"name": "身价周报推送",
|
||||||
|
"description": "每周推送身价周报(上升榜/下降榜/综合排行)",
|
||||||
|
"trigger_type": "every_week_time",
|
||||||
|
# weekday 取值与 datetime.weekday() 一致:周一=0 ... 周日=6。
|
||||||
|
"trigger_config": {"weekday": 0, "time_str": "09:35"},
|
||||||
|
"target_scope": "all_enabled_groups",
|
||||||
|
"target_config": {},
|
||||||
|
"payload": {},
|
||||||
|
"default_enabled": True,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
async def run_scheduled_action(self, action_key: str, context: Dict[str, Any]) -> Dict[str, Any]:
|
async def run_scheduled_action(self, action_key: str, context: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""执行调度动作。"""
|
"""执行调度动作。"""
|
||||||
if action_key != "value_rank_daily_recompute":
|
if action_key not in {"value_rank_daily_recompute", "value_rank_weekly_report_push"}:
|
||||||
return {"success": False, "summary": f"不支持动作: {action_key}", "detail": {}}
|
return {"success": False, "summary": f"不支持动作: {action_key}", "detail": {}}
|
||||||
|
|
||||||
stat_date = datetime.now().strftime("%Y-%m-%d")
|
|
||||||
target_groups = [str(g).strip() for g in (context.get("target_groups") or []) if str(g).strip()]
|
target_groups = [str(g).strip() for g in (context.get("target_groups") or []) if str(g).strip()]
|
||||||
if not target_groups:
|
if not target_groups:
|
||||||
target_groups = [
|
target_groups = [
|
||||||
@@ -466,14 +499,39 @@ class ValueRankPlugin(MessagePluginInterface):
|
|||||||
success_groups: List[str] = []
|
success_groups: List[str] = []
|
||||||
failed_groups: Dict[str, str] = {}
|
failed_groups: Dict[str, str] = {}
|
||||||
updated_users = 0
|
updated_users = 0
|
||||||
|
pushed_groups: List[str] = []
|
||||||
|
|
||||||
|
stat_date = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
bot = context.get("bot") or getattr(self, "bot", None)
|
||||||
|
|
||||||
|
# 周报任务先确保当日快照存在,再执行推送,避免“有报表命令但无数据”。
|
||||||
|
if action_key == "value_rank_weekly_report_push" and not bot:
|
||||||
|
return {"success": False, "summary": "周报推送失败:bot 未注入", "detail": {}}
|
||||||
|
|
||||||
for gid in target_groups:
|
for gid in target_groups:
|
||||||
try:
|
try:
|
||||||
|
# 每个群都先重算一次,确保报告与排行数据是“当天最新口径”。
|
||||||
updated_users += self._recompute_group_snapshot(gid, stat_date)
|
updated_users += self._recompute_group_snapshot(gid, stat_date)
|
||||||
|
if action_key == "value_rank_weekly_report_push":
|
||||||
|
weekly_text = await self._build_weekly_report_text(gid)
|
||||||
|
await bot.send_text_message(gid, weekly_text, "")
|
||||||
|
pushed_groups.append(gid)
|
||||||
success_groups.append(gid)
|
success_groups.append(gid)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failed_groups[gid] = str(e)
|
failed_groups[gid] = str(e)
|
||||||
|
|
||||||
|
if action_key == "value_rank_weekly_report_push":
|
||||||
|
return {
|
||||||
|
"success": len(failed_groups) == 0,
|
||||||
|
"summary": f"身价周报完成:推送{len(pushed_groups)}群,失败{len(failed_groups)}群",
|
||||||
|
"detail": {
|
||||||
|
"stat_date": stat_date,
|
||||||
|
"updated_users": updated_users,
|
||||||
|
"pushed_groups": pushed_groups,
|
||||||
|
"failed_groups": failed_groups,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": len(failed_groups) == 0,
|
"success": len(failed_groups) == 0,
|
||||||
"summary": f"身价重算完成:成功{len(success_groups)}群,失败{len(failed_groups)}群",
|
"summary": f"身价重算完成:成功{len(success_groups)}群,失败{len(failed_groups)}群",
|
||||||
@@ -740,6 +798,72 @@ class ValueRankPlugin(MessagePluginInterface):
|
|||||||
lines.append(f"{medal} {nick_a} × {nick_b} | 互动{score:.1f} | @次数{mention_count}")
|
lines.append(f"{medal} {nick_a} × {nick_b} | 互动{score:.1f} | @次数{mention_count}")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
async def _build_weekly_report_text(self, group_id: str) -> str:
|
||||||
|
"""构建“身价周报”文本。
|
||||||
|
|
||||||
|
周报口径:
|
||||||
|
1. 综合排行:取今日 Top5;
|
||||||
|
2. 上升榜/下降榜:对比“今日分数 - 7天前分数”;
|
||||||
|
3. 若7天前无数据则按 0 处理,保证新群也可输出。
|
||||||
|
"""
|
||||||
|
if not self.db:
|
||||||
|
return "❌ 身价模块未初始化"
|
||||||
|
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
# 使用 SQL DATE_SUB 也可计算,但这里使用 Python 日期便于阅读与调试。
|
||||||
|
base_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# 确保今天有快照,避免周报命令调用时出现空数据。
|
||||||
|
today_rank_rows = self.db.get_today_rankings(today, group_id, 50)
|
||||||
|
if not today_rank_rows:
|
||||||
|
self._recompute_group_snapshot(group_id, today)
|
||||||
|
today_rank_rows = self.db.get_today_rankings(today, group_id, 50)
|
||||||
|
if not today_rank_rows:
|
||||||
|
return "📊 本周暂无可用身价数据。"
|
||||||
|
|
||||||
|
today_score_map = self.db.get_snapshot_score_map(today, group_id)
|
||||||
|
base_score_map = self.db.get_snapshot_score_map(base_date, group_id)
|
||||||
|
|
||||||
|
# 计算用户分数变化,并做升降序榜单。
|
||||||
|
delta_rows: List[Tuple[str, float, float, float]] = []
|
||||||
|
for user_id, today_score in today_score_map.items():
|
||||||
|
old_score = float(base_score_map.get(user_id, 0.0))
|
||||||
|
delta = round(float(today_score) - old_score, 2)
|
||||||
|
delta_rows.append((user_id, float(today_score), old_score, delta))
|
||||||
|
|
||||||
|
delta_rows_sorted_up = sorted(delta_rows, key=lambda x: x[3], reverse=True)
|
||||||
|
delta_rows_sorted_down = sorted(delta_rows, key=lambda x: x[3])
|
||||||
|
|
||||||
|
cm = ContactManager.get_instance()
|
||||||
|
lines: List[str] = [
|
||||||
|
f"📈 身价周报({today})",
|
||||||
|
f"对比基线:{base_date}",
|
||||||
|
"",
|
||||||
|
"🏆 本周综合排行 Top5",
|
||||||
|
]
|
||||||
|
for idx, row in enumerate(today_rank_rows[:5], start=1):
|
||||||
|
uid = str(row.get("user_id") or "")
|
||||||
|
nick = cm.get_group_name(group_id, uid) or uid
|
||||||
|
score = float(row.get("score") or 0.0)
|
||||||
|
title = str(row.get("title") or "")
|
||||||
|
lines.append(f"{idx}. {nick} | {score:.2f} | {title}")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append("🚀 本周上升榜 Top5")
|
||||||
|
for idx, (uid, today_score, old_score, delta) in enumerate(delta_rows_sorted_up[:5], start=1):
|
||||||
|
nick = cm.get_group_name(group_id, uid) or uid
|
||||||
|
lines.append(f"{idx}. {nick} | {old_score:.2f} -> {today_score:.2f} | {delta:+.2f}")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append("🧊 本周波动下行 Top5")
|
||||||
|
for idx, (uid, today_score, old_score, delta) in enumerate(delta_rows_sorted_down[:5], start=1):
|
||||||
|
nick = cm.get_group_name(group_id, uid) or uid
|
||||||
|
lines.append(f"{idx}. {nick} | {old_score:.2f} -> {today_score:.2f} | {delta:+.2f}")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
lines.append("提示:分数由积分/发言/活跃/社交影响力综合计算。")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
def _build_explain_text(self) -> str:
|
def _build_explain_text(self) -> str:
|
||||||
"""输出算法说明文本。"""
|
"""输出算法说明文本。"""
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user