TFT阵容顾问(AI Agent)TFT 多源格式转换器(二)
上一篇我们搭建了转换器的“大脑”——给定一个标准化的英雄列表,它就能输出羁绊状态、阵容摘要和装备建议。但这还只是个半成品。真正好用的工具,应该能直接吞下乱七八糟的各种输入格式。这一篇,我们就来给大脑装上“眼睛和嘴巴”,并把它包装成一个即开即用的命令行工具。
一、回顾:内核已经就绪,就差输入层
在上一篇文章里,我们实现了两个核心函数:
-
calc_traits():接收英雄列表,返回激活的羁绊详情。 -
build_summary():接收英雄列表和羁绊结果,生成体检报告和问题清单。
只要把喂进去的数据整理成这种格式:
champions = [
{"id": "TFT16_Aatrox", "star": 2, "items": [...], "position": {...}},
...
]
实际应用中,我们获取的数据往往五花八门:
- Riot 官方 API 返回的复杂结构数据(participant.units 字段)
- 截图识别模块生成的 JSON(字段命名不规范)
- 玩家随意输入的文本(如"Aatrox, Riven, Garen"字符串)
- 甚至直接拖入终端的图片文件(二进制数据流)
本文将重点解决数据格式兼容性问题,实现智能识别各类输入格式的功能,同时打造简洁高效的命令行交互界面。
二、多源输入的统一入口设计
转换器最终会暴露出一个主函数 convert(),它接收一个参数 source,内部自动判断 source 的类型并分发给对应的处理器。
def convert(source, assets_dir="./tft_assets", set_num=16):
if isinstance(source, (bytes, bytearray)):
return from_image(source, assets_dir)
if isinstance(source, (dict, list)):
return from_riot_json(source, set_num)
if isinstance(source, str):
s = source.strip()
if s.startswith(("[", "{")):
try:
return from_riot_json(json.loads(s), set_num)
except json.JSONDecodeError:
pass
return from_text(s, set_num)
return {"error": f"不支持的输入类型: {type(source)}"}
这种基于类型判断和特征探测的双重机制,使得调用者无需关注数据来源,直接传入任意数据即可。接下来我们将详细解析各个格式处理器的工作原理。
2.1 Riot 官方 JSON 处理器:from_riot_json()
Riot 的 TFT 对局 API 返回的数据里,每个参与者身上都有一个 units 数组,每个 unit 包含 character_id(英雄API名)、tier(星级)、itemNames(装备名列表)、可选的位置信息等。
def from_riot_json(data: Any, set_num: int = 16) -> Dict:
# 兼容两种结构:直接传 units 列表,或传包含 units 的 dict
units = (data if isinstance(data, list)
else data.get("units", data.get("champions", [])))
champions: List[Dict] = []
for u in units:
raw_id = (u.get("character_id") or u.get("champion_id")
or u.get("id") or u.get("name") or "")
champ_id = normalize_champ_id(raw_id, set_num)
short_id = strip_prefix(champ_id)
# 装备列表处理
items = []
for i in u.get("itemNames", u.get("items", [])):
if i:
items.append(normalize_item_id(i))
champions.append({
"id" : champ_id,
"short_id": short_id,
"name_en" : short_id,
"star" : int(u.get("tier", 1)), # Riot 用 tier 表示星级
"cost" : 0, # 稍后从数据库补全
"items" : items,
"position": u.get("position", {}),
})
# 补全英雄费用(cost)
_load_db()
for c in champions:
db_entry = _champion_db.get(c["id"], {})
c["cost"] = db_entry.get("cost", 0)
traits = calc_traits(champions, set_num)
summary, issues = build_summary(champions, traits)
return {
"team_size": len(champions),
"champions": champions,
"traits": traits,
"summary": summary,
"equipment_issues": issues,
"_source": "riot_json",
}
值得关注的几个设计要点:
-
多字段容错机制:系统支持通过 character_id、champion_id、id 或 name 等多个可能字段进行查询,采用链式取值方式确保接口兼容性。
-
装备字段统一处理:针对不同 API 返回的 itemNames 和 items 等不一致字段名称,实现了统一兼容处理。
-
费用数据补充:由于 Riot 数据源不直接提供英雄费用信息,系统通过本地英雄数据库进行关联查询。为此需要确保 normalize_champ_id 的键名已正确映射。
2.2 自由文本处理器:from_text()
这是最“野生”的输入方式——玩家直接在聊天框敲了几个英雄名,希望快速看看阵容能开出什么羁绊。
def from_text(text: str, set_num: int = 16) -> Dict:
_load_db()
# 提取所有英文单词(支持单引号,比如 Cho'Gath)
words = re.findall(r"[A-Za-z][A-Za-z']+", text)
champions: List[Dict] = []
found_ids: set = set()
for word in words:
for full_id, entry in _champion_db.items():
sid = entry.get("short_id", "")
# 大小写不敏感匹配
if sid.lower() == word.lower() and full_id not in found_ids:
champions.append({
"id" : full_id,
"short_id": sid,
"name_en" : sid,
"star" : 1,
"cost" : entry.get("cost", 0),
"items" : [],
"position": {},
})
found_ids.add(full_id)
break
if not champions:
return {"error": "未能从文本中识别出英雄,请使用英雄英文 ID"}
traits = calc_traits(champions, set_num)
summary, issues = build_summary(champions, traits)
return {
"team_size": len(champions),
"champions": champions,
"traits": traits,
"summary": summary,
"equipment_issues": issues,
"_source": "text",
}
这段代码的核心逻辑是采用无脑匹配策略:
忽略标点、空格和大小写差异,直接用正则表达式提取单词文本。
然后遍历整个 _champion_db 数据库,逐个比对单词与英雄的 short_id(不区分大小写)。
一旦找到匹配项就立即终止内层循环,继续处理下一个单词。
存在的局限性(设计权衡): 当文本中出现如"Ashe"时,若数据库同时存在Ashe和AshenKing两个英雄,只会匹配到第一个符合条件的英雄(取决于字典遍历顺序)。不过在当前50+英雄的正常赛季中,这种歧义情况几乎不会出现,因此这个方案已经"足够好用"。
2.3 截图识别处理器:from_image()
截图识别本身是一个独立的大工程(下一篇博客会专门讲),这里我们假设已经有一个名为 tft_screen_capture 的 Python 模块,它提供了一个 recognize() 函数,输入图片字节流,返回分析结果。
def from_image(image_bytes: bytes, assets_dir: str = "./tft_assets") -> Dict:
try:
from tft_screen_capture import recognize
result = recognize(image_bytes, assets_dir=assets_dir)
if not result.get("error"):
# 补全费用(因为识别模块可能不知道费用)
_load_db()
for c in result.get("champions", []):
db_entry = _champion_db.get(c.get("id", ""), {})
c["cost"] = db_entry.get("cost", 0)
return result
except ImportError:
return {"error": "tft_screen_capture.py 未找到,请确保截图识别模块已安装"}
except Exception as e:
return {"error": str(e)}
处理逻辑很简单:尝试导入模块 → 调用识别 → 补全费用 → 返回结果。如果模块不存在或识别过程出错,统一返回一个带有 error 字段的字典,让上层知道发生了什么。
这里没有把截图识别逻辑写死在转换器里,而是保持模块解耦。转换器只负责“如果用户传了图片,我就委托给专门的模块去处理”。这种松耦合让两个模块可以独立迭代升级。
三、命令行入口:让脚本变成真正的工具
前面写的都是函数库,要想让普通用户(甚至是非程序员玩家)也能用,必须给它加一个命令行界面。在 Python 里,最简单的做法就是利用 if __name__ == "__main__": 块。
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("用法: python tft_converter.py <input.json|text|image.png>")
sys.exit(0)
src_arg = sys.argv[1]
src_path = Path(src_arg)
# 判断是文件路径还是纯文本内容
if src_path.exists():
if src_path.suffix.lower() in (".png", ".jpg", ".jpeg", ".bmp"):
data = src_path.read_bytes()
else:
data = src_path.read_text(encoding="utf-8")
try:
data = json.loads(data)
except json.JSONDecodeError:
pass
else:
data = src_arg
result = convert(data)
print(json.dumps(result, ensure_ascii=False, indent=2))
3.1 逐行解析
参数检查:
if len(sys.argv) < 2:
print("用法: python tft_converter.py <input.json|text|image.png>")
sys.exit(0)
当用户不带任何参数运行脚本时,输出一行友好提示,告诉用户应该怎么用。这是命令行工具的基本素养。
智能判断输入介质:
src_path = Path(src_arg)
if src_path.exists():
# 是一个存在的文件路径
if src_path.suffix.lower() in (".png", ".jpg", ".jpeg", ".bmp"):
data = src_path.read_bytes()
else:
data = src_path.read_text(encoding="utf-8")
try:
data = json.loads(data)
except json.JSONDecodeError:
pass
else:
data = src_arg
-
根据文件是否存在及类型执行相应处理:
-
对于存在的文件:
- 图片文件(通过后缀名判断)以二进制模式读取
- 其他文件以文本模式读取
- 尝试用 json.loads 解析为 Python 对象
- 若解析失败(如纯英雄名列表),保留原始字符串内容
-
对于不存在的输入:
- 直接将其视为原始数据(如 "Aatrox, Garen" 这类自由文本)
调用统一转换器并输出:
result = convert(data)
print(json.dumps(result, ensure_ascii=False, indent=2))
不管数据怎么来、长什么样,最终都交给 convert() 处理,然后把结果以漂亮 JSON 的形式打印到控制台。
3.2 实际使用示例
# 1. 分析 Riot API 抓下来的对局数据
python tft_converter.py match_123456.json
# 2. 快速试算一个脑中构思的阵容
python tft_converter.py "Aatrox, Riven, Mordekaiser, Garen, Darius"
# 3. 分析手机截屏(需搭配 tft_screen_capture 模块)
python tft_converter.py my_team.png
# 4. 将结果保存到文件
python tft_converter.py my_team.json > analysis_result.json
四、兼容旧版函数名:不让重构变成破坏性更新
_calc_traits = calc_traits
_build_summary = build_summary
这是因为另一个还在开发中的项目 tft_screen_capture.py 里,内部调用的函数名还是老版本的 _calc_traits 和 _build_summary(带下划线前缀)。为了不立刻去修改那个仓库的代码,我在这里提供了向后兼容的别名。
五、本篇小结
我们成功将一个"仅支持标准格式"的阵容引擎,升级为能够直接处理图片、JSON甚至自然语句的实用工具。整个改造过程主要完成了三个关键改进:
-
统一接口设计
- 创建
convert()作为统一入口 - 通过
isinstance自动识别输入类型并路由到对应处理器 - 完全屏蔽底层细节,调用者无需关心数据来源
- 创建
-
多格式处理器实现
from_riot_json():解析官方API的复杂结构,采用多级字段回退机制确保兼容性from_text():使用正则表达式提取英文单词,支持大小写不敏感的快速英雄匹配from_image():与截图识别模块解耦,专注委托调用和费用补全
-
命令行集成
- 利用
sys.argv和pathlib自动判断输入类型 - 支持文件路径和纯文本两种输入方式
- 保持脚本一键调用的便利性,同时不影响库功能
- 利用
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)