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:
1 parent
614e1d7e00
commit
a31aab00e8
752 files changed
+6241
-2506
No files matched your search
@@ -0,0 +1,132 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user