feat(value_rank): 新增社交桥梁榜与个人趋势能力

- 新增社交桥梁榜查询能力,基于社交边聚合输出连接人数、触达次数与互动分

- 新增我的趋势命令,支持按天查看近N天身价分与排名变化

- 周报增加社交桥梁人物摘要,提升群聊可读性与趣味性

- 同步扩展插件命令配置与趋势参数配置(default_trend_days/max_trend_days)
This commit is contained in:
liuwei
2026-04-21 14:17:00 +08:00
parent d64d11a384
commit 2c90bc2ebe
2 changed files with 173 additions and 7 deletions

View File

@@ -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

View File

@@ -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]。"""