Files
HeXi/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/gametools.py
T
sansenhoshiandClaude Code a31aab00e8 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>
2026-10-13 17:53:06 +08:00

133 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""EA 系取数(api.gametools.network)。
- `fetch_all_stats`:bf3/bf4/bf1/bfv/bf6 的 `/all` 战绩接口(by-id 与 by-name
并发请求,谁先拿到有效数据用谁)。
- `get_player_info_by_name` / `get_player_info_by_ea_id`:`bfglobal/player` 查人,
绑定流程用。
- `get_bf6_rank`:`bf6/profile` 取等级(playerCard.rank)。
gametools 是同步 requests 的部分一律 `asyncio.to_thread`,避免卡事件循环。
"""
import asyncio
import json
import httpx
import requests
from nonebot import logger
_API = "https://api.gametools.network"
_HEADERS = {"accept": "application/json"}
async def fetch_all_stats(game, player, player_id, user_id, platform):
"""查 `{game}/all/`:by-id 与 by-name 并发,先成功(且无 errors)的胜出。"""
base = f"{_API}/{game}/all/?format_values=true"
url_by_id = (
f"{base}&playerid={player_id}&oid={user_id}"
f"&platform={platform}&skip_battlelog=false&lang=en-us"
)
url_by_name = (
f"{base}&name={player}&platform={platform}&skip_battlelog=false&lang=en-us"
)
async with httpx.AsyncClient(timeout=10.0) as client:
# 创建两个并发任务
task_id = asyncio.create_task(client.get(url_by_id))
task_name = asyncio.create_task(client.get(url_by_name))
# 等待第一个完成的任务
done, pending = await asyncio.wait(
[task_id, task_name], return_when=asyncio.FIRST_COMPLETED
)
# 取消未完成的任务
for task in pending:
task.cancel()
# 检查已完成的任务是否成功且无 errors
for task in done:
try:
response = await task
if response.status_code == 200:
data = response.json()
if "errors" not in data:
logger.info(
f"成功获取数据({'ID' if task is task_id else 'Name'}方式)"
)
return data
except (httpx.RequestError, ValueError) as e:
logger.warning(f"请求或解析失败: {e}")
# 如果所有尝试都失败,返回错误
return {"errors": ["未找到玩家"]}
async def _get_player_json(url: str) -> dict:
"""requests 放线程池 + 超时;网络/解析失败统一降级成 {"errors": [...]}。"""
try:
response = await asyncio.to_thread(
requests.get, url, headers=_HEADERS, timeout=10
)
return json.loads(response.text)
except Exception as err: # noqa: BLE001 - 调用方按 errors 分支处理
logger.warning(f"查询玩家信息失败: {err}")
return {"errors": [str(err)]}
def player_display_name(info: dict, fallback: str = "error") -> str:
"""从 bfglobal/player 响应里取展示名。
该接口实测有两种形态:一种给 personaName,另一种(同一玩家也会出现)只给
name + nucleusId;漏掉 name 时卡片会把玩家名显示成 "error"(老代码的问题)。
"""
return (
info.get("personaName")
or info.get("name")
or info.get("displayName")
or info.get("username")
or fallback
)
async def get_player_info_by_name(user_name, platform) -> dict:
url = (
f"{_API}/bfglobal/player/?name={user_name}"
f"&platform={platform}&skip_battlelog=false"
)
logger.info(f"查询用户请求url:{url}")
return await _get_player_json(url)
async def get_player_info_by_ea_id(player_id, user_id, platform) -> dict:
url = (
f"{_API}/bfglobal/player/?playerid={player_id}&oid={user_id}"
f"&platform={platform}&skip_battlelog=false"
)
return await _get_player_json(url)
async def get_bf6_rank(player, player_id, user_id, platform) -> int:
"""取BF6等级:gametools profile 接口返回 playerProfiles[0].playerCard.rank"""
url = (
f"{_API}/bf6/profile/?name={player}"
f"&playerid={player_id}&nucleus_id={user_id}&platform={platform}"
f"&skip_battlelog=true&lang=en-us"
)
try:
response = await asyncio.to_thread(
requests.get, url, headers=_HEADERS, timeout=10
)
profiles = response.json().get("playerProfiles") or []
except Exception as err: # noqa: BLE001 - 等级取不到就当 0,不打断整卡
logger.warning(f"查询BF6等级失败: {err}")
return 0
logger.info(f"BF6 profile 返回:{str(profiles)[:300]}")
if not isinstance(profiles, list) or not profiles:
logger.info(f"未搜索到用户: {player}")
return 0
profile = profiles[0] if isinstance(profiles[0], dict) else {}
# 该接口的真实字段:playerCard.rank 是等级数字,rankName 是称号名
rank = (profile.get("playerCard") or {}).get("rank") or 0
logger.info(f"BF6等级: {rank}(称号:{profile.get('rankName')})")
return rank