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,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