修复关系图头像缺失并调整名字与核心标识
This commit is contained in:
@@ -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 "?")}</text>'
|
||||
)
|
||||
|
||||
# 最强核心节点补一个皇冠标记:
|
||||
# 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'<g>'
|
||||
f'<rect x="{x - 15.0:.1f}" y="{crown_y + 18.0:.1f}" width="30.0" height="5.5" rx="2.8" ry="2.8" '
|
||||
f'fill="rgba(255, 183, 3, 0.98)" stroke="rgba(168, 104, 0, 0.85)" stroke-width="1.0"></rect>'
|
||||
f'<polygon points="{crown_points_text}" fill="rgba(255, 202, 40, 0.98)" '
|
||||
f'stroke="rgba(168, 104, 0, 0.85)" stroke-width="1.2"></polygon>'
|
||||
f'<circle cx="{x - 10.0:.1f}" cy="{crown_y + 6.0:.1f}" r="2.2" fill="rgba(255,255,255,0.96)"></circle>'
|
||||
f'<circle cx="{x + 0.0:.1f}" cy="{crown_y + 2.0:.1f}" r="2.4" fill="rgba(255,255,255,0.96)"></circle>'
|
||||
f'<circle cx="{x + 10.0:.1f}" cy="{crown_y + 6.0:.1f}" r="2.2" fill="rgba(255,255,255,0.96)"></circle>'
|
||||
f'</g>'
|
||||
)
|
||||
|
||||
# “连接人数”改成贴在头像上的数字徽标:
|
||||
# 1. 用户反馈原先那行“连接x · 分数y”混在线条里看不清;
|
||||
# 2. 数字徽标直接挂在节点边缘,读图时更像“这个人连了多少人”;
|
||||
@@ -1595,30 +1643,17 @@ class ValueRankPlugin(MessagePluginInterface):
|
||||
f'font-size="{badge_font_size}" fill="#2F3B52" font-weight="700">{badge_text}</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'<rect x="{label_box_x:.1f}" y="{label_box_y:.1f}" width="{label_width:.1f}" height="{label_height:.1f}" '
|
||||
|
||||
Reference in New Issue
Block a user