fix: handle partial match failures and add per-request timeout

- Add return_exceptions=True to asyncio.gather so one failed match
  doesn't crash the entire query; filter out failed results gracefully.
- Replace session-level timeout with per-request timeout (10s) in
  _api_request to avoid shared time budget across 9 requests.
- Remove session-level ClientTimeout that was too tight for batch.
- Add diagnostic logging at plugin init, query_stats entry, and
  data fetch stages to help trace agent interception issues.
- Support stadia platform in config schema description.
- Add _truncate_text for long player name image rendering.
- Clean up unused mode_key tuple element in mode_rows.
This commit is contained in:
sakuradairong
2026-05-17 19:11:43 +08:00
parent 57812483dd
commit 868ed2f1e6
2 changed files with 41 additions and 12 deletions

View File

@@ -5,7 +5,7 @@
"default": ""
},
"default_platform": {
"description": "默认查询平台,可选: steam | psn | xbox | kakao",
"description": "默认查询平台,可选: steam | psn | xbox | kakao | stadia",
"type": "string",
"default": "steam"
}

51
main.py
View File

@@ -101,6 +101,13 @@ def _text_w(draw: IDraw.ImageDraw, text: str, font: FreeTypeFont | FontClass) ->
bbox = draw.textbbox((0, 0), text, font=font)
return bbox[2] - bbox[0]
def _truncate_text(draw: IDraw.ImageDraw, text: str, font: FreeTypeFont | FontClass, max_px: int) -> str:
if _text_w(draw, text, font) <= max_px:
return text
while text and _text_w(draw, text + "", font) > max_px:
text = text[:-1]
return text + ""
def _render_image(
name: str,
@@ -115,7 +122,7 @@ def _render_image(
s = gm_stats.get(mode_key, {})
if s.get("roundsPlayed", 0) == 0:
continue
mode_rows.append((mode_key, mode_label, s))
mode_rows.append((mode_label, s))
match_rows = []
for match_data in match_results:
@@ -136,7 +143,7 @@ def _render_image(
total_h = (
PAD + H_HEADER + PAD + H_BAN_BAR
+ H_SEC_TITLE + len(mode_rows) * (H_MODE_ROW + 10)
+ (PAD + H_SEC_TITLE + len(match_rows) * (H_MATCH_ROW + 8) if match_rows else 0)
+ (PAD // 2 + H_SEC_TITLE + len(match_rows) * (H_MATCH_ROW + 8) if match_rows else 0)
+ H_FOOTER + PAD
)
@@ -150,9 +157,9 @@ def _render_image(
f_small = _load_font(15)
y = PAD
draw.rectangle([PAD, y, W - PAD, y + H_HEADER - 10], fill=CARD, outline=ACCENT, width=2)
draw.text((PAD + 18, y + 14), name, font=f_big, fill=ACCENT)
name_max_w = COL_W - 36 - _text_w(draw, "PUBG 战绩", f_med) - 28
draw.text((PAD + 18, y + 14), _truncate_text(draw, name, f_big, name_max_w), font=f_big, fill=ACCENT)
draw.text((PAD + 18, y + 50), f"[{platform.upper()}]", font=f_norm, fill=GRAY)
draw.text((W - PAD - 18 - _text_w(draw, "PUBG 战绩", f_med), y + 28), "PUBG 战绩", font=f_med, fill=ACCENT2)
y += H_HEADER + PAD // 2
@@ -169,7 +176,7 @@ def _render_image(
draw.line([(PAD, y + 30), (W - PAD, y + 30)], fill=ACCENT, width=1)
y += H_SEC_TITLE
for _, mode_label, s in mode_rows:
for mode_label, s in mode_rows:
rounds = s.get("roundsPlayed", 0)
wins = s.get("wins", 0)
top10 = s.get("top10s", 0)
@@ -351,10 +358,15 @@ async def _api_request(
url: str,
params: Optional[dict] = None,
retry: int = 0,
request_timeout: int = 10,
) -> dict:
for attempt in range(retry + 1):
try:
async with session.get(url, params=params) as resp:
async with session.get(
url,
params=params,
timeout=aiohttp.ClientTimeout(total=request_timeout),
) as resp:
if resp.status == 404:
raise PubgApiError("资源不存在 (404)")
if resp.status == 401:
@@ -393,6 +405,7 @@ class PubgPlugin(Star):
super().__init__(context)
self.config = config
self.api_base = "https://api.pubg.com/shards"
logger.info(f"[pubg_plugin] 插件已加载Pillow={'可用' if PIL_OK else '不可用(将回退为文字输出)'}")
if not PIL_OK:
logger.warning("[pubg_plugin] 未安装 Pillow将回退为文字输出。pip install Pillow")
@@ -410,6 +423,7 @@ class PubgPlugin(Star):
@filter.command("查ID")
@filter.command("查询")
async def query_stats(self, event: AstrMessageEvent):
logger.info(f"[pubg_plugin] query_stats 被调用: message={event.message_str!r}")
parts = event.message_str.strip().split()
if len(parts) < 2:
yield event.plain_result(
@@ -421,6 +435,7 @@ class PubgPlugin(Star):
player_name = parts[1]
platform = parts[2].lower() if len(parts) > 2 else self._get_platform()
logger.info(f"[pubg_plugin] 开始查询: player={player_name}, platform={platform}")
valid_platforms = {"steam", "psn", "xbox", "kakao", "stadia"}
if platform not in valid_platforms:
@@ -476,10 +491,7 @@ class PubgPlugin(Star):
"Accept": "application/vnd.api+json",
}
async with aiohttp.ClientSession(
headers=headers,
timeout=aiohttp.ClientTimeout(total=API_TIMEOUT),
) as session:
async with aiohttp.ClientSession(headers=headers) as session:
player_data = await _api_request(
session,
f"{self.api_base}/{platform}/players",
@@ -503,8 +515,9 @@ class PubgPlugin(Star):
.get("matches", {})
.get("data", [])
][:MATCH_LIMIT]
logger.info(f"[pubg_plugin] 获取该玩家的 {len(match_ids)} 场对局详情")
lifetime_data, *match_results = await asyncio.gather(
results = await asyncio.gather(
_api_request(
session,
f"{self.api_base}/{platform}/players/{player_id}/seasons/lifetime",
@@ -518,8 +531,24 @@ class PubgPlugin(Star):
)
for mid in match_ids
],
return_exceptions=True,
)
lifetime_data = results[0]
match_results_raw = results[1:]
if isinstance(lifetime_data, Exception):
if isinstance(lifetime_data, PubgApiError):
raise lifetime_data
raise PubgApiError(f"获取生涯数据失败: {lifetime_data}")
match_results = [
r for r in match_results_raw
if not isinstance(r, Exception)
]
if match_results_raw and not match_results:
logger.warning("[pubg_plugin] 所有对局详情获取失败,仅显示生涯数据")
gm_stats = lifetime_data["data"]["attributes"]["gameModeStats"]
player_info = PlayerInfo(
id=player_id,