diff --git a/plugins/value_rank/main.py b/plugins/value_rank/main.py
index 8ddeff7..3c7fd69 100644
--- a/plugins/value_rank/main.py
+++ b/plugins/value_rank/main.py
@@ -1247,10 +1247,37 @@ class ValueRankPlugin(MessagePluginInterface):
if len(selected_nodes) < 2:
return None
- selected_edges: List[Tuple[str, str, float, float]] = []
+ # 记录每条无向边对应的双向明细次数:
+ # pair_dir_count[(a,b)] = {"a_to_b": x, "b_to_a": y}
+ # 这样在图上可以直接标注“双向@次数”,避免只看总互动分看不出方向关系。
+ pair_dir_count: Dict[Tuple[str, str], Dict[str, int]] = {}
+ for row in edge_rows:
+ uid_a = str(row.get("from_user_id") or "").strip()
+ uid_b = str(row.get("to_user_id") or "").strip()
+ if not uid_a or not uid_b or uid_a == uid_b:
+ continue
+ a, b = (uid_a, uid_b) if uid_a < uid_b else (uid_b, uid_a)
+ mention_count = int(float(row.get("mention_count_window") or 0.0))
+ bucket = pair_dir_count.setdefault((a, b), {"a_to_b": 0, "b_to_a": 0})
+ if uid_a == a and uid_b == b:
+ bucket["a_to_b"] += mention_count
+ else:
+ bucket["b_to_a"] += mention_count
+
+ selected_edges: List[Tuple[str, str, float, float, int, int]] = []
for (a, b), data in edge_map.items():
if a in selected_set and b in selected_set:
- selected_edges.append((a, b, float(data.get("mention_count", 0.0)), float(data.get("score", 0.0))))
+ dir_bucket = pair_dir_count.get((a, b), {"a_to_b": 0, "b_to_a": 0})
+ selected_edges.append(
+ (
+ a,
+ b,
+ float(data.get("mention_count", 0.0)),
+ float(data.get("score", 0.0)),
+ int(dir_bucket.get("a_to_b", 0)),
+ int(dir_bucket.get("b_to_a", 0)),
+ )
+ )
if not selected_edges:
return None
@@ -1274,7 +1301,7 @@ class ValueRankPlugin(MessagePluginInterface):
self,
group_id: str,
selected_nodes: List[str],
- selected_edges: List[Tuple[str, str, float, float]],
+ selected_edges: List[Tuple[str, str, float, float, int, int]],
partner_map: Dict[str, set],
node_score_map: Dict[str, float],
) -> str:
@@ -1303,7 +1330,11 @@ class ValueRankPlugin(MessagePluginInterface):
cm = ContactManager.get_instance()
edge_svg_parts: List[str] = []
- for a, b, mention_count, score in selected_edges:
+ # 双向@标签层:
+ # 1. 每条边中点标注“a->b / b->a”;
+ # 2. 使用白底半透明标签提升可读性,避免与连线重叠难辨识。
+ edge_label_parts: List[str] = []
+ for a, b, mention_count, score, a_to_b_count, b_to_a_count in selected_edges:
ax, ay = pos_map[a]
bx, by = pos_map[b]
normalized = max(0.12, min(score / max_edge_score, 1.0))
@@ -1313,6 +1344,33 @@ class ValueRankPlugin(MessagePluginInterface):
f''
)
+ # 通过字典序固定方向,确保同一条边每次渲染文案方向一致。
+ # 标签文案改为“昵称 + 双向次数 + 恶搞关系标签”,不再展示 wxid。
+ mx, my = (ax + bx) / 2.0, (ay + by) / 2.0
+ nick_a = cm.get_group_name(group_id, a) or a
+ nick_b = cm.get_group_name(group_id, b) or b
+
+ total_count = int(a_to_b_count) + int(b_to_a_count)
+ diff_count = abs(int(a_to_b_count) - int(b_to_a_count))
+ if total_count >= 12 and diff_count <= 2:
+ relation_tag = "双向奔赴"
+ elif diff_count >= 8 and max(int(a_to_b_count), int(b_to_a_count)) >= 10:
+ relation_tag = "单向上头"
+ elif total_count <= 2:
+ relation_tag = "点头之交"
+ elif int(a_to_b_count) == 0 or int(b_to_a_count) == 0:
+ relation_tag = "单箭头输出"
+ else:
+ relation_tag = "互相捧场"
+
+ label_text = f"{nick_a}→{nick_b} {a_to_b_count}/{b_to_a_count} | {relation_tag}"
+ safe_label = html.escape(label_text)
+ edge_label_parts.append(
+ f''
+ f''
+ f'{safe_label}'
+ f''
+ )
# 节点头像层拆分为 defs + body 两段:
# 1. defs 内定义每个节点的裁剪路径,避免头像越界;
@@ -1399,6 +1457,7 @@ class ValueRankPlugin(MessagePluginInterface):
"__GROUP_TITLE__": group_title,
"__SUMMARY_TEXT__": summary_text,
"__EDGE_SVG__": "".join(edge_svg_parts),
+ "__EDGE_LABELS__": "".join(edge_label_parts),
"__NODE_DEFS__": "".join(node_defs_parts),
"__NODE_SVG__": "".join(node_svg_parts),
}
diff --git a/plugins/value_rank/templates/social_graph.html b/plugins/value_rank/templates/social_graph.html
index 99d6b77..6ce9614 100644
--- a/plugins/value_rank/templates/social_graph.html
+++ b/plugins/value_rank/templates/social_graph.html
@@ -53,6 +53,7 @@
__NODE_DEFS__
__EDGE_SVG__
+ __EDGE_LABELS__
__NODE_SVG__