import asyncio import time from typing import Any, Awaitable, Callable import httpx from nonebot import logger from ..config import config # 简单的进程内缓存 {key: (过期时间戳, 数据)},用于聚合战况时只打一次接口 _cache: dict[str, tuple[float, Any]] = {} def _headers(language: str | None = None) -> dict[str, str]: # 两个 X-Super-* 头必须都存在,否则公共实例(ValidateClients)会直接返回 400 lang = language or config.hd2_api_language return { "Accept-Language": f"{lang},en-US;q=0.5", "X-Super-Client": config.hd2_super_client, "X-Super-Contact": config.hd2_super_contact or config.hd2_super_client, } def _base_url() -> str: return config.hd2_api_base_url.rstrip("/") async def _get_json(url: str, language: str | None = None) -> Any: async with httpx.AsyncClient(timeout=15) as client: resp = await client.get(url, headers=_headers(language)) if resp.status_code != 200: raise httpx.HTTPStatusError( f"{resp.status_code} {resp.reason_phrase} for {url}\n{resp.text[:400]}", request=resp.request, response=resp, ) return resp.json() async def _cached(key: str, fetcher: Callable[[], Awaitable[Any]]) -> Any: now = time.time() hit = _cache.get(key) if hit and hit[0] > now: return hit[1] data = await fetcher() _cache[key] = (now + config.hd2_cache_ttl, data) return data # ---- 原始接口 fetchers(不带缓存,供各 get_* 包装) ---- async def _fetch_war() -> Any: return await _get_json(f"{_base_url()}/api/v1/war") async def _fetch_major_orders() -> Any: return await _get_json(f"{_base_url()}/api/v1/assignments") async def _fetch_campaigns(language: str | None) -> Any: return await _get_json(f"{_base_url()}/api/v1/campaigns", language) async def _fetch_planets(language: str | None) -> Any: return await _get_json(f"{_base_url()}/api/v1/planets", language) async def _fetch_planet_detail(index: int, language: str | None) -> Any: return await _get_json(f"{_base_url()}/api/v1/planets/{index}", language) async def _fetch_raw_status() -> Any: # 原始接口一次返回全部:planetActiveEffects / spaceStations / planetStatus 等 return await _get_json(f"{_base_url()}/raw/api/WarSeason/801/Status") # ---- 对外封装(各自带缓存,可单独调用) ---- async def get_war() -> Any: """星系战况概要(年代起止/阵营/影响倍率等)。""" return await _cached("war", _fetch_war) async def get_major_orders() -> Any: """重要指令(最高指令 / assignments,含任务目标、奖励、阵营)。""" return await _cached("assignments", _fetch_major_orders) async def get_campaigns(language: str | None = None) -> Any: """当前参与战役的星球列表(含每颗参战星球的进度/事件/危害等)。""" key = f"campaigns:{language or ''}" return await _cached(key, lambda: _fetch_campaigns(language)) async def get_planets(language: str | None = None) -> Any: """全星球表(每颗星的 biome/hazards/event/currentOwner/statistics 等)。""" key = f"planets:{language or ''}" return await _cached(key, lambda: _fetch_planets(language)) async def get_planet_detail(index: int, language: str | None = None) -> Any: """单星球详细情况(biome/hazards/event/statistics/attacking/regions 等)。""" key = f"planet:{index}:{language or ''}" return await _cached(key, lambda: _fetch_planet_detail(index, language)) async def get_raw_status() -> Any: """原始 WarSeason/801/Status(planetStatus/planetActiveEffects/spaceStations...)。""" return await _cached("raw_status", _fetch_raw_status) async def get_space_stations() -> list[Any]: """空间站(民主空间站 DSS 等)。""" raw = await get_raw_status() return list((raw or {}).get("spaceStations") or []) async def get_planet_effects(index: int | None = None) -> dict[int, list[int]]: """星球当前生效效果 {星球index: [galacticEffectId...]};传 index 时只返回该星球的集合。""" raw = await get_raw_status() effects: dict[int, list[int]] = {} for item in (raw or {}).get("planetActiveEffects") or []: if ( isinstance(item, dict) and item.get("index") is not None and item.get("galacticEffectId") is not None ): effects.setdefault(item["index"], []).append(item["galacticEffectId"]) if index is not None: return {index: effects.get(index, [])} return effects # ---- 聚合入口(渲染战报用;也可改为按需调用各 get_* 组装) ---- async def get_briefing_data() -> dict[str, Any]: """并发拉取战报所需的分组数据,带缓存,返回聚合结果。""" war, assignments, campaigns, planets, raw_status = await asyncio.gather( get_war(), get_major_orders(), get_campaigns(), get_planets(language="en-US"), # 用英文星名匹配高清地形图;currentOwner 等不受语言影响 get_raw_status(), ) logger.info("已获取 helldivers 战况数据") planet_english = { p["index"]: p.get("name") for p in (planets or []) if p.get("index") is not None and p.get("name") } # 星球当前效果:{星球index: [galacticEffectId...]} planet_effects: dict[int, list[int]] = {} for item in (raw_status or {}).get("planetActiveEffects") or []: if isinstance(item, dict) and item.get("index") is not None and item.get("galacticEffectId") is not None: planet_effects.setdefault(item["index"], []).append(item["galacticEffectId"]) # 空间站(民主空间站 DSS 等):独立展示,不混进普通星球效果 space_stations = (raw_status or {}).get("spaceStations") or [] return { "war": war, "assignments": assignments, "campaigns": campaigns, "planets": planets, "planet_english": planet_english, "planet_effects": planet_effects, "space_stations": space_stations, "fetched_at": int(time.time()), }