diff --git a/plugins/value_rank/main.py b/plugins/value_rank/main.py index 8e8ed80..6946b1e 100644 --- a/plugins/value_rank/main.py +++ b/plugins/value_rank/main.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- +import base64 import json import math +import mimetypes import re import xml.etree.ElementTree as ET from datetime import datetime, timedelta @@ -1459,6 +1461,7 @@ class ValueRankPlugin(MessagePluginInterface): } cm = ContactManager.get_instance() + strongest_uid = selected_nodes[0] if selected_nodes else "" edge_svg_parts: List[str] = [] # 边数字标签只给“最重要的少数边”: # 1. 全边标数字时,图面会被 0/1、1/0 这类噪声淹没; @@ -1484,6 +1487,24 @@ class ValueRankPlugin(MessagePluginInterface): units += 1.0 if ord(ch) < 128 else 1.75 return units + def _build_avatar_data_url(raw_avatar_url: str, wxid: str) -> str: + """优先读取本地缓存头像并转 data URL,减少截图时依赖远端头像可用性。""" + # 社交图渲染发生在 Playwright 截图流程中: + # 1. 如果直接给远端头像 URL,链接过期、网络抖动或防盗链都可能导致头像空白; + # 2. 这里优先确保头像已缓存到本地,再内联为 data URL,截图时最稳定; + # 3. 如果本地缓存仍失败,再回退到原始 URL,尽量不影响图谱整体生成。 + local_avatar_path = str(cm.ensure_head_image_cached(wxid) or "").strip() + if local_avatar_path: + try: + avatar_path = Path(local_avatar_path) + image_bytes = avatar_path.read_bytes() + mime_type = mimetypes.guess_type(str(avatar_path))[0] or "image/jpeg" + base64_str = base64.b64encode(image_bytes).decode("utf-8") + return f"data:{mime_type};base64,{base64_str}" + except Exception as exc: + self.LOG.debug(f"[{self.name}] 头像转 data url 失败: wxid={wxid}, error={exc}") + return str(raw_avatar_url or "").strip() + for edge_idx, (a, b, mention_count, score, a_to_b_count, b_to_a_count) in enumerate(selected_edges, start=1): ax, ay = pos_map[a] bx, by = pos_map[b] @@ -1541,9 +1562,8 @@ class ValueRankPlugin(MessagePluginInterface): nick = cm.get_group_name(group_id, uid) or uid display_nick = _shorten_graph_nick(str(nick), 8) safe_nick = html.escape(display_nick) - avatar_url = str(cm.get_head_image(uid) or "").strip() + avatar_url = _build_avatar_data_url(str(cm.get_head_image(uid) or "").strip(), uid) ring_idx = int(node_meta_map.get(uid, {}).get("ring_idx", 0)) - angle = float(node_meta_map.get(uid, {}).get("angle", 0.0)) # 使用“连接度越高环线越偏暖”的视觉策略,帮助快速识别核心节点。 ring_color = "rgba(255, 152, 0, 0.95)" if size_norm >= 0.6 else "rgba(79, 123, 201, 0.95)" @@ -1576,6 +1596,34 @@ class ValueRankPlugin(MessagePluginInterface): f'{html.escape(str(nick)[:1] or "?")}' ) + # 最强核心节点补一个皇冠标记: + # 1. strongest_uid 取当前排序后的第一名,代表群里最强的桥梁/核心节点; + # 2. 皇冠放在头像上方,视觉上比文字提示更直观; + # 3. 使用简单 SVG 形状,避免依赖字体里的 emoji / 特殊字符。 + if uid == strongest_uid: + crown_y = y - node_radius - 16.0 + crown_points = [ + (x - 15.0, crown_y + 18.0), + (x - 10.0, crown_y + 6.0), + (x - 2.5, crown_y + 14.0), + (x + 0.0, crown_y + 2.0), + (x + 2.5, crown_y + 14.0), + (x + 10.0, crown_y + 6.0), + (x + 15.0, crown_y + 18.0), + ] + crown_points_text = " ".join([f"{px:.1f},{py:.1f}" for px, py in crown_points]) + node_svg_parts.append( + f'' + f'' + f'' + f'' + f'' + f'' + f'' + ) + # “连接人数”改成贴在头像上的数字徽标: # 1. 用户反馈原先那行“连接x · 分数y”混在线条里看不清; # 2. 数字徽标直接挂在节点边缘,读图时更像“这个人连了多少人”; @@ -1595,30 +1643,17 @@ class ValueRankPlugin(MessagePluginInterface): f'font-size="{badge_font_size}" fill="#2F3B52" font-weight="700">{badge_text}' ) - # 节点文案只保留昵称,并加半透明底板: - # 1. 去掉第二行数值后,名字可以更干净地贴在节点旁边; - # 2. 底板能把名字从复杂连线背景里“托”出来; - # 3. 长昵称做截断,避免外围节点之间互相顶住。 - outward_dx = math.cos(angle) - outward_dy = math.sin(angle) - label_gap = node_radius + (14.0 if ring_idx == 0 else 18.0 + ring_idx * 2.0) - title_x = x + outward_dx * label_gap - title_y = y + outward_dy * label_gap - if abs(outward_dx) < 0.20: - anchor = "middle" - elif outward_dx > 0: - anchor = "start" - else: - anchor = "end" + # 名字固定放在头像正下方,避免沿径向排布带来的“漂移感”: + # 1. 用户反馈名字位置飘忽,说明相对角度布局不利于快速扫图; + # 2. 统一放在头像下方后,读图路径会稳定很多; + # 3. 底板保留,继续提升名字在复杂线条背景上的可读性。 + title_x = x + title_y = y + node_radius + 24.0 + anchor = "middle" label_units = _estimate_label_units(display_nick) label_width = max(44.0, min(156.0, 10.0 * label_units + 18.0)) label_height = 24.0 - if anchor == "middle": - label_box_x = title_x - label_width / 2.0 - elif anchor == "start": - label_box_x = title_x - 8.0 - else: - label_box_x = title_x - label_width + 8.0 + label_box_x = title_x - label_width / 2.0 label_box_y = title_y - 18.0 node_svg_parts.append( f'