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'