refactor(bf-bot): 插件重构为 handlers/services/utils 分层,资源归入 res/

- 旧平铺模块(data/data_utils/database_op/image_builder*/get_bf6_data/
  gametools_bf6/bf6_data/img_utils)拆分为 handlers(绑定/查询)、
  services(fetchers/processing/query)、utils(渲染/参数)分层;
  新增 repository.py / config.py / paths.py
- 字体/图片/模板资源迁入 res/,新增 BF2042 卡面素材(bg/object_icon 等)
- browser_ext/:BF2042 扩展捕获桥(WS 直连兜底通道)源码入库
- 新增 hexi/web/src/plugins/bf_bot 前端管理页
- 新增 tests:test_bf_bot_helpers / repository / tracker_api

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
sansenhoshiandClaude Code committed 2026-10-13 17:53:06 +08:00
1 parent 614e1d7e00
commit a31aab00e8
752 files changed
+6241 -2506

No files matched your search

@@ -0,0 +1,301 @@
"""战绩加工:最佳武器/载具、载具摧毁数、六维雷达数据与称号评级。"""
from functools import reduce
from nonebot import logger
from ..utils.params import SPECIALIST_NAMES, interval_table, round_data
async def get_best_weapon_and_best_vehicle(player_stat):
weapon = sorted(player_stat["weapons"], key=lambda k: k["kills"], reverse=True)[0]
vehicle = sorted(player_stat["vehicles"], key=lambda k: k["kills"], reverse=True)[0]
return weapon, vehicle
async def get_best_classes(player_stat):
classes_data = player_stat["classes"]
data = [obj for obj in classes_data if obj.get("className") != "All"]
best_classes = max(data, key=lambda x: x["kills"])
best_classes_name = best_classes["className"]
return best_classes_name
async def get_vehicle_destroyed(player_stat):
vehicle = reduce(lambda acc, x: acc + x["destroyed"], player_stat, 0)
return vehicle
async def stats_calculator(player_stat, game):
per500_data = round_data[game]
# 场次(无直接数值反馈,采用胜利数+失败数)
match_played = player_stat["wins"] + player_stat["loses"]
kd = player_stat["killDeath"]
kpm = player_stat["killsPerMinute"]
kill_assists = player_stat["killAssists"]
if game == "bf3":
headshots = float(0)
else:
headshots = float(player_stat["headshots"].replace("%", ""))
spm = player_stat["scorePerMinute"]
revives = player_stat["revives"]
apr = kill_assists / match_played
rpr = revives / match_played
# 计算比值
# kd
ratio_kd = round(kd / per500_data["kd"], 2)
# kpm
ratio_kpm = round(kpm / per500_data["kpm"], 2)
# 爆头率
ratio_hs = round(headshots / 100 / per500_data["hs"], 2)
# 场均救援
ratio_rpr = round(rpr / per500_data["rpr"], 2)
# 场均助攻
ratio_apr = round(apr / per500_data["apr"], 2)
# 分钟得分
ratio_spm = round(spm / per500_data["spm"], 2)
logger.info(
"雷达图数据"
f"\n生涯KD比值:{ratio_kd}\n生涯Kpm比值:{ratio_kpm}\n生涯准度比值:{ratio_hs}"
f"\n场均救援比值:{ratio_rpr}\n场均助攻比值:{ratio_apr}\n分钟得分比值:{ratio_spm}"
)
stat_data = [ratio_kd, ratio_apr, ratio_hs, ratio_spm, ratio_kpm, ratio_rpr]
# 血腥度 kd*0.5 + kpm *0.4 +hs*0.1
player_blood = kd * 0.5 + kpm * 0.4 + headshots * 0.1
ratio_blood = player_blood / per500_data["blood"]
# 贡献度 场均助攻*0.4 + 场均救援*0.4 + spm*0.2
player_dedication = rpr * 0.4 + apr * 0.4 + spm * 0.2
ratio_dedication = player_dedication / per500_data["dedication"]
# 全能度 血腥度*0.5 + 贡献度*0.5
player_all_round = player_blood * 0.5 + player_dedication * 0.5
ratio_all_round = player_all_round / per500_data["all_round"]
logger.info(
"称号评级数据"
f"\n血腥度:{player_blood} 比值:{ratio_blood}"
f"\n贡献度:{player_dedication} 比值:{ratio_dedication}"
f"\n全能度:{player_all_round} 比值:{ratio_all_round}"
)
level_designation = get_level_designation(
ratio_blood, ratio_dedication, ratio_all_round
)
return stat_data, level_designation
def get_level_designation(blood, dedication, all_round):
feature_type = ""
final_max = max([blood, dedication, all_round])
if final_max == blood:
feature_type = "blood"
elif final_max == dedication:
feature_type = "dedication"
elif final_max == all_round:
feature_type = "all_round"
logger.info(f"最高能力:{feature_type}")
logger.info(f"最高能力值:{final_max}")
final_info = get_feature(final_max, feature_type)
return final_info
def get_feature(value, feature_type):
"""根据输入值和数值类别,返回对应特征值"""
for (low, high), features in interval_table.items():
if low <= value < high:
return features[feature_type]
raise ValueError(f"数值 {value} 不在任何区间内")
# --------------------------------------------------------------------------- #
# BF2042 生涯卡数据(旧 project 卡片槽位 → tracker.gg 数据映射)
# --------------------------------------------------------------------------- #
_PLACEHOLDER = "–"
def _seg_stat(seg: dict, *keys):
"""从段 stats 取第一个命中的显示值(displayValue 优先)。"""
stats = (seg or {}).get("stats") or {}
for key in keys:
entry = stats.get(key)
if isinstance(entry, dict):
dv = entry.get("displayValue")
if dv is not None:
return dv
v = entry.get("value")
if v is not None:
return v
elif entry is not None:
return entry
return _PLACEHOLDER
def _seg_value(seg: dict, *keys) -> float:
stats = (seg or {}).get("stats") or {}
for key in keys:
entry = stats.get(key)
if isinstance(entry, dict) and entry.get("value") is not None:
return float(entry["value"])
return 0.0
def _fmt_minutes_hours(minutes: float) -> str:
"""tracker timePlayed(分钟) → "X.XX" 小时字符串(旧卡习惯)。"""
try:
return f"{float(minutes) / 60:.2f}"
except (TypeError, ValueError):
return _PLACEHOLDER
def _s_level(level) -> str:
"""等级 S 前缀:tracker 的 298 → 'S199'(与旧 gametools 一致)。"""
try:
lv = int(level)
except (TypeError, ValueError):
return str(level)
return f"S{lv - 99}" if lv - 99 > 0 else str(lv)
def _top_rows(segments: list[dict], top: int) -> list[dict]:
"""武器/载具段 → 卡片 TOP 行(按击杀降序)。"""
ranked = sorted(segments or [], key=lambda s: _seg_value(s, "kills"), reverse=True)
rows = []
for seg in ranked[:top]:
meta = seg.get("metadata") or {}
rows.append(
{
"名称": (meta.get("name") or "").strip() or _PLACEHOLDER,
"类别": meta.get("category") or "",
"击杀": _seg_stat(seg, "kills"),
"爆头率": _seg_stat(seg, "headshotPercentage"),
"命中率": _seg_stat(seg, "shotsAccuracy"),
"KPM": _seg_stat(seg, "killsPerMinute"),
"时长": _fmt_minutes_hours(_seg_value(seg, "timePlayed")),
"摧毁": _seg_stat(seg, "destroyedWith"),
# 原始值给排序/调试用
"_kills": _seg_value(seg, "kills"),
}
)
return rows
def build_bf2042_career_card(
profile: dict,
weapon_segs: list[dict] | None,
vehicle_segs: list[dict] | None,
soldier_segs: list[dict] | None = None,
) -> dict:
"""tracker.gg 数据 → 旧卡片字段契约(dev/bf2042 的 data 结构)。
字段名沿用旧版(userName/killDeath/infantryKillDeath/secondsPlayed/
dividedKills/humanPrecentage/weapons[].headshots 等),渲染层照旧坐标绘制;
tracker 没有的数据(命中率/mvp/标记敌人数/处决等)填占位符 "–"。
"""
data = (profile or {}).get("data") or {}
segments = data.get("segments") or []
overview = next((s for s in segments if s.get("type") == "overview"), {})
info = data.get("platformInfo") or {}
kills = _seg_value(overview, "kills")
kd = _seg_value(overview, "kdRatio")
kpm = _seg_value(overview, "killsPerMinute")
human_kills = _seg_value(overview, "humanKills")
human_per = round(100.0 * human_kills / kills, 1) if kills else 0.0
ratio = (human_kills / kills) if kills else 0.0
level = _seg_stat(overview, "level")
try:
level_s = int(level) - 99
level = f"S {level_s:03d}" if level_s > 0 else f"{int(level):03d}"
except (TypeError, ValueError):
level = str(level)
# 真实 K/D、真实 KPM(旧卡算法:按"对人击杀占比"折算)
real_kd = round(kd * ratio, 2)
real_kpm = round(kpm * ratio, 2)
# 最佳专家(soldier 段→旧 classes/characterName 结构)
best = None
ranked = sorted(
soldier_segs or [], key=lambda s: _seg_value(s, "kills"), reverse=True
)
if ranked:
seg = ranked[0]
raw_name = ((seg.get("metadata") or {}).get("name") or "").strip()
best = {
"characterName": raw_name,
"displayName": SPECIALIST_NAMES.get(raw_name, raw_name),
"className": (seg.get("metadata") or {}).get("category") or "",
"kills": _seg_stat(seg, "kills"),
"kpm": _seg_stat(seg, "killsPerMinute"),
"killDeath": _seg_stat(seg, "kdRatio"),
"secondsPlayed": round(_seg_value(seg, "timePlayed")),
}
# 鉴定文案(旧阈值)
if kpm > 1.00 and kd > 2 and real_kd > 1.5:
verdict, verdict_color = "Pro哥\n你带我走吧T_T", "gold"
else:
verdict, verdict_color = "薯薯\n别拷打我了哥>_<", "skyblue"
def _objects(segs: list[dict], name_key: str, is_vehicle: bool) -> list[dict]:
rows = []
for seg in sorted(
segs or [], key=lambda x: _seg_value(x, "kills"), reverse=True
)[:5]:
meta = seg.get("metadata") or {}
rows.append(
{
name_key: (meta.get("name") or "").strip(),
"type": meta.get("category") or "",
"kills": _seg_stat(seg, "kills"),
"headshots": _seg_stat(seg, "headshotPercentage"),
"accuracy": _seg_stat(seg, "shotsAccuracy"),
"killsPerMinute": _seg_stat(seg, "killsPerMinute"),
"timeEquipped": round(_seg_value(seg, "timePlayed")),
"destroyed": _seg_stat(seg, "destroyedWith") if is_vehicle else "–",
}
)
return rows
return {
"userName": info.get("platformUserHandle") or _PLACEHOLDER,
"platform": info.get("platformSlug") or "origin",
"level": level,
"secondsPlayed": int(_seg_value(overview, "timePlayed") // 3600),
"matchesPlayed": _seg_stat(overview, "matches"),
"mvp": "–", # tracker 无
"bestSquad": "–", # tracker 无
"playerTakeDowns": _seg_stat(overview, "meleeKills"),
"kills": _seg_stat(overview, "kills"),
"deaths": _seg_stat(overview, "deaths"),
"killDeath": _seg_stat(overview, "kdRatio"),
"infantryKillDeath": round(kd * ratio, 2),
"killsPerMinute": _seg_stat(overview, "killsPerMinute"),
"killsPerMatch": _seg_stat(overview, "killsPerMatch"),
"headShots": _seg_stat(overview, "headshotPercentage"),
"accuracy": "–", # tracker overview 无整体命中率
"winPercent": _seg_stat(overview, "wlPercentage"),
"humanPrecentage": f"{human_per}%",
"dividedKills": {
"ai": _seg_stat(overview, "aiKills"),
"vehicle": _seg_stat(overview, "vehicleKills"),
"longDistance": "–", # tracker 无
"inRound": {"longDistance": "–"},
},
"revives": _seg_stat(overview, "revives"),
"enemiesSpotted": "–", # tracker 无
"vehiclesDestroyed": _seg_stat(overview, "vehiclesDestroyed"),
"bestClass": best,
"realKd": real_kd,
"realKpm": real_kpm,
"verdict": verdict,
"verdictColor": verdict_color,
"weapons": _objects(weapon_segs or [], "weaponName", False),
"vehicles": _objects(vehicle_segs or [], "vehicleName", True),
}