diff --git a/plugins/value_rank/config.toml b/plugins/value_rank/config.toml index 997d9ef..a8b4022 100644 --- a/plugins/value_rank/config.toml +++ b/plugins/value_rank/config.toml @@ -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 diff --git a/plugins/value_rank/main.py b/plugins/value_rank/main.py index e399b0c..40a3679 100644 --- a/plugins/value_rank/main.py +++ b/plugins/value_rank/main.py @@ -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]。"""