nonebot-plugin-R6States 1.0.0__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (16) hide show
  1. {nonebot_plugin_r6states-1.0.0 → nonebot_plugin_r6states-1.1.0}/PKG-INFO +3 -9
  2. {nonebot_plugin_r6states-1.0.0 → nonebot_plugin_r6states-1.1.0}/README.md +2 -8
  3. {nonebot_plugin_r6states-1.0.0 → nonebot_plugin_r6states-1.1.0}/pyproject.toml +1 -1
  4. {nonebot_plugin_r6states-1.0.0 → nonebot_plugin_r6states-1.1.0}/src/nonebot_plugin_R6States/__init__.py +41 -27
  5. {nonebot_plugin_r6states-1.0.0 → nonebot_plugin_r6states-1.1.0}/src/nonebot_plugin_R6States/cache.py +7 -15
  6. nonebot_plugin_r6states-1.1.0/src/nonebot_plugin_R6States/config.py +13 -0
  7. {nonebot_plugin_r6states-1.0.0 → nonebot_plugin_r6states-1.1.0}/src/nonebot_plugin_R6States/config_mannger.py +0 -11
  8. {nonebot_plugin_r6states-1.0.0 → nonebot_plugin_r6states-1.1.0}/src/nonebot_plugin_R6States/formatter.py +18 -3
  9. {nonebot_plugin_r6states-1.0.0 → nonebot_plugin_r6states-1.1.0}/src/nonebot_plugin_R6States/renderer.py +7 -14
  10. {nonebot_plugin_r6states-1.0.0 → nonebot_plugin_r6states-1.1.0}/src/nonebot_plugin_R6States/service.py +31 -10
  11. nonebot_plugin_r6states-1.0.0/src/nonebot_plugin_R6States/config.py +0 -12
  12. {nonebot_plugin_r6states-1.0.0 → nonebot_plugin_r6states-1.1.0}/LICENSE +0 -0
  13. {nonebot_plugin_r6states-1.0.0 → nonebot_plugin_r6states-1.1.0}/src/nonebot_plugin_R6States/assets/MonaSans.ttf +0 -0
  14. {nonebot_plugin_r6states-1.0.0 → nonebot_plugin_r6states-1.1.0}/src/nonebot_plugin_R6States/assets/NotoSansSC.ttf +0 -0
  15. {nonebot_plugin_r6states-1.0.0 → nonebot_plugin_r6states-1.1.0}/src/nonebot_plugin_R6States/r6data.py +0 -0
  16. {nonebot_plugin_r6states-1.0.0 → nonebot_plugin_r6states-1.1.0}/src/nonebot_plugin_R6States/storage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nonebot-plugin-R6States
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: 查询指定玩家的各项数据
5
5
  Author-Email: Siornya <wxh200607@outlook.com>
6
6
  License: MIT
@@ -43,16 +43,10 @@ Description-Content-Type: text/markdown
43
43
 
44
44
  可以直接将 `nonebot_plugin_R6States` 文件夹放入插件目录中。
45
45
 
46
- ### 基础指令
46
+ ### 数据查询
47
47
 
48
48
  ```text
49
- /R6 <player_id>
50
- ```
51
-
52
- ### 多人查询
53
-
54
- ```text
55
- /R6 -g <id1> <id2> ... <idN>
49
+ /R6 <player_ids...>
56
50
  ```
57
51
 
58
52
  ### 帮助信息
@@ -30,16 +30,10 @@
30
30
 
31
31
  可以直接将 `nonebot_plugin_R6States` 文件夹放入插件目录中。
32
32
 
33
- ### 基础指令
33
+ ### 数据查询
34
34
 
35
35
  ```text
36
- /R6 <player_id>
37
- ```
38
-
39
- ### 多人查询
40
-
41
- ```text
42
- /R6 -g <id1> <id2> ... <idN>
36
+ /R6 <player_ids...>
43
37
  ```
44
38
 
45
39
  ### 帮助信息
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-R6States"
3
- version = "1.0.0"
3
+ version = "1.1.0"
4
4
  description = "查询指定玩家的各项数据"
5
5
  authors = [
6
6
  { name = "Siornya", email = "wxh200607@outlook.com" },
@@ -1,11 +1,13 @@
1
- from nonebot import on_command, logger
1
+ import asyncio
2
+
3
+ from nonebot import on_command, logger, get_driver
2
4
  from nonebot.adapters import Message
3
5
  from nonebot.params import CommandArg
4
6
  from nonebot.plugin import PluginMetadata, get_plugin_config
5
7
  from nonebot.adapters.onebot.v11 import MessageEvent, MessageSegment, GroupMessageEvent
6
8
 
7
9
  from .config import Config
8
- from .service import VALID_PLATFORMS, ServiceError, get_full_stats
10
+ from .service import VALID_PLATFORMS, ServiceError, aclose, get_full_stats
9
11
  from .formatter import format_full_stats
10
12
  from .renderer import render_full_stats
11
13
  from .config_mannger import (
@@ -18,7 +20,7 @@ from .config_mannger import (
18
20
  __plugin_meta__ = PluginMetadata(
19
21
  name="彩六数据查询",
20
22
  description="查询指定玩家的数据",
21
- usage="/r6 <id> [平台] /r6key <key> /r6help",
23
+ usage="/r6 <id...> [平台] /r6key <key> /r6help",
22
24
  homepage="https://github.com/Siornya/nonebot-plugin-R6States",
23
25
  type="application",
24
26
  config=Config,
@@ -27,28 +29,32 @@ __plugin_meta__ = PluginMetadata(
27
29
 
28
30
  plugin_config = get_plugin_config(Config)
29
31
 
32
+
33
+ @get_driver().on_shutdown
34
+ async def _shutdown():
35
+ await aclose()
36
+
37
+
30
38
  r6 = on_command("r6", aliases={"R6"}, priority=10, block=True)
31
39
  r6_key = on_command("r6key", aliases={"R6key", "R6DAPI", "r6dapi"}, priority=5, block=True)
32
40
  r6_help = on_command("r6help", aliases={"R6help"}, priority=5, block=True)
33
41
 
34
42
  HELP_TEXT = (
35
43
  "彩六数据查询\n"
36
- "/r6 <id> [平台] 查询玩家数据(平台默认 uplay,可选 psn/xbl)\n"
44
+ "/r6 <id...> [平台] 查询玩家数据(最多5个,空格分隔;平台默认 uplay,可选 psn/xbl)\n"
37
45
  "/r6key <key> 设置本群/本人的 r6data API Key\n"
38
- "/r6help 显示本帮助\n"
39
- "Key 可在 https://r6data.com/ 免费获取;优先用你的个人 Key,没有则用群 Key"
46
+ "/r6help 显示帮助\n"
47
+ "聊群中API KEY优先用个人 Key,没有则用群 Key"
40
48
  )
41
49
 
42
50
 
43
51
  def _scope_id(event: MessageEvent) -> str:
44
- """设置 key 的归属:群聊按群、私聊按人。"""
45
52
  if isinstance(event, GroupMessageEvent):
46
53
  return str(event.group_id)
47
54
  return str(event.user_id)
48
55
 
49
56
 
50
57
  def _lookup_scopes(event: MessageEvent) -> list[str]:
51
- """查询时的 key 候选顺序:个人优先,个人没设则回退到群。"""
52
58
  if isinstance(event, GroupMessageEvent):
53
59
  return [str(event.user_id), str(event.group_id)]
54
60
  return [str(event.user_id)]
@@ -64,7 +70,7 @@ async def _(event: MessageEvent, args: Message = CommandArg()):
64
70
  key = args.extract_plain_text().strip()
65
71
  if not key:
66
72
  await r6_key.finish(
67
- "请在命令后输入 API Key,例如:/r6key abcdef...\n"
73
+ "请在命令后输入 API Key,例如:/r6key <key>\n"
68
74
  "没有的话可在 https://r6data.com/ 免费获取"
69
75
  )
70
76
 
@@ -78,7 +84,7 @@ async def _(event: MessageEvent, args: Message = CommandArg()):
78
84
  async def _(event: MessageEvent, args: Message = CommandArg()):
79
85
  tokens = args.extract_plain_text().split()
80
86
  if not tokens:
81
- await r6.finish("用法:/r6 <id> [平台],详见 /r6help")
87
+ await r6.finish("用法:/r6 <id...> [平台],详见 /r6help")
82
88
 
83
89
  # 末尾 token 若是平台名则作为平台,其余都当作玩家 id。
84
90
  platform = "uplay"
@@ -97,21 +103,29 @@ async def _(event: MessageEvent, args: Message = CommandArg()):
97
103
  if age is not None and age >= KEY_TTL_DAYS:
98
104
  await r6.send(f"⚠️ 当前 API Key 已设置 {age:.0f} 天,可能已过期,如查询失败请 /r6key 重设")
99
105
 
100
- for player_id in tokens:
101
- try:
102
- data = await get_full_stats(
103
- player_id, scopes, platform, season_year=plugin_config.current_season
104
- )
105
- if plugin_config.r6_output_image:
106
- try:
107
- png = render_full_stats(player_id, data)
108
- await r6.send(MessageSegment.image(png))
109
- continue
110
- except Exception as e: # noqa: BLE001 - 渲染失败回退文本
111
- logger.warning(f"图片渲染失败,回退文本: {type(e).__name__}: {e}")
112
- await r6.send(format_full_stats(player_id, data))
113
- except ServiceError as e:
114
- await r6.send(f"❌ {player_id}:{e.message}")
115
- except Exception as e: # noqa: BLE001
116
- logger.error(f"查询 {player_id} 出错: {type(e).__name__}: {e}")
106
+ # 并发取数(单个失败不连累其余),再按原顺序逐个发送
107
+ results = await asyncio.gather(
108
+ *(
109
+ get_full_stats(pid, scopes, platform, season_year=plugin_config.current_season)
110
+ for pid in tokens
111
+ ),
112
+ return_exceptions=True,
113
+ )
114
+
115
+ for player_id, data in zip(tokens, results):
116
+ if isinstance(data, ServiceError):
117
+ logger.warning(f"查询 {player_id} 失败: {data.message}")
118
+ await r6.send(f"❌ {player_id}:{data.message}")
119
+ continue
120
+ if isinstance(data, BaseException):
121
+ logger.error(f"查询 {player_id} 出错: {type(data).__name__}: {data}")
117
122
  await r6.send(f"❌ {player_id}:查询失败")
123
+ continue
124
+ if plugin_config.r6_output_image:
125
+ try:
126
+ png = render_full_stats(player_id, data)
127
+ await r6.send(MessageSegment.image(png))
128
+ continue
129
+ except Exception as e: # noqa: BLE001 - 渲染失败回退文本
130
+ logger.warning(f"图片渲染失败,回退文本: {type(e).__name__}: {e}")
131
+ await r6.send(format_full_stats(player_id, data))
@@ -1,10 +1,3 @@
1
- """轻量本地缓存:单 JSON 文件 + 每条目独立 TTL + 异步锁 + 原子写。
2
-
3
- 设计动机(对比旧的 players.yaml):
4
- - 旧实现按**整文件** mtime 判 TTL,一次写入会重置所有条目的有效期,过期还整包丢弃。
5
- - 这里每条目自带写入时间戳,**各自过期**;TTL 由调用方按 endpoint 指定。
6
- - 写入走 "临时文件 + os.replace" 原子替换,配异步锁,避免并发下半写/相互覆盖。
7
- """
8
1
  from __future__ import annotations
9
2
 
10
3
  import os
@@ -29,7 +22,6 @@ class JSONCache:
29
22
  data = json.load(f)
30
23
  return data if isinstance(data, dict) else {}
31
24
  except (json.JSONDecodeError, OSError):
32
- # 缓存损坏不应影响主流程:当作空缓存重建。
33
25
  return {}
34
26
 
35
27
  def _atomic_write(self, data: dict[str, dict[str, Any]]) -> None:
@@ -46,18 +38,18 @@ class JSONCache:
46
38
  pass
47
39
  raise
48
40
 
49
- async def get(self, key: str, ttl: float) -> Optional[Any]:
50
- """命中且未超过 ttl(秒)则返回缓存值,否则返回 None。"""
41
+ async def get(self, key: str) -> Optional[Any]:
51
42
  async with self._lock:
52
43
  entry = self._load().get(key)
53
- if not entry:
54
- return None
55
- if time.time() - entry.get("ts", 0) > ttl:
44
+ if not entry or entry.get("exp", 0) < time.time():
56
45
  return None
57
46
  return entry.get("value")
58
47
 
59
- async def set(self, key: str, value: Any) -> None:
48
+ async def set(self, key: str, value: Any, ttl: float) -> None:
49
+ """写入并带上过期时间;顺手剔除已过期条目,避免文件无限增长。"""
50
+ now = time.time()
60
51
  async with self._lock:
61
52
  data = self._load()
62
- data[key] = {"ts": time.time(), "value": value}
53
+ data[key] = {"exp": now + ttl, "value": value}
54
+ data = {k: v for k, v in data.items() if v.get("exp", 0) > now}
63
55
  self._atomic_write(data)
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class Config(BaseModel):
7
+ # 当前赛季代码,查询默认按此赛季过滤。填 "all" 则不过滤、查生涯。
8
+ # 在 .env 里用 CURRENT_SEASON 覆盖
9
+ current_season: str = "Y11S2"
10
+
11
+ # 查询结果渲染成图片,失败时自动回退文本
12
+ # 在 .env 用 R6_OUTPUT_IMAGE 覆盖
13
+ r6_output_image: bool = True
@@ -1,9 +1,3 @@
1
- """按群/按人管理 r6data api-key。
2
-
3
- 落盘格式(带设置时间戳,用于过期提醒;key 官方有效期约 1 个月)::
4
-
5
- {"apikeys": {"<id>": {"key": "...", "set_at": 1712345678.0}}}
6
- """
7
1
  from __future__ import annotations
8
2
 
9
3
  import json
@@ -53,10 +47,6 @@ def get_apikey(target_id: str) -> Optional[str]:
53
47
 
54
48
 
55
49
  def resolve_apikey(scopes: list[str]) -> tuple[Optional[str], Optional[str]]:
56
- """按候选顺序找第一个有 key 的归属,返回 (key, 命中的 scope)。
57
-
58
- 群聊传 [群号, 用户号]:群没设 key 时回退到个人 key。
59
- """
60
50
  for scope in scopes:
61
51
  key = get_apikey(scope)
62
52
  if key:
@@ -65,7 +55,6 @@ def resolve_apikey(scopes: list[str]) -> tuple[Optional[str], Optional[str]]:
65
55
 
66
56
 
67
57
  def get_apikey_age_days(target_id: str) -> Optional[float]:
68
- """key 已设置的天数;旧格式/未设置返回 None。"""
69
58
  entry = _entry(target_id)
70
59
  if not entry:
71
60
  return None
@@ -18,9 +18,21 @@ _BOARD_CN = {
18
18
  "event": "活动", "warmup": "热身",
19
19
  }
20
20
 
21
+ #: 1000 起每档 100 分、每大段 5 小级;<1000 未定级,>=4500 冠军。
22
+ _RANK_TIERS = ("紫铜", "青铜", "白银", "黄金", "铂金", "翡翠", "钻石")
23
+
24
+
25
+ def rank_name(mmr: int) -> str:
26
+ if mmr < 1000:
27
+ return "未定级"
28
+ if mmr >= 4500:
29
+ return "冠军"
30
+ idx = (mmr - 1000) // 100
31
+ return f"{_RANK_TIERS[idx // 5]}{5 - idx % 5}"
32
+
21
33
 
22
34
  def _format_boards(profiles: list[dict[str, Any]]) -> list[str]:
23
- """各 board 取最新赛季档案,输出概览行。"""
35
+ """各 board 取最新赛季档案,输出概览行;排位行补段位名。"""
24
36
  out: list[str] = []
25
37
  for fam in profiles:
26
38
  for board in fam.get("board_ids_full_profiles") or []:
@@ -33,9 +45,12 @@ def _format_boards(profiles: list[dict[str, Any]]) -> list[str]:
33
45
  kills, deaths = p.get("kills", 0), p.get("deaths", 0)
34
46
  wr = wins / (wins + losses) * 100 if (wins + losses) else 0
35
47
  kd = kills / deaths if deaths else float(kills)
36
- name = _BOARD_CN.get(board.get("board_id", ""), board.get("board_id", "?"))
48
+ bid = board.get("board_id", "")
49
+ name = _BOARD_CN.get(bid, bid or "?")
50
+ rp = p.get("rank_points", 0)
51
+ rank_str = f"{rank_name(rp)} " if bid == "ranked" else ""
37
52
  out.append(
38
- f"{name} RP{p.get('rank_points', 0)}({p.get('max_rank_points', 0)}) "
53
+ f"{name} {rank_str}RP{rp}({p.get('max_rank_points', 0)}) "
39
54
  f"胜负{wins}/{losses}({wr:.0f}%) KD{kd:.2f} 掉线{p.get('abandon', 0)}"
40
55
  )
41
56
  return out
@@ -1,10 +1,3 @@
1
- """把 fullStats 渲染成图片(PNG 字节)。
2
-
3
- - 双字体:拉丁/数字用 Mona Sans,汉字用 Noto Sans CJK,按字符分段、基线对齐。
4
- - 2x 超采样:按 SCALE 倍分辨率绘制,避免客户端放大时发虚。
5
- - 干员区为对齐表格,数字右对齐;攻/防用不同颜色。
6
- 内容提取复用 formatter 的 _format_boards(档案行);干员直接读结构化字段以便排版。
7
- """
8
1
  from __future__ import annotations
9
2
 
10
3
  from io import BytesIO
@@ -33,12 +26,12 @@ _DEF = (94, 160, 232) # 防
33
26
  _SCALE = 2 # 超采样倍率
34
27
  _W = 760 * _SCALE
35
28
  _PAD = 36 * _SCALE
29
+ _PER_SIDE = 4 # 攻/防各展示几个干员
36
30
 
37
31
  _font_cache: dict[tuple[str, int, Optional[int]], ImageFont.FreeTypeFont] = {}
38
32
 
39
33
 
40
34
  def _font(path: str, size: int, weight: Optional[int] = None) -> ImageFont.FreeTypeFont:
41
- """按字号取字体;weight 给定时尝试调可变字重(失败则忽略)。"""
42
35
  key = (path, size * _SCALE, weight)
43
36
  if key in _font_cache:
44
37
  return _font_cache[key]
@@ -100,7 +93,7 @@ def _ascent(size: int) -> int:
100
93
  return _font(_NOTO, size).getmetrics()[0]
101
94
 
102
95
 
103
- def render_full_stats(player_id: str, data: dict[str, Any], top_n: int = 8) -> bytes:
96
+ def render_full_stats(player_id: str, data: dict[str, Any]) -> bytes:
104
97
  info = data.get("data") or {}
105
98
  handle = (info.get("platformInfo") or {}).get("platformUserHandle") or player_id
106
99
  meta = info.get("metadata") or {}
@@ -149,18 +142,18 @@ def render_full_stats(player_id: str, data: dict[str, Any], top_n: int = 8) -> b
149
142
  section("干员 Top")
150
143
  by_rounds = lambda o: o.get("roundsPlayed", 0) # noqa: E731
151
144
  atks = sorted((o for o in operators if o.get("side") == "Attacker"),
152
- key=by_rounds, reverse=True)[:4]
145
+ key=by_rounds, reverse=True)[:_PER_SIDE]
153
146
  defs = sorted((o for o in operators if o.get("side") == "Defender"),
154
- key=by_rounds, reverse=True)[:4]
147
+ key=by_rounds, reverse=True)[:_PER_SIDE]
155
148
 
156
149
  gap = 48 * _SCALE
157
150
  col_w = (_W - 2 * _PAD - gap) // 2
158
151
  lx, rx = p, p + col_w + gap
159
152
 
160
- # 列头:攻 / 防
153
+ # 列头
161
154
  y += _ascent(19)
162
- _draw(draw, lx, y, "", 19, _ATK, weight=700)
163
- _draw(draw, rx, y, "", 19, _DEF, weight=700)
155
+ _draw(draw, lx, y, "进攻方", 19, _ATK, weight=700)
156
+ _draw(draw, rx, y, "防守方", 19, _DEF, weight=700)
164
157
  y += _font(_NOTO, 19).getmetrics()[1] + 12 * _SCALE
165
158
 
166
159
  def _cell(cx: int, base: int, op: dict[str, Any]) -> None:
@@ -1,19 +1,22 @@
1
1
  """数据源(API 为主)+ 缓存层。
2
2
 
3
- 对外只暴露 ``get_operator_stats``:先查本地缓存,未命中再打 r6data API
3
+ 对外暴露 ``get_full_stats``:先查本地缓存,未命中再打 r6data 的 fullStats
4
4
  成功后回写缓存。错误统一转成 ``ServiceError``,由指令层决定怎么回话。
5
+ 复用一个进程级共享的 httpx 客户端(连接池复用),指令层须在 on_shutdown 调 ``aclose``。
5
6
  """
6
7
  from __future__ import annotations
7
8
 
8
- from typing import Any
9
+ from typing import Any, Optional
10
+
11
+ import httpx
9
12
 
10
13
  from .cache import JSONCache
11
14
  from .storage import PLAYER_CACHE_FILE
12
15
  from .r6data import R6Client, R6APIError
13
16
  from .config_mannger import resolve_apikey
14
17
 
15
- #: 玩家快照属于会变的数据,缓存 30 分钟即可显著降低 API 用量。
16
- FULL_STATS_TTL = 30 * 60
18
+ #: 玩家快照属于会变的数据,缓存 45 分钟即可显著降低 API 用量。
19
+ FULL_STATS_TTL = 45 * 60
17
20
 
18
21
  VALID_PLATFORMS = ("uplay", "psn", "xbl")
19
22
 
@@ -22,6 +25,24 @@ APIKEY_HELP = "可在 https://r6data.com/ 免费获取 Key,再用 /r6key <key>
22
25
 
23
26
  _cache = JSONCache(PLAYER_CACHE_FILE)
24
27
 
28
+ #: 进程级共享 httpx 客户端,注入给每个 R6Client,避免每次查询重建连接池/握手。
29
+ _http: Optional[httpx.AsyncClient] = None
30
+
31
+
32
+ def _client() -> httpx.AsyncClient:
33
+ global _http
34
+ if _http is None or _http.is_closed:
35
+ _http = httpx.AsyncClient(timeout=15.0)
36
+ return _http
37
+
38
+
39
+ async def aclose() -> None:
40
+ """关闭共享 httpx 客户端(在 nonebot on_shutdown 调用)。"""
41
+ global _http
42
+ if _http is not None and not _http.is_closed:
43
+ await _http.aclose()
44
+ _http = None
45
+
25
46
 
26
47
  class ServiceError(Exception):
27
48
  def __init__(self, message: str, *, expired: bool = False) -> None:
@@ -45,15 +66,15 @@ async def get_full_stats(
45
66
  raise ServiceError(f"未设置 API Key。{APIKEY_HELP}")
46
67
 
47
68
  cache_key = f"fullStats:{platform}:{season_year or 'all'}:{modes or 'all'}:{player_id.lower()}"
48
- cached = await _cache.get(cache_key, ttl=FULL_STATS_TTL)
69
+ cached = await _cache.get(cache_key)
49
70
  if cached is not None:
50
71
  return cached
51
72
 
52
73
  try:
53
- async with R6Client(api_key=api_key) as r6:
54
- data = await r6.players.get_full_stats(
55
- player_id, platform, season_year=season_year, modes=modes
56
- )
74
+ r6 = R6Client(api_key=api_key, client=_client())
75
+ data = await r6.players.get_full_stats(
76
+ player_id, platform, season_year=season_year, modes=modes
77
+ )
57
78
  except R6APIError as e:
58
79
  if e.status == 401:
59
80
  raise ServiceError(
@@ -64,5 +85,5 @@ async def get_full_stats(
64
85
  except Exception as e: # noqa: BLE001 - 网络等异常统一兜底
65
86
  raise ServiceError(f"请求失败:{type(e).__name__}") from e
66
87
 
67
- await _cache.set(cache_key, data)
88
+ await _cache.set(cache_key, data, FULL_STATS_TTL)
68
89
  return data
@@ -1,12 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pydantic import BaseModel
4
-
5
-
6
- class Config(BaseModel):
7
- #: 当前赛季代码(如 "Y10S4")。查询默认按此赛季过滤;填 "all" 则不过滤、查生涯。
8
- #: 在 .env 里用 CURRENT_SEASON=Y10S4 覆盖;每赛季更新一次即可。
9
- current_season: str = "Y11S2"
10
-
11
- #: 查询结果是否渲染成图片(失败时自动回退文本)。.env 用 R6_OUTPUT_IMAGE=false 关闭。
12
- r6_output_image: bool = True