feat(value_rank): 新增社交桥梁榜与个人趋势能力
- 新增社交桥梁榜查询能力,基于社交边聚合输出连接人数、触达次数与互动分 - 新增我的趋势命令,支持按天查看近N天身价分与排名变化 - 周报增加社交桥梁人物摘要,提升群聊可读性与趣味性 - 同步扩展插件命令配置与趋势参数配置(default_trend_days/max_trend_days)
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
[ValueRank]
|
||||
enable = true
|
||||
command = ["我的身价", "身价排行", "社交热度榜", "搭子榜", "身价周报", "身价说明", "重算身价"]
|
||||
command = ["我的身价", "身价排行", "社交热度榜", "搭子榜", "社交桥梁榜", "我的趋势", "身价周报", "身价说明", "重算身价"]
|
||||
command-format = """
|
||||
📊 身价系统命令:
|
||||
1. 我的身价
|
||||
2. 身价排行 [名次]
|
||||
3. 社交热度榜 [名次]
|
||||
4. 搭子榜 [名次]
|
||||
5. 身价周报
|
||||
6. 身价说明
|
||||
7. 重算身价(管理员)
|
||||
5. 社交桥梁榜 [名次]
|
||||
6. 我的趋势 [天数]
|
||||
7. 身价周报
|
||||
8. 身价说明
|
||||
9. 重算身价(管理员)
|
||||
"""
|
||||
|
||||
# 统计窗口(天)
|
||||
@@ -28,6 +30,8 @@ base_score_scale = 1000
|
||||
# 排行默认展示数量
|
||||
default_rank_limit = 10
|
||||
max_rank_limit = 50
|
||||
default_trend_days = 7
|
||||
max_trend_days = 30
|
||||
|
||||
# @关系批处理(插件定时任务)参数
|
||||
mention_batch_size = 200
|
||||
|
||||
@@ -253,6 +253,60 @@ class ValueRankDB(BaseDBOperator):
|
||||
"""
|
||||
return self.execute_query(sql, (group_id, social_window_days, limit)) or []
|
||||
|
||||
def get_social_bridge_rankings(self, group_id: str, social_window_days: int, limit: int) -> List[Dict[str, Any]]:
|
||||
"""读取社交桥梁榜。
|
||||
|
||||
口径说明:
|
||||
1. 将有向边转换为“用户-伙伴”的无向视角(from 与 to 都算该用户触达);
|
||||
2. 用去重伙伴数衡量“桥梁覆盖面”,再用互动分做同分排序;
|
||||
3. 该榜更强调“连接不同人”的能力,而不是单点高频互动。
|
||||
"""
|
||||
sql = """
|
||||
SELECT
|
||||
user_id,
|
||||
COUNT(DISTINCT partner_id) AS partner_count,
|
||||
SUM(mention_count) AS mention_count_window,
|
||||
SUM(interaction_score) AS interaction_score_window
|
||||
FROM (
|
||||
SELECT
|
||||
from_user_id AS user_id,
|
||||
to_user_id AS partner_id,
|
||||
mention_count,
|
||||
interaction_score
|
||||
FROM t_social_edges_daily
|
||||
WHERE group_id = %s
|
||||
AND stat_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
|
||||
UNION ALL
|
||||
SELECT
|
||||
to_user_id AS user_id,
|
||||
from_user_id AS partner_id,
|
||||
mention_count,
|
||||
interaction_score
|
||||
FROM t_social_edges_daily
|
||||
WHERE group_id = %s
|
||||
AND stat_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
|
||||
) t
|
||||
GROUP BY user_id
|
||||
ORDER BY partner_count DESC, interaction_score_window DESC, mention_count_window DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
return self.execute_query(
|
||||
sql,
|
||||
(group_id, social_window_days, group_id, social_window_days, limit),
|
||||
) or []
|
||||
|
||||
def get_user_score_trend(self, group_id: str, user_id: str, days: int) -> List[Dict[str, Any]]:
|
||||
"""读取用户近 N 天身价趋势(按日期升序)。"""
|
||||
sql = """
|
||||
SELECT stat_date, score, rank_no
|
||||
FROM t_value_rank_snapshot
|
||||
WHERE group_id = %s
|
||||
AND user_id = %s
|
||||
AND stat_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
|
||||
ORDER BY stat_date ASC
|
||||
"""
|
||||
return self.execute_query(sql, (group_id, user_id, days)) or []
|
||||
|
||||
def get_snapshot_score_map(self, stat_date: str, group_id: str) -> Dict[str, float]:
|
||||
"""读取某天群内所有用户分数字典:{user_id: score}。"""
|
||||
sql = """
|
||||
@@ -420,7 +474,7 @@ class ValueRankPlugin(MessagePluginInterface):
|
||||
"""
|
||||
|
||||
FEATURE_KEY = "VALUE_RANK"
|
||||
FEATURE_DESCRIPTION = "📊 身价排行 [我的身价, 身价排行, 身价说明, 重算身价]"
|
||||
FEATURE_DESCRIPTION = "📊 身价排行 [我的身价, 身价排行, 社交热度榜, 搭子榜, 社交桥梁榜, 我的趋势, 身价周报, 身价说明, 重算身价]"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -461,8 +515,11 @@ class ValueRankPlugin(MessagePluginInterface):
|
||||
|
||||
# 配置默认值:即使未配置 config.toml,也能以保守参数运行。
|
||||
self.enable = True
|
||||
self._commands = ["我的身价", "身价排行", "社交热度榜", "搭子榜", "身价周报", "身价说明", "重算身价"]
|
||||
self.command_format = "我的身价 | 身价排行 [名次] | 身价说明 | 重算身价"
|
||||
self._commands = ["我的身价", "身价排行", "社交热度榜", "搭子榜", "社交桥梁榜", "我的趋势", "身价周报", "身价说明", "重算身价"]
|
||||
self.command_format = (
|
||||
"我的身价 | 身价排行 [名次] | 社交热度榜 [名次] | 搭子榜 [名次] | "
|
||||
"社交桥梁榜 [名次] | 我的趋势 [天数] | 身价周报 | 身价说明 | 重算身价"
|
||||
)
|
||||
|
||||
self.message_window_days = 7
|
||||
self.active_window_days = 30
|
||||
@@ -478,6 +535,8 @@ class ValueRankPlugin(MessagePluginInterface):
|
||||
|
||||
self.default_rank_limit = 10
|
||||
self.max_rank_limit = 50
|
||||
self.default_trend_days = 7
|
||||
self.max_trend_days = 30
|
||||
self.mention_batch_size = 200
|
||||
self.mention_window_start_minutes = 20
|
||||
self.mention_window_end_minutes = 10
|
||||
@@ -505,6 +564,8 @@ class ValueRankPlugin(MessagePluginInterface):
|
||||
|
||||
self.default_rank_limit = int(cfg.get("default_rank_limit", self.default_rank_limit))
|
||||
self.max_rank_limit = int(cfg.get("max_rank_limit", self.max_rank_limit))
|
||||
self.default_trend_days = int(cfg.get("default_trend_days", self.default_trend_days))
|
||||
self.max_trend_days = int(cfg.get("max_trend_days", self.max_trend_days))
|
||||
self.mention_batch_size = int(cfg.get("mention_batch_size", self.mention_batch_size))
|
||||
self.mention_window_start_minutes = int(cfg.get("mention_window_start_minutes", self.mention_window_start_minutes))
|
||||
self.mention_window_end_minutes = int(cfg.get("mention_window_end_minutes", self.mention_window_end_minutes))
|
||||
@@ -580,6 +641,18 @@ class ValueRankPlugin(MessagePluginInterface):
|
||||
await bot.send_text_message(roomid, text, sender)
|
||||
return True, "查询成功"
|
||||
|
||||
if command == "社交桥梁榜":
|
||||
limit = self._parse_rank_limit(content)
|
||||
text = await self._build_social_bridge_text(roomid, limit)
|
||||
await bot.send_text_message(roomid, text, sender)
|
||||
return True, "查询成功"
|
||||
|
||||
if command == "我的趋势":
|
||||
days = self._parse_trend_days(content)
|
||||
text = await self._build_my_trend_text(roomid, sender, days)
|
||||
await bot.send_text_message(roomid, text, sender)
|
||||
return True, "查询成功"
|
||||
|
||||
if command == "身价周报":
|
||||
text = await self._build_weekly_report_text(roomid)
|
||||
await bot.send_text_message(roomid, text, sender)
|
||||
@@ -985,6 +1058,66 @@ class ValueRankPlugin(MessagePluginInterface):
|
||||
lines.append(f"{medal} {nick_a} × {nick_b} | 互动{score:.1f} | @次数{mention_count}")
|
||||
return "\n".join(lines)
|
||||
|
||||
async def _build_social_bridge_text(self, group_id: str, limit: int) -> str:
|
||||
"""构建“社交桥梁榜”输出文本。
|
||||
|
||||
说明:
|
||||
1. 伙伴数越高,说明该成员连接的人越广;
|
||||
2. 同伙伴数时按互动分排序,避免“浅连接”占优。
|
||||
"""
|
||||
if not self.db:
|
||||
return "❌ 身价模块未初始化"
|
||||
|
||||
rows = self.db.get_social_bridge_rankings(group_id, self.social_window_days, limit)
|
||||
if not rows:
|
||||
return "📊 近期暂无社交桥梁数据。"
|
||||
|
||||
cm = ContactManager.get_instance()
|
||||
lines = [f"🌉 社交桥梁榜(近{self.social_window_days}天 Top{len(rows)})"]
|
||||
for idx, row in enumerate(rows, start=1):
|
||||
user_id = str(row.get("user_id") or "")
|
||||
partner_count = int(row.get("partner_count") or 0)
|
||||
mention_count = int(row.get("mention_count_window") or 0)
|
||||
score = float(row.get("interaction_score_window") or 0.0)
|
||||
nick = cm.get_group_name(group_id, user_id) or user_id
|
||||
medal = "🥇" if idx == 1 else "🥈" if idx == 2 else "🥉" if idx == 3 else f"{idx}."
|
||||
lines.append(f"{medal} {nick} | 连接{partner_count}人 | 互动{score:.1f} | 触达{mention_count}")
|
||||
return "\n".join(lines)
|
||||
|
||||
async def _build_my_trend_text(self, group_id: str, user_id: str, days: int) -> str:
|
||||
"""构建“我的趋势”输出文本。"""
|
||||
if not self.db:
|
||||
return "❌ 身价模块未初始化"
|
||||
|
||||
# 先保障今日快照存在,避免趋势末尾为空导致用户误解“今天没分数”。
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
today_row = self.db.get_user_snapshot(today, group_id, user_id)
|
||||
if not today_row:
|
||||
self._recompute_group_snapshot(group_id, today)
|
||||
|
||||
trend_rows = self.db.get_user_score_trend(group_id, user_id, days)
|
||||
if not trend_rows:
|
||||
return "📊 暂无你的趋势数据,请先在群内产生消息后再试。"
|
||||
|
||||
cm = ContactManager.get_instance()
|
||||
nick = cm.get_group_name(group_id, user_id) or user_id
|
||||
|
||||
first_score = float(trend_rows[0].get("score") or 0.0)
|
||||
last_score = float(trend_rows[-1].get("score") or 0.0)
|
||||
delta = round(last_score - first_score, 2)
|
||||
trend_flag = "上涨" if delta > 0 else "下滑" if delta < 0 else "持平"
|
||||
|
||||
lines = [f"📉 {nick} 的身价趋势(近{days}天)"]
|
||||
for row in trend_rows:
|
||||
stat_date = str(row.get("stat_date") or "")
|
||||
score = float(row.get("score") or 0.0)
|
||||
rank_no = int(row.get("rank_no") or 0)
|
||||
lines.append(f"- {stat_date}: {score:.2f}(第{rank_no}名)")
|
||||
|
||||
lines.append("")
|
||||
lines.append(f"趋势结论:{trend_flag} {delta:+.2f}")
|
||||
return "\n".join(lines)
|
||||
|
||||
async def _build_weekly_report_text(self, group_id: str) -> str:
|
||||
"""构建“身价周报”文本。
|
||||
|
||||
@@ -1049,6 +1182,18 @@ class ValueRankPlugin(MessagePluginInterface):
|
||||
|
||||
lines.append("")
|
||||
lines.append("提示:分数由积分/发言/活跃/社交影响力综合计算。")
|
||||
|
||||
# 加一段“社交桥梁人物”,让周报除了涨跌之外还能看到连接贡献。
|
||||
bridge_rows = self.db.get_social_bridge_rankings(group_id, self.social_window_days, 3)
|
||||
if bridge_rows:
|
||||
lines.append("")
|
||||
lines.append("🌉 本周社交桥梁人物")
|
||||
for idx, row in enumerate(bridge_rows, start=1):
|
||||
uid = str(row.get("user_id") or "")
|
||||
nick = cm.get_group_name(group_id, uid) or uid
|
||||
partner_count = int(row.get("partner_count") or 0)
|
||||
lines.append(f"{idx}. {nick} | 连接{partner_count}人")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _process_pending_mentions_for_group(self, group_id: str) -> Dict[str, int]:
|
||||
@@ -1241,6 +1386,23 @@ class ValueRankPlugin(MessagePluginInterface):
|
||||
limit = max(1, limit)
|
||||
return min(limit, self.max_rank_limit)
|
||||
|
||||
def _parse_trend_days(self, content: str) -> int:
|
||||
"""解析趋势天数参数。
|
||||
|
||||
约束策略:
|
||||
1. 默认展示近7天,避免输出过长;
|
||||
2. 上限限制到配置值,防止单次命令读取过多历史。
|
||||
"""
|
||||
parts = content.split()
|
||||
if len(parts) < 2:
|
||||
return self.default_trend_days
|
||||
try:
|
||||
days = int(parts[1])
|
||||
except Exception:
|
||||
return self.default_trend_days
|
||||
days = max(1, days)
|
||||
return min(days, self.max_trend_days)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_linear(value: float, ceiling: float) -> float:
|
||||
"""线性归一化并截断到 [0, 1]。"""
|
||||
|
||||
Reference in New Issue
Block a user