Skip to content

Commit 79b3572

Browse files
committed
v1.5: SSL auto-retry, 3-tier rune recommendations, ApexLol data scraper
- Add SSL EOF auto-retry (2 retries with 2s delay) for proxy connectivity issues - Enhance prompt to recommend 3 augment builds (best/alternative/different style) - Add ApexLol data scraper module for champion augment data - Add data management module (apexlol_data.py) - Update model to gemini-3.1-flash-lite-preview - Update .gitignore to exclude test files
1 parent 46a62e5 commit 79b3572

9 files changed

Lines changed: 1480 additions & 111 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@ screenshots/
55
gh.exe
66
lcu_client.py
77
.env
8+
apexlol_cache/
9+
test_scraper.py
10+
test_filter.py
11+
test_filter2.py
12+
test_models.py

apexlol_data.py

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# -*- coding: utf-8 -*-
2+
"""ARAM 助手 - ApexLol 数据查询模块
3+
4+
管理本地缓存的 apexlol.info 数据,提供英雄联动信息查询。
5+
"""
6+
7+
import os
8+
import json
9+
import time
10+
import logging
11+
12+
log = logging.getLogger("ARAM")
13+
14+
# ==================== 英雄名称映射 ====================
15+
# 中文标题 -> 英文 ID(爬取时自动构建,这里是常用别名的手动补充)
16+
CHAMPION_ALIASES = {
17+
# 常用简称 -> ID
18+
"卡特": "Katarina", "火男": "Brand", "剑圣": "MasterYi",
19+
"大嘴": "KogMaw", "小法": "Veigar", "女枪": "MissFortune",
20+
"老鼠": "Twitch", "锤石": "Thresh", "狗头": "Nasus",
21+
"牛头": "Alistar", "猴子": "MonkeyKing", "蛮王": "Tryndamere",
22+
"石头人": "Malphite", "机器人": "Blitzcrank", "稻草人": "Fiddlesticks",
23+
"酒桶": "Gragas", "皇子": "JarvanIV", "螳螂": "Khazix",
24+
"人马": "Hecarim", "薇恩": "Vayne", "亚索": "Yasuo",
25+
"永恩": "Yone", "艾克": "Ekko", "EZ": "Ezreal",
26+
"ez": "Ezreal", "VN": "Vayne", "vn": "Vayne",
27+
"ADC": None, # 通用标签不映射
28+
}
29+
30+
# 全局缓存
31+
_cache = None
32+
_name_to_id = None
33+
34+
35+
def _build_name_map(data: dict) -> dict:
36+
"""从缓存数据构建 名称 -> ID 的映射表。"""
37+
name_map = {}
38+
39+
# 从 champion_list 构建
40+
for champ in data.get("champion_list", []):
41+
champ_id = champ["id"]
42+
cn_title = champ.get("cn_title", "")
43+
44+
# 中文标题 -> ID(如 "不祥之刃" -> "Katarina")
45+
if cn_title:
46+
name_map[cn_title] = champ_id
47+
48+
# 英文 ID 本身(大小写不敏感)
49+
name_map[champ_id.lower()] = champ_id
50+
51+
# 从 champions 数据构建
52+
for champ_id, info in data.get("champions", {}).items():
53+
cn_title = info.get("cn_title", "")
54+
if cn_title:
55+
name_map[cn_title] = champ_id
56+
57+
cn_name = info.get("cn_name", "")
58+
if cn_name:
59+
name_map[cn_name] = champ_id
60+
61+
# 加入手动别名
62+
for alias, cid in CHAMPION_ALIASES.items():
63+
if cid:
64+
name_map[alias] = cid
65+
66+
return name_map
67+
68+
69+
def load_cache(cache_dir: str) -> dict:
70+
"""加载本地缓存数据。"""
71+
global _cache, _name_to_id
72+
73+
cache_file = os.path.join(cache_dir, "apexlol_data.json")
74+
if not os.path.exists(cache_file):
75+
log.warning("[ApexLol] 缓存文件不存在")
76+
return {}
77+
78+
try:
79+
with open(cache_file, "r", encoding="utf-8") as f:
80+
_cache = json.load(f)
81+
_name_to_id = _build_name_map(_cache)
82+
log.info(f"[ApexLol] ✅ 已加载缓存 ({len(_cache.get('champions', {}))} 英雄)")
83+
return _cache
84+
except Exception as e:
85+
log.error(f"[ApexLol] 加载缓存失败: {e}")
86+
return {}
87+
88+
89+
def is_cache_valid(cache_dir: str, ttl_days: int = 7) -> bool:
90+
"""检查缓存是否有效(文件存在且未过期)。"""
91+
cache_file = os.path.join(cache_dir, "apexlol_data.json")
92+
if not os.path.exists(cache_file):
93+
return False
94+
95+
try:
96+
mtime = os.path.getmtime(cache_file)
97+
age_days = (time.time() - mtime) / 86400
98+
return age_days < ttl_days
99+
except Exception:
100+
return False
101+
102+
103+
def resolve_champion_id(name: str) -> str | None:
104+
"""将英雄名(中文/英文/别名)解析为英文 ID。"""
105+
global _name_to_id
106+
if _name_to_id is None:
107+
return None
108+
109+
# 精确匹配
110+
if name in _name_to_id:
111+
return _name_to_id[name]
112+
113+
# 大小写不敏感匹配
114+
lower = name.lower()
115+
if lower in _name_to_id:
116+
return _name_to_id[lower]
117+
118+
# 模糊匹配:包含关系
119+
for key, cid in _name_to_id.items():
120+
if name in key or key in name:
121+
return cid
122+
123+
return None
124+
125+
126+
def lookup_champion(name: str) -> str:
127+
"""查询单个英雄的海克斯联动分析文本。
128+
129+
Args:
130+
name: 英雄名(中文标题、英文 ID 或别名)
131+
132+
Returns:
133+
格式化的联动分析文本,未找到返回空字符串
134+
"""
135+
global _cache
136+
if not _cache:
137+
return ""
138+
139+
champ_id = resolve_champion_id(name)
140+
if not champ_id:
141+
return ""
142+
143+
champ_data = _cache.get("champions", {}).get(champ_id)
144+
if not champ_data or not champ_data.get("synergies"):
145+
return ""
146+
147+
cn_title = champ_data.get("cn_title", champ_id)
148+
lines = [f"【{cn_title}({champ_id})的海克斯联动数据 - 来源: apexlol.info】"]
149+
150+
for s in champ_data["synergies"]:
151+
hex_names = " + ".join(s.get("hex_names", []))
152+
rating = s.get("rating", "")
153+
tiers = " / ".join(s.get("hex_tiers", []))
154+
tag = s.get("tag", "")
155+
156+
header_parts = []
157+
if rating:
158+
header_parts.append(f"[{rating}级]")
159+
header_parts.append(hex_names)
160+
if tiers:
161+
header_parts.append(f"({tiers})")
162+
if tag:
163+
header_parts.append(f"- {tag}")
164+
165+
lines.append(f"\n{' '.join(header_parts)}")
166+
lines.append(f" {s.get('analysis', '')}")
167+
168+
return "\n".join(lines)
169+
170+
171+
def lookup_champions(names: list[str], highlight_mine: str = None) -> str:
172+
"""批量查询多个英雄的联动数据,拼接为 Gemini 可用的参考文本。
173+
174+
Args:
175+
names: 英雄名列表
176+
highlight_mine: 我的英雄名(会放在最前面并额外标注)
177+
178+
Returns:
179+
拼接后的参考文本
180+
"""
181+
sections = []
182+
183+
# 先处理"我的英雄"
184+
if highlight_mine:
185+
my_data = lookup_champion(highlight_mine)
186+
if my_data:
187+
sections.append(f"=== ⭐ 我的英雄 ===\n{my_data}")
188+
189+
# 处理其余英雄
190+
for name in names:
191+
if highlight_mine and name == highlight_mine:
192+
continue
193+
data = lookup_champion(name)
194+
if data:
195+
sections.append(data)
196+
197+
if not sections:
198+
return ""
199+
200+
header = (
201+
"📚 以下是来自 apexlol.info 的海克斯联动分析数据(专业玩家社区贡献的机制级分析)。\n"
202+
"请重点参考这些数据来推荐海克斯符文,特别注意隐藏的联动机制(如某技能能触发特定符文效果等)。\n"
203+
"注意:这些数据是参考建议,请结合当前阵容做出最佳判断。\n"
204+
"=" * 60
205+
)
206+
207+
return header + "\n\n" + "\n\n".join(sections)
208+
209+
210+
def get_cache_info(cache_dir: str) -> dict:
211+
"""获取缓存状态信息。"""
212+
cache_file = os.path.join(cache_dir, "apexlol_data.json")
213+
if not os.path.exists(cache_file):
214+
return {"exists": False}
215+
216+
try:
217+
mtime = os.path.getmtime(cache_file)
218+
age_hours = (time.time() - mtime) / 3600
219+
size_mb = os.path.getsize(cache_file) / (1024 * 1024)
220+
221+
with open(cache_file, "r", encoding="utf-8") as f:
222+
data = json.load(f)
223+
224+
return {
225+
"exists": True,
226+
"age_hours": round(age_hours, 1),
227+
"size_mb": round(size_mb, 2),
228+
"champion_count": len(data.get("champions", {})),
229+
"scraped_at": data.get("meta", {}).get("scraped_at", "unknown"),
230+
}
231+
except Exception:
232+
return {"exists": False}

0 commit comments

Comments
 (0)