🐛 修复 CF查询插件 import 路径,改用 @filter.command 和 event.plain_result

This commit is contained in:
ATRI
2026-07-08 11:43:12 +08:00
parent a9c4c7b054
commit 2319309129

View File

@@ -1,7 +1,7 @@
""" """
ATRI CF 查分与刷题分析插件 📊 ATRI CF 查分与刷题分析插件 📊 v1.0
查询 Codeforces 用户信息、Rating、刷题统计,生成可视化图表 查询 Codeforces 用户信息、Rating、刷题统计
支持用户绑定QQ ↔ CF handle 支持用户绑定、可视化图表、分析报告
""" """
import json import json
import os import os
@@ -11,8 +11,9 @@ import urllib.request
from collections import Counter from collections import Counter
from pathlib import Path from pathlib import Path
from astrbot.api.all import Star, AstrMessageEvent, CommandResult, Plain, Image from astrbot.api.all import *
from astrbot.core.star.register import register from astrbot.api.event import filter, AstrMessageEvent
from astrbot.api.star import Context, Star, register
# ---------- 数据存储 ---------- # ---------- 数据存储 ----------
BIND_DIR = Path("data/plugins/astrbot_plugin_cf_query") BIND_DIR = Path("data/plugins/astrbot_plugin_cf_query")
@@ -34,7 +35,7 @@ def _save_bindings(data: dict):
# ---------- CF API 工具函数 ---------- # ---------- CF API 工具函数 ----------
def _cf_api(url: str) -> dict | None: def _cf_api(url: str) -> dict | None:
"""调用 CF API返回 dict 或 None""" """调用 CF API返回 result 或 None"""
req = urllib.request.Request(url, headers={"User-Agent": "ATRI-CF-Plugin/1.0"}) req = urllib.request.Request(url, headers={"User-Agent": "ATRI-CF-Plugin/1.0"})
try: try:
with urllib.request.urlopen(req, timeout=15) as resp: with urllib.request.urlopen(req, timeout=15) as resp:
@@ -72,9 +73,7 @@ def _analyze_submissions(submissions: list) -> dict:
wa = sum(1 for s in submissions if "WRONG_ANSWER" in s.get("verdict", "")) wa = sum(1 for s in submissions if "WRONG_ANSWER" in s.get("verdict", ""))
tle = sum(1 for s in submissions if "TIME_LIMIT_EXCEEDED" in s.get("verdict", "")) tle = sum(1 for s in submissions if "TIME_LIMIT_EXCEEDED" in s.get("verdict", ""))
ce = sum(1 for s in submissions if "COMPILATION_ERROR" in s.get("verdict", "")) ce = sum(1 for s in submissions if "COMPILATION_ERROR" in s.get("verdict", ""))
other = total - ac - wa - tle - ce
# 按 rating 统计通过题数(去重)
rating_counter = Counter() rating_counter = Counter()
tag_counter = Counter() tag_counter = Counter()
solved = set() solved = set()
@@ -92,13 +91,10 @@ def _analyze_submissions(submissions: list) -> dict:
"pid": pid, "pid": pid,
"name": s["problem"]["name"], "name": s["problem"]["name"],
"rating": rtg, "rating": rtg,
"tags": s["problem"].get("tags", []),
"time": s.get("creationTimeSeconds", 0), "time": s.get("creationTimeSeconds", 0),
}) })
# 最近 10 条 AC
ac_records.sort(key=lambda x: x["time"], reverse=True) ac_records.sort(key=lambda x: x["time"], reverse=True)
recent_ac = ac_records[:10]
return { return {
"total": total, "total": total,
@@ -106,16 +102,14 @@ def _analyze_submissions(submissions: list) -> dict:
"wa": wa, "wa": wa,
"tle": tle, "tle": tle,
"ce": ce, "ce": ce,
"other": other,
"solved_count": len(solved), "solved_count": len(solved),
"rating_dist": dict(sorted(rating_counter.items())), "rating_dist": dict(sorted(rating_counter.items())),
"tag_dist": dict(tag_counter.most_common(10)), "tag_dist": dict(tag_counter.most_common(10)),
"recent_ac": recent_ac, "recent_ac": ac_records[:10],
} }
def _generate_rating_chart(handle: str, history: list, save_path: str) -> bool: def _generate_rating_chart(handle: str, history: list, save_path: str) -> bool:
"""生成 Rating 变化柱状图"""
try: try:
import matplotlib import matplotlib
matplotlib.use("Agg") matplotlib.use("Agg")
@@ -123,16 +117,9 @@ def _generate_rating_chart(handle: str, history: list, save_path: str) -> bool:
import numpy as np import numpy as np
except ImportError: except ImportError:
return False return False
if not history: if not history:
return False return False
dates, ratings, old_ratings, changes, colors = [], [], [], [], []
dates = []
ratings = []
old_ratings = []
changes = []
colors = []
for c in history: for c in history:
ts = time.strftime("%m/%d", time.localtime(c["ratingUpdateTimeSeconds"])) ts = time.strftime("%m/%d", time.localtime(c["ratingUpdateTimeSeconds"]))
dates.append(f"#{c['contestId']}\n{ts}") dates.append(f"#{c['contestId']}\n{ts}")
@@ -145,33 +132,29 @@ def _generate_rating_chart(handle: str, history: list, save_path: str) -> bool:
fig, ax = plt.subplots(figsize=(10, 5.5)) fig, ax = plt.subplots(figsize=(10, 5.5))
fig.patch.set_facecolor("#f8f4f0") fig.patch.set_facecolor("#f8f4f0")
ax.set_facecolor("#ffffff") ax.set_facecolor("#ffffff")
x = np.arange(len(dates)) x = np.arange(len(dates))
bars = ax.bar(x, ratings, width=0.5, color=colors, edgecolor="white", linewidth=1.5, zorder=3) bars = ax.bar(x, ratings, width=0.5, color=colors, edgecolor="white", linewidth=1.5, zorder=3)
ax.bar(x, old_ratings, width=0.5, color="#e0d0c0", alpha=0.4, edgecolor="white", linewidth=1.5, zorder=2) ax.bar(x, old_ratings, width=0.5, color="#e0d0c0", alpha=0.4, edgecolor="white", linewidth=1.5, zorder=2)
for i, (bar, r, ch) in enumerate(zip(bars, ratings, changes)):
for i, (bar, rating, change) in enumerate(zip(bars, ratings, changes)): ax.text(bar.get_x() + bar.get_width()/2, bar.get_height()+15, str(r),
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 15, ha="center", va="bottom", fontsize=13, fontweight="bold", color="#333")
str(rating), ha="center", va="bottom", fontsize=13, fontweight="bold", color="#333") sign = "+" if ch > 0 else ""
sign = "+" if change > 0 else "" clr = "#3eb86b" if ch > 0 else "#e8785a"
clr = "#3eb86b" if change > 0 else "#e8785a" mid = old_ratings[i] + (r - old_ratings[i]) / 2
mid_y = old_ratings[i] + (rating - old_ratings[i]) / 2 ax.annotate(f"{sign}{ch}", xy=(bar.get_x()+bar.get_width()/2, mid),
ax.annotate(f"{sign}{change}", xy=(bar.get_x() + bar.get_width() / 2, mid_y),
fontsize=11, fontweight="bold", color="white", ha="center", va="center", fontsize=11, fontweight="bold", color="white", ha="center", va="center",
bbox=dict(boxstyle="round,pad=0.2", facecolor=clr, edgecolor="none", alpha=0.85)) bbox=dict(boxstyle="round,pad=0.2", facecolor=clr, edgecolor="none", alpha=0.85))
ax.set_xticks(x) ax.set_xticks(x)
ax.set_xticklabels(dates, fontsize=10) ax.set_xticklabels(dates, fontsize=10)
ax.set_title(f"{handle} · Rating Changes", fontsize=17, fontweight="bold", color="#d06040", pad=15) ax.set_title(f"{handle} · Rating Changes", fontsize=17, fontweight="bold", color="#d06040", pad=15)
ax.set_ylabel("Rating", fontsize=12, fontweight="bold", color="#555") ax.set_ylabel("Rating", fontsize=12, fontweight="bold", color="#555")
ax.set_ylim(0, max(ratings) * 1.3) ax.set_ylim(0, max(ratings) * 1.3)
ax.spines["top"].set_visible(False) for s in ["top", "right"]:
ax.spines["right"].set_visible(False) ax.spines[s].set_visible(False)
ax.spines["left"].set_color("#ddd") for s in ["left", "bottom"]:
ax.spines["bottom"].set_color("#ddd") ax.spines[s].set_color("#ddd")
ax.tick_params(colors="#888") ax.tick_params(colors="#888")
ax.grid(axis="y", alpha=0.3, color="#ddd", zorder=0) ax.grid(axis="y", alpha=0.3, color="#ddd", zorder=0)
plt.tight_layout() plt.tight_layout()
fig.savefig(save_path, dpi=180, bbox_inches="tight", facecolor="#f8f4f0") fig.savefig(save_path, dpi=180, bbox_inches="tight", facecolor="#f8f4f0")
plt.close() plt.close()
@@ -179,7 +162,6 @@ def _generate_rating_chart(handle: str, history: list, save_path: str) -> bool:
def _generate_problem_chart(handle: str, rating_dist: dict, save_path: str) -> bool: def _generate_problem_chart(handle: str, rating_dist: dict, save_path: str) -> bool:
"""生成解题难度分布柱状图"""
try: try:
import matplotlib import matplotlib
matplotlib.use("Agg") matplotlib.use("Agg")
@@ -187,45 +169,34 @@ def _generate_problem_chart(handle: str, rating_dist: dict, save_path: str) -> b
import numpy as np import numpy as np
except ImportError: except ImportError:
return False return False
if not rating_dist: if not rating_dist:
return False return False
labels_map = {0: "Unrated"} labels_map = {0: "Unrated"}
labels = [] labels, values, colors = [], [], []
values = []
colors = []
palette = ["#b0b0b0", "#8bc34a", "#4caf50", "#26a69a", "#00bcd4", "#2196f3", "#7e57c2", "#9c27b0"] palette = ["#b0b0b0", "#8bc34a", "#4caf50", "#26a69a", "#00bcd4", "#2196f3", "#7e57c2", "#9c27b0"]
for i, (rtg, cnt) in enumerate(sorted(rating_dist.items())):
for i, (rating, cnt) in enumerate(sorted(rating_dist.items())): labels.append(labels_map.get(rtg, str(rtg)))
lbl = labels_map.get(rating, str(rating))
labels.append(lbl)
values.append(cnt) values.append(cnt)
colors.append(palette[i % len(palette)]) colors.append(palette[i % len(palette)])
fig, ax = plt.subplots(figsize=(10, 5)) fig, ax = plt.subplots(figsize=(10, 5))
fig.patch.set_facecolor("#f8f4f0") fig.patch.set_facecolor("#f8f4f0")
ax.set_facecolor("#ffffff") ax.set_facecolor("#ffffff")
x = np.arange(len(labels)) x = np.arange(len(labels))
bars = ax.bar(x, values, width=0.55, color=colors, edgecolor="white", linewidth=1.5, zorder=3) bars = ax.bar(x, values, width=0.55, color=colors, edgecolor="white", linewidth=1.5, zorder=3)
for bar, val in zip(bars, values): for bar, val in zip(bars, values):
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.1, ax.text(bar.get_x() + bar.get_width()/2, bar.get_height()+0.1, str(val),
str(val), ha="center", va="bottom", fontsize=15, fontweight="bold", color="#333") ha="center", va="bottom", fontsize=15, fontweight="bold", color="#333")
ax.set_xticks(x) ax.set_xticks(x)
ax.set_xticklabels(labels, fontsize=12, fontweight="bold") ax.set_xticklabels(labels, fontsize=12, fontweight="bold")
ax.set_title(f"{handle} · Problem Ratings", fontsize=17, fontweight="bold", color="#d06040", pad=15) ax.set_title(f"{handle} · Problem Ratings", fontsize=17, fontweight="bold", color="#d06040", pad=15)
ax.set_ylabel("Problems Solved", fontsize=12, fontweight="bold", color="#555") ax.set_ylabel("Problems Solved", fontsize=12, fontweight="bold", color="#555")
ax.set_ylim(0, max(values) * 1.3) ax.set_ylim(0, max(values) * 1.3)
ax.spines["top"].set_visible(False) for s in ["top", "right"]:
ax.spines["right"].set_visible(False) ax.spines[s].set_visible(False)
ax.spines["left"].set_color("#ddd") for s in ["left", "bottom"]:
ax.spines["bottom"].set_color("#ddd") ax.spines[s].set_color("#ddd")
ax.tick_params(colors="#888") ax.tick_params(colors="#888")
ax.grid(axis="y", alpha=0.3, color="#ddd", zorder=0) ax.grid(axis="y", alpha=0.3, color="#ddd", zorder=0)
plt.tight_layout() plt.tight_layout()
fig.savefig(save_path, dpi=180, bbox_inches="tight", facecolor="#f8f4f0") fig.savefig(save_path, dpi=180, bbox_inches="tight", facecolor="#f8f4f0")
plt.close() plt.close()
@@ -233,147 +204,80 @@ def _generate_problem_chart(handle: str, rating_dist: dict, save_path: str) -> b
def _generate_tag_chart(handle: str, tag_dist: dict, save_path: str) -> bool: def _generate_tag_chart(handle: str, tag_dist: dict, save_path: str) -> bool:
"""生成标签分布饼图"""
try: try:
import matplotlib import matplotlib
matplotlib.use("Agg") matplotlib.use("Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
except ImportError: except ImportError:
return False return False
if not tag_dist: if not tag_dist:
return False return False
labels = list(tag_dist.keys()) labels = list(tag_dist.keys())
values = list(tag_dist.values()) values = list(tag_dist.values())
colors = ["#e8785a", "#f0a030", "#4a90d9", "#3eb86b", "#9b59b6", colors = ["#e8785a", "#f0a030", "#4a90d9", "#3eb86b", "#9b59b6",
"#1abc9c", "#e74c3c", "#3498db", "#f39c12", "#2ecc71", "#95a5a6"] "#1abc9c", "#e74c3c", "#3498db", "#f39c12", "#2ecc71", "#95a5a6"]
fig, ax = plt.subplots(figsize=(8, 6)) fig, ax = plt.subplots(figsize=(8, 6))
fig.patch.set_facecolor("#f8f4f0") fig.patch.set_facecolor("#f8f4f0")
wedges, texts, autotexts = ax.pie(values, labels=None, autopct="%1.0f%%",
wedges, texts, autotexts = ax.pie( startangle=90, pctdistance=0.78,
values, labels=None, autopct="%1.0f%%", colors=colors[:len(labels)],
startangle=90, pctdistance=0.78, wedgeprops=dict(width=0.4, edgecolor="white", linewidth=2),
colors=colors[:len(labels)], textprops=dict(fontsize=11, fontweight="bold"))
wedgeprops=dict(width=0.4, edgecolor="white", linewidth=2), ax.set_title(f"{handle} · Tags", fontsize=17, fontweight="bold", color="#d06040", pad=20)
textprops=dict(fontsize=11, fontweight="bold"),
)
ax.set_title(f"{handle} · Tags Distribution", fontsize=17, fontweight="bold", color="#d06040", pad=20)
ax.legend(wedges, [f"{l} ({v})" for l, v in zip(labels, values)], ax.legend(wedges, [f"{l} ({v})" for l, v in zip(labels, values)],
title="Tags", title_fontsize=12, title="Tags", title_fontsize=12,
loc="center left", bbox_to_anchor=(1, 0.5), loc="center left", bbox_to_anchor=(1, 0.5),
fontsize=10, framealpha=0.9) fontsize=10, framealpha=0.9)
plt.tight_layout(rect=[0, 0.03, 0.75, 0.97]) plt.tight_layout(rect=[0, 0.03, 0.75, 0.97])
fig.savefig(save_path, dpi=180, bbox_inches="tight", facecolor="#f8f4f0") fig.savefig(save_path, dpi=180, bbox_inches="tight", facecolor="#f8f4f0")
plt.close() plt.close()
return True return True
def _combine_charts(chart_paths: list, save_path: str) -> bool: def _combine_charts(paths: list, save_path: str) -> bool:
"""合并多张图为一张大图"""
try: try:
from PIL import Image from PIL import Image
except ImportError: except ImportError:
return False return False
imgs = [Image.open(p) for p in paths if os.path.exists(p)]
imgs = []
for p in chart_paths:
if os.path.exists(p):
imgs.append(Image.open(p))
if not imgs: if not imgs:
return False return False
width = max(img.width for img in imgs) width = max(img.width for img in imgs)
resized = [] resized = []
for img in imgs: for img in imgs:
ratio = width / img.width ratio = width / img.width
resized.append(img.resize((width, int(img.height * ratio)), Image.LANCZOS)) resized.append(img.resize((width, int(img.height * ratio)), Image.LANCZOS))
total_h = sum(img.height for img in resized) + 10 * (len(resized) - 1)
total_height = sum(img.height for img in resized) + 10 * (len(resized) - 1) combined = Image.new("RGB", (width, total_h), "#f8f4f0")
combined = Image.new("RGB", (width, total_height), "#f8f4f0")
y = 0 y = 0
for img in resized: for img in resized:
combined.paste(img, (0, y)) combined.paste(img, (0, y))
y += img.height + 10 y += img.height + 10
combined.save(save_path, quality=92) combined.save(save_path, quality=92)
return True return True
def _build_text_report(handle: str, user_info: dict, history: list, analysis: dict) -> str: # ---------- 主插件类 ----------
"""构建文本分析报告""" @register("astrbot_plugin_cf_query", "ATRI (YHN-04B-009)", "CF 查分与刷题分析,支持绑定、图表、报告", "1.0.0", "https://gitea.kronecker.cc/Kronecker/ATRI-NOTES")
rating = user_info.get("rating", 0)
max_rating = user_info.get("maxRating", 0)
rank = user_info.get("rank", "unrated")
max_rank = user_info.get("maxRank", "unrated")
total_contests = len(history)
wins = sum(1 for c in history if c["newRating"] > c["oldRating"])
losses = sum(1 for c in history if c["newRating"] < c["oldRating"])
best_rank = min((c["rank"] for c in history), default="-")
s = analysis
ac_rate = f"{s['ac'] / s['total'] * 100:.1f}%" if s["total"] > 0 else "-"
lines = [
f"📊 CF Profile: {handle}",
f"━━━━━━━━━━━━━━━━━━━━",
f"Rating: {rating} ({rank.title()})",
f"最高: {max_rating} ({max_rank.title()})",
f"参赛: {total_contests}场 | 涨{win if (win := wins) else 0}{loss if (loss := losses) else 0}",
f"最佳排名: #{best_rank}",
f"━━━━━━━━━━━━━━━━━━━━",
f"📝 刷题统计",
f"提交: {s['total']} | AC: {s['ac']} | AC率: {ac_rate}",
f"通过不同题目: {s['solved_count']}",
]
if s["rating_dist"]:
lines.append("━━━━━━━━━━━━━━━━━━━━")
lines.append("🎯 难度分布:")
for r, c in sorted(s["rating_dist"].items()):
lbl = "Unrated" if r == 0 else str(r)
bar = "" * c
lines.append(f" {lbl:>7}: {bar} {c}")
if s["recent_ac"]:
lines.append("━━━━━━━━━━━━━━━━━━━━")
lines.append("🔥 最近AC:")
for a in s["recent_ac"][:5]:
rtg = f"[{a['rating']}]" if a["rating"] else "[?]"
lines.append(f"{a['pid']} {rtg} {a['name']}")
return "\n".join(lines)
# ---------- 插件主类 ----------
class CFQueryPlugin(Star): class CFQueryPlugin(Star):
def __init__(self, context=None, config: dict = None): def __init__(self, context: Context, config: dict = None):
super().__init__(context, config) super().__init__(context, config)
self.config = config or {} self.config = config or {}
self.temp_dir = Path("/AstrBot/data/temp") self.temp_dir = Path("/AstrBot/data/temp")
# ========== 用户绑定 ========== # ========== 绑定 ==========
@register("cfbind", "绑定CF账号到QQ", "cfbind <CF用户名> 或 cfbind 用户名") @filter.command("cfbind")
async def cf_bind(self, event: AstrMessageEvent): async def cf_bind(self, event: AstrMessageEvent):
"""绑定CF账号""" """绑定CF账号cfbind <用户名>"""
args = event.get_args() args = event.get_args()
if not args: if not args:
yield CommandResult().message("❌ 用法: cfbind <CF用户名>") yield event.plain_result("❌ 用法: cfbind <CF用户名>")
return return
handle = args[0].strip() handle = args[0].strip()
# 验证handle是否存在
info = _query_user_info(handle) info = _query_user_info(handle)
if not info: if not info:
yield CommandResult().message(f"❌ CF用户 {handle} 不存在,请检查用户名") yield event.plain_result(f"❌ CF用户 {handle} 不存在,请检查用户名")
return return
uid = str(event.get_sender_id()) uid = str(event.get_sender_id())
bindings = _load_bindings() bindings = _load_bindings()
bindings[uid] = { bindings[uid] = {
@@ -382,129 +286,136 @@ class CFQueryPlugin(Star):
"rating": info.get("rating", 0), "rating": info.get("rating", 0),
} }
_save_bindings(bindings) _save_bindings(bindings)
rating = info.get("rating", 0) rating = info.get("rating", 0)
rank = info.get("rank", "unrated").title() rank = info.get("rank", "unrated").title()
yield CommandResult().message(f"✅ 绑定成功!\n{handle} | Rating: {rating} ({rank})") yield event.plain_result(f"✅ 绑定成功!\n{handle} | Rating: {rating} ({rank})")
@register("cfunbind", "解绑CF账号", "cfunbind") @filter.command("cfunbind")
async def cf_unbind(self, event: AstrMessageEvent): async def cf_unbind(self, event: AstrMessageEvent):
"""解绑CF账号""" """解绑CF账号"""
uid = str(event.get_sender_id()) uid = str(event.get_sender_id())
bindings = _load_bindings() bindings = _load_bindings()
if uid not in bindings: if uid not in bindings:
yield CommandResult().message("❌ 你还没有绑定CF账号") yield event.plain_result("❌ 你还没有绑定CF账号")
return return
handle = bindings[uid]["handle"] yield event.plain_result(f"✅ 已解绑 {bindings[uid]['handle']}")
del bindings[uid] del bindings[uid]
_save_bindings(bindings) _save_bindings(bindings)
yield CommandResult().message(f"✅ 已解绑 {handle}")
@register("cfwho", "查看已绑定的CF账号", "cfwho") @filter.command("cfwho")
async def cf_who(self, event: AstrMessageEvent): async def cf_who(self, event: AstrMessageEvent):
"""查看自己的绑定""" """查看绑定信息"""
uid = str(event.get_sender_id()) uid = str(event.get_sender_id())
bindings = _load_bindings() bindings = _load_bindings()
if uid not in bindings: if uid not in bindings:
yield CommandResult().message("❌ 你还没有绑定CF账号使用 cfbind <用户名> 绑定") yield event.plain_result("❌ 你还没有绑定CF账号使用 cfbind <用户名> 绑定")
return return
info = bindings[uid] info = bindings[uid]
yield CommandResult().message(f"🔗 已绑定: {info['handle']} (绑定时间: {time.strftime('%Y-%m-%d', time.localtime(info['bind_time']))})") t = time.strftime('%Y-%m-%d', time.localtime(info['bind_time']))
yield event.plain_result(f"🔗 已绑定: {info['handle']} ({t})")
# ========== CF 查询 ========== @filter.command("cftop")
@register("cf", "查询CF用户信息并生成报告", "cf <用户名> 或 cf查已绑定用户") async def cf_top(self, event: AstrMessageEvent):
"""群内CF排行榜"""
bindings = _load_bindings()
if not bindings:
yield event.plain_result("📭 还没有人绑定CF账号")
return
users = []
for uid, info in bindings.items():
handle = info["handle"]
fresh = _query_user_info(handle)
rating = fresh.get("rating", 0) if fresh else info.get("rating", 0)
users.append((handle, rating))
time.sleep(0.5)
users.sort(key=lambda x: x[1], reverse=True)
lines = ["🏆 群内CF排行榜", "━━━━━━━━━━━━━━"]
emojis = ["🥇", "🥈", "🥉"]
for i, (handle, rating) in enumerate(users[:10]):
emo = emojis[i] if i < 3 else f"{i+1}."
lines.append(f"{emo} {handle}{rating}")
yield event.plain_result("\n".join(lines))
# ========== 主查询 ==========
@filter.command("cf")
async def cf_query(self, event: AstrMessageEvent): async def cf_query(self, event: AstrMessageEvent):
"""查询CF用户信息""" """查询CF用户cf <用户名> 或 cf查已绑定"""
args = event.get_args() args = event.get_args()
# 确定要查的handle
handle = None handle = None
if args: if args:
handle = args[0].strip() handle = args[0].strip()
else: else:
# 尝试从绑定中获取
uid = str(event.get_sender_id()) uid = str(event.get_sender_id())
bindings = _load_bindings() bindings = _load_bindings()
if uid in bindings: if uid in bindings:
handle = bindings[uid]["handle"] handle = bindings[uid]["handle"]
if not handle: if not handle:
yield CommandResult().message("❌ 用法: cf <用户名> 或先用 cfbind 绑定后直接 cf") yield event.plain_result("❌ 用法: cf <用户名> 或先用 cfbind 绑定后直接 cf")
return return
yield CommandResult().message(f"🔍 正在查询 {handle} 的数据...") yield event.plain_result(f"🔍 正在查询 {handle} 的数据...")
# 并行获取数据
user_info = _query_user_info(handle) user_info = _query_user_info(handle)
if not user_info: if not user_info:
yield CommandResult().message(f"❌ 查询失败,用户 {handle} 不存在或网络错误") yield event.plain_result(f"❌ 查询失败,用户 {handle} 不存在或网络异常")
return return
history = _query_rating_history(handle) history = _query_rating_history(handle)
submissions = _query_submissions(handle, 200) submissions = _query_submissions(handle, 200)
analysis = _analyze_submissions(submissions) analysis = _analyze_submissions(submissions)
# 生成文本报告 # --- 文本报告 ---
text_report = _build_text_report(handle, user_info, history, analysis) rating = user_info.get("rating", 0)
yield CommandResult().message(text_report) max_r = user_info.get("maxRating", 0)
rank = user_info.get("rank", "unrated").title()
wins = sum(1 for c in history if c["newRating"] > c["oldRating"])
losses = sum(1 for c in history if c["newRating"] < c["oldRating"])
best_rank = min((c["rank"] for c in history), default="-")
ac_rate = f"{analysis['ac']/analysis['total']*100:.1f}%" if analysis["total"] > 0 else "-"
lines = [
f"📊 CF Profile: {handle}",
f"━━━━━━━━━━━━━━━━━━━━",
f"Rating: {rating} ({rank}) | 最高: {max_r}",
f"参赛: {len(history)}场 | 涨{wins}{losses} | 最佳排名: #{best_rank}",
f"━━━━━━━━━━━━━━━━━━━━",
f"提交: {analysis['total']} | AC: {analysis['ac']} | AC率: {ac_rate}",
f"通过不同题目: {analysis['solved_count']}",
]
if analysis["rating_dist"]:
lines.append("━━━━━━━━━━━━━━━━━━━━")
lines.append("🎯 难度分布:")
for r, c in sorted(analysis["rating_dist"].items()):
lbl = "Unrated" if r == 0 else str(r)
lines.append(f" {lbl:>7}: {''*c} {c}")
if analysis["recent_ac"]:
lines.append("━━━━━━━━━━━━━━━━━━━━")
lines.append("🔥 最近AC:")
for a in analysis["recent_ac"][:5]:
rtg = f"[{a['rating']}]" if a["rating"] else "[?]"
lines.append(f"{a['pid']} {rtg} {a['name']}")
yield event.plain_result("\n".join(lines))
# --- 图表 ---
ts = int(time.time())
charts = []
rc = str(self.temp_dir / f"cf_rating_{handle}_{ts}.png")
if _generate_rating_chart(handle, history, rc):
charts.append(rc)
pc = str(self.temp_dir / f"cf_problems_{handle}_{ts}.png")
if _generate_problem_chart(handle, analysis["rating_dist"], pc):
charts.append(pc)
tc = str(self.temp_dir / f"cf_tags_{handle}_{ts}.png")
if _generate_tag_chart(handle, analysis["tag_dist"], tc):
charts.append(tc)
if not charts:
return
# 生成图表
timestamp = int(time.time())
chart_files = []
style = self.config.get("default_chart_style", "combined") style = self.config.get("default_chart_style", "combined")
if style == "combined" and len(charts) >= 2:
# Rating柱状图 combined = str(self.temp_dir / f"cf_combined_{handle}_{ts}.png")
rating_chart = str(self.temp_dir / f"cf_rating_{handle}_{timestamp}.png") if _combine_charts(charts, combined):
if _generate_rating_chart(handle, history, rating_chart): yield event.image_result(combined)
chart_files.append(rating_chart)
# 难度柱状图
problem_chart = str(self.temp_dir / f"cf_problems_{handle}_{timestamp}.png")
if _generate_problem_chart(handle, analysis["rating_dist"], problem_chart):
chart_files.append(problem_chart)
# 标签饼图
tag_chart = str(self.temp_dir / f"cf_tags_{handle}_{timestamp}.png")
if _generate_tag_chart(handle, analysis["tag_dist"], tag_chart):
chart_files.append(tag_chart)
if not chart_files:
yield CommandResult().message("📊 数据不足以生成图表")
return
if style == "combined" and len(chart_files) >= 2:
combined_path = str(self.temp_dir / f"cf_combined_{handle}_{timestamp}.png")
if _combine_charts(chart_files, combined_path):
yield CommandResult().chain().image(combined_path)
return return
for c in charts:
# 分开发送 yield event.image_result(c)
for cf in chart_files:
yield CommandResult().chain().image(cf)
@register("cftop", "查看群内已绑定用户的CF排名", "cftop")
async def cf_top(self, event: AstrMessageEvent):
"""查看群内CF排名"""
bindings = _load_bindings()
if not bindings:
yield CommandResult().message("📭 还没有人绑定CF账号")
return
# 刷新所有已绑用户的rating
users = []
for uid, info in bindings.items():
handle = info["handle"]
fresh = _query_user_info(handle)
rating = fresh.get("rating", 0) if fresh else info.get("rating", 0)
users.append((handle, rating, uid))
time.sleep(0.5) # CF API限速
users.sort(key=lambda x: x[1], reverse=True)
lines = ["🏆 群内CF排行榜", "━━━━━━━━━━━━━━"]
rank_emoji = ["🥇", "🥈", "🥉"]
for i, (handle, rating, uid) in enumerate(users[:10]):
emo = rank_emoji[i] if i < 3 else f"{i+1}."
lines.append(f"{emo} {handle}{rating}")
yield CommandResult().message("\n".join(lines))