295 lines
10 KiB
Python
295 lines
10 KiB
Python
"""查询编排:目标解析 → 取数 → 加工 → 渲染,返回给 Trigger 层发送的结果。
|
||||
|
|
|
|||
|
|
只产出结果对象(文本/图片字节),不直接发消息;"正在查询…"等进度提示通过
|
|||
|
|
`notify` 回调交给 Trigger 层发送,本层保持与 nonebot 消息 API 解耦。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import asyncio
|
|||
|
|
import time
|
|||
|
|
from dataclasses import dataclass
|
|||
|
|
from collections.abc import Awaitable, Callable
|
|||
|
|
|
|||
|
|
from nonebot import logger
|
|||
|
|
|
|||
|
|
from ..repository import Bf2042SettingsManager, UserManager
|
|||
|
|
from ..utils.args import (
|
|||
|
|
BF2042_ERROR_HINT,
|
|||
|
|
BF_GAME_NAMES,
|
|||
|
|
parse_bf2042_args,
|
|||
|
|
)
|
|||
|
|
from ..utils.render_bf2042 import build_bf2042_career_card, build_bf2042_list_card
|
|||
|
|
from ..utils.render_ea import build_bf6_stats_card, build_stats_card
|
|||
|
|
from ..utils.text import convert_to_hours
|
|||
|
|
from .fetchers.bf2042 import (
|
|||
|
|
bf2042_list_title,
|
|||
|
|
get_bf2042_gamemodes,
|
|||
|
|
get_bf2042_info,
|
|||
|
|
get_bf2042_list,
|
|||
|
|
)
|
|||
|
|
from .fetchers.gametools import (
|
|||
|
|
fetch_all_stats,
|
|||
|
|
get_bf6_rank,
|
|||
|
|
get_player_info_by_name,
|
|||
|
|
player_display_name,
|
|||
|
|
)
|
|||
|
|
from .processing import (
|
|||
|
|
get_best_classes,
|
|||
|
|
get_best_weapon_and_best_vehicle,
|
|||
|
|
get_vehicle_destroyed,
|
|||
|
|
stats_calculator,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
Notify = Callable[[str], Awaitable[None]]
|
|||
|
|
|
|||
|
|
_PLATFORM = "pc"
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class QueryReply:
|
|||
|
|
"""一次查询的最终回复:文本 +(可选)PNG 图片字节。"""
|
|||
|
|
|
|||
|
|
text: str
|
|||
|
|
image: bytes | None = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def run_query(
|
|||
|
|
*, cmd: str, input_text: str, qq_id: str, notify: Notify
|
|||
|
|
) -> QueryReply:
|
|||
|
|
"""`/bft` 及全部别名共用的查询入口。
|
|||
|
|
|
|||
|
|
cmd: 实际输入的游戏别名(bf3/bf4/bf1/bfv/bf2042/bf6)。
|
|||
|
|
input_text: 命令参数(EAID 或 BF2042 子命令),可为空。
|
|||
|
|
qq_id: 触发者 QQ,用于读绑定与取头像。
|
|||
|
|
notify: 进度提示回调("正在查询…")。
|
|||
|
|
"""
|
|||
|
|
started = time.time()
|
|||
|
|
if cmd not in BF_GAME_NAMES:
|
|||
|
|
return QueryReply(
|
|||
|
|
"指令异常:请使用 bf3 / bf4 / bf1 / bfv / bf2042 / bf6(可带 EAID)"
|
|||
|
|
)
|
|||
|
|
if cmd == "bf2042":
|
|||
|
|
reply = await _run_bf2042(input_text, qq_id, notify)
|
|||
|
|
else:
|
|||
|
|
reply = await _run_ea(cmd, input_text, qq_id, notify)
|
|||
|
|
if reply.image is not None:
|
|||
|
|
reply.text = f"本次查询耗时:{round(time.time() - started, 3)}s"
|
|||
|
|
return reply
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _resolve_bound_record(qq_id: str) -> QueryReply | dict:
|
|||
|
|
"""无参数查询时取绑定记录;未绑定返回给用户的提示。"""
|
|||
|
|
content = UserManager().get_user_by_qq(qq_id)
|
|||
|
|
if not content:
|
|||
|
|
return QueryReply(
|
|||
|
|
"未检测到id,也未检测到绑定记录,请使用 绑定 EAID 进行绑定操作"
|
|||
|
|
)
|
|||
|
|
return content
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _run_ea(cmd: str, input_text: str, qq_id: str, notify: Notify) -> QueryReply:
|
|||
|
|
"""EA 系(BF3/4/1/5/6):gametools 取数 + 卡片渲染。"""
|
|||
|
|
if not input_text:
|
|||
|
|
content = _resolve_bound_record(qq_id)
|
|||
|
|
if isinstance(content, QueryReply):
|
|||
|
|
return content
|
|||
|
|
player = content["ea_player_name"]
|
|||
|
|
player_id = content["ea_player_id"]
|
|||
|
|
user_id = content["ea_user_id"]
|
|||
|
|
else:
|
|||
|
|
content = await get_player_info_by_name(input_text, _PLATFORM)
|
|||
|
|
if "errors" in content:
|
|||
|
|
return QueryReply("未找到玩家,请检查是否拼写正确")
|
|||
|
|
player_id = content["personaId"]
|
|||
|
|
user_id = content["userId"]
|
|||
|
|
player = player_display_name(content)
|
|||
|
|
await notify(f"正在查询 {player} 的 {BF_GAME_NAMES[cmd]} 数据,请耐心等待")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
rank = 0
|
|||
|
|
if cmd == "bf6":
|
|||
|
|
play_stat, rank = await asyncio.gather(
|
|||
|
|
fetch_all_stats(cmd, player, player_id, user_id, _PLATFORM),
|
|||
|
|
get_bf6_rank(player, player_id, user_id, _PLATFORM),
|
|||
|
|
)
|
|||
|
|
logger.info(f"等级:{rank}")
|
|||
|
|
else:
|
|||
|
|
play_stat = await fetch_all_stats(
|
|||
|
|
cmd, player, player_id, user_id, _PLATFORM
|
|||
|
|
)
|
|||
|
|
if "errors" in play_stat:
|
|||
|
|
logger.warning(play_stat["errors"][0])
|
|||
|
|
return QueryReply(str(play_stat["errors"][0]))
|
|||
|
|
|
|||
|
|
img = await _render_ea_card(cmd, play_stat, rank, player, qq_id)
|
|||
|
|
if isinstance(img, QueryReply):
|
|||
|
|
return img
|
|||
|
|
return QueryReply("", image=img)
|
|||
|
|
except Exception as e: # noqa: BLE001 - 兜底给用户可见提示,明细写日志
|
|||
|
|
logger.exception(f"异常:{e}")
|
|||
|
|
return QueryReply("查询异常,请稍后再试")
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _render_ea_card(cmd: str, play_stat, rank, player, qq_id):
|
|||
|
|
"""渲染卡片;返回 PNG 字节,或对用户的错误提示(QueryReply)。"""
|
|||
|
|
weapon, vehicle = await get_best_weapon_and_best_vehicle(play_stat)
|
|||
|
|
pid = play_stat["userId"]
|
|||
|
|
kd = play_stat["killDeath"]
|
|||
|
|
kpm = play_stat["killsPerMinute"]
|
|||
|
|
acc = play_stat["accuracy"]
|
|||
|
|
# 战地3 特化
|
|||
|
|
if cmd == "bf3":
|
|||
|
|
head_shots = play_stat["headShots"]
|
|||
|
|
else:
|
|||
|
|
head_shots = play_stat["headshots"]
|
|||
|
|
time_play = convert_to_hours(play_stat["timePlayed"])
|
|||
|
|
logger.info(f"游玩时长监测:{time_play}")
|
|||
|
|
if time_play == 0:
|
|||
|
|
return QueryReply("查询接口异常,请等待一段时间后查询")
|
|||
|
|
|
|||
|
|
kills = int(play_stat["kills"])
|
|||
|
|
kill_assists = int(play_stat["killAssists"])
|
|||
|
|
revives = int(play_stat["revives"])
|
|||
|
|
wins = int(play_stat["wins"])
|
|||
|
|
loses = int(play_stat["loses"])
|
|||
|
|
best_weapon = weapon
|
|||
|
|
best_vehicle = vehicle
|
|||
|
|
best_class = play_stat["bestClass"]
|
|||
|
|
destroyed = await get_vehicle_destroyed(play_stat["vehicles"])
|
|||
|
|
|
|||
|
|
if cmd == "bf6":
|
|||
|
|
best_class = await get_best_classes(play_stat)
|
|||
|
|
captured = play_stat["objective"]["captured"]
|
|||
|
|
score = sum(item.get("score", 0) for item in play_stat["classes"])
|
|||
|
|
# 计算4兵种游玩时长
|
|||
|
|
seconds = sum(item.get("secondsPlayed", 0) for item in play_stat["classes"])
|
|||
|
|
kpm = round(kills / (int(seconds) / 60), 2)
|
|||
|
|
logger.info(f"击杀数:{kills},游玩分钟数:{seconds / 60},计算kpm值:{kpm}")
|
|||
|
|
spm = int(score / (int(seconds) / 60))
|
|||
|
|
logger.info(f"计算spm值:{spm}")
|
|||
|
|
img = await build_bf6_stats_card(
|
|||
|
|
game="bf6",
|
|||
|
|
qq_id=qq_id,
|
|||
|
|
player=player,
|
|||
|
|
pid=pid,
|
|||
|
|
rank=rank,
|
|||
|
|
kd=kd,
|
|||
|
|
kpm=kpm,
|
|||
|
|
spm=spm,
|
|||
|
|
acc=acc,
|
|||
|
|
head_shots=head_shots,
|
|||
|
|
time_play=time_play,
|
|||
|
|
kills=kills,
|
|||
|
|
kill_assists=kill_assists,
|
|||
|
|
revives=revives,
|
|||
|
|
repairs=play_stat["repairs"],
|
|||
|
|
captured=captured,
|
|||
|
|
score=score,
|
|||
|
|
wins=wins,
|
|||
|
|
loses=loses,
|
|||
|
|
destroyed=destroyed,
|
|||
|
|
best_weapon=best_weapon,
|
|||
|
|
best_vehicle=best_vehicle,
|
|||
|
|
best_class=best_class,
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
spm = play_stat["scorePerMinute"]
|
|||
|
|
longest_head_shot = play_stat["longestHeadShot"]
|
|||
|
|
highest_ill_streak = play_stat["highestKillStreak"]
|
|||
|
|
stat_data, level_designation = await stats_calculator(play_stat, cmd)
|
|||
|
|
img = await build_stats_card(
|
|||
|
|
cmd,
|
|||
|
|
player,
|
|||
|
|
pid,
|
|||
|
|
kd,
|
|||
|
|
kpm,
|
|||
|
|
spm,
|
|||
|
|
acc,
|
|||
|
|
head_shots,
|
|||
|
|
play_stat["rank"],
|
|||
|
|
time_play,
|
|||
|
|
kills,
|
|||
|
|
kill_assists,
|
|||
|
|
revives,
|
|||
|
|
wins,
|
|||
|
|
loses,
|
|||
|
|
destroyed,
|
|||
|
|
best_weapon,
|
|||
|
|
best_vehicle,
|
|||
|
|
best_class,
|
|||
|
|
longest_head_shot,
|
|||
|
|
highest_ill_streak,
|
|||
|
|
stat_data,
|
|||
|
|
level_designation,
|
|||
|
|
qq_id=qq_id,
|
|||
|
|
)
|
|||
|
|
if img is None:
|
|||
|
|
return QueryReply("图片渲染失败,请稍后再试")
|
|||
|
|
return img.getvalue()
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _run_bf2042(input_text: str, qq_id: str, notify: Notify) -> QueryReply:
|
|||
|
|
"""BF2042 独立流程:tracker.gg 浏览器通道。
|
|||
|
|
|
|||
|
|
与其它战地不同,BF2042 不经过 EA 接口,所以玩家名要么来自指令参数,
|
|||
|
|
要么来自绑定记录,不需要 personaId/userId。
|
|||
|
|
|
|||
|
|
指令形式:
|
|||
|
|
/bf2042 [EAID] 生涯卡
|
|||
|
|
/bf2042 武器|载具|装备 [EAID] 榜单卡
|
|||
|
|
/bf2042 模式 [EAID] 各模式明细
|
|||
|
|
"""
|
|||
|
|
target = parse_bf2042_args(input_text)
|
|||
|
|
player = target.player
|
|||
|
|
if not player:
|
|||
|
|
content = _resolve_bound_record(qq_id)
|
|||
|
|
if isinstance(content, QueryReply):
|
|||
|
|
return content
|
|||
|
|
player = content["ea_player_name"]
|
|||
|
|
|
|||
|
|
if target.view == "list":
|
|||
|
|
what = f"{bf2042_list_title(target.kind)}榜"
|
|||
|
|
elif target.view == "gamemode":
|
|||
|
|
what = "各模式数据"
|
|||
|
|
else:
|
|||
|
|
what = "数据"
|
|||
|
|
await notify(f"正在查询 {player} 的 {BF_GAME_NAMES['bf2042']}{what},请耐心等待")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
if target.view == "list":
|
|||
|
|
result = await get_bf2042_list(player, _PLATFORM, target.kind)
|
|||
|
|
elif target.view == "gamemode":
|
|||
|
|
result = await get_bf2042_gamemodes(player, _PLATFORM)
|
|||
|
|
else:
|
|||
|
|
result = await get_bf2042_info(player, _PLATFORM)
|
|||
|
|
except Exception as e: # noqa: BLE001 - 兜底,别让异常静默
|
|||
|
|
logger.exception(f"BF2042 查询异常:{e}")
|
|||
|
|
return QueryReply("BF2042 查询异常,请稍后再试")
|
|||
|
|
|
|||
|
|
if not result.ok:
|
|||
|
|
logger.warning(f"BF2042 查询失败({result.reason}):{result.detail}")
|
|||
|
|
hint = BF2042_ERROR_HINT.get(result.reason, "查询失败,请稍后再试")
|
|||
|
|
return QueryReply(f"BF2042 {hint}")
|
|||
|
|
|
|||
|
|
info = result.data if isinstance(result.data, dict) else {}
|
|||
|
|
if target.view == "career":
|
|||
|
|
# 新版生涯卡:旧版布局 + tracker.gg 数据映射;背景按支持者特权选
|
|||
|
|
from .processing import build_bf2042_career_card as build_card_slots
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
support = Bf2042SettingsManager().is_support(qq_id)
|
|||
|
|
except Exception: # noqa: BLE001 - 装饰性功能,失败退回公共背景
|
|||
|
|
support = False
|
|||
|
|
card = build_card_slots(
|
|||
|
|
info.get("_profile") or {},
|
|||
|
|
info.get("_weapons") or [],
|
|||
|
|
info.get("_vehicles") or [],
|
|||
|
|
info.get("_soldiers") or [],
|
|||
|
|
)
|
|||
|
|
card["_qq_id"] = qq_id
|
|||
|
|
card["_support"] = support
|
|||
|
|
img = build_bf2042_career_card(card)
|
|||
|
|
else:
|
|||
|
|
img = build_bf2042_list_card(info)
|
|||
|
|
return QueryReply("", image=img.getvalue())
|