Skill: ssunk/dota2-stats-skill (version 1.0.5)

--- name: dota2-stats-skill description: Use this for Dota 2 / DOTA2 / OpenDota questions about players, Steam64/account_id, match_id, heroes, hero matchups, pro matches, teams, leagues, live games, ranks and win rates. Runs a Python standard-library CLI with 27 common OpenDota commands. Default output is Chinese; use --lang en for English. 用于刀塔2/Dota2/DOTA2/OpenDota 查询：玩家战绩、玩家ID、Steam ID、比赛ID、比赛详情、英雄数据、英雄克制、英雄胜率、职业赛事、战队、联赛、实时比赛、段位和胜率。 --- # Dota 2 Stats Query Skill This skill uses a Python script to query Dota 2 data via the [OpenDota API](https://docs.opendota.com/), focused on common player, match, hero, team, league and pro-scene workflows. ## Execution Use the script from the installed skill directory. Do not assume the current working directory is the skill root unless it has been verified. ### Claude Code ```bash python ~/.claude/skills/dota2-stats-skill/scripts/dota2_query.py <command> [args] ``` ### OpenClaw ```bash python ~/.openclaw/plugin-skills/dota2-stats-skill/scripts/dota2_query.py <command> [args] ``` When already inside this skill directory, the shorter form is also valid: ```bash python scripts/dota2_query.py <command> [args] ``` The script uses only the Python standard library, so no pip install is required. It needs network access to `https://api.opendota.com/api` and uses browser-like HTTP headers to reduce 403 risk. ## Command Selection - Player name / 玩家名: use `search <name>` first, then `player <account_id>` and usually `recent <account_id>`. - Steam64 ID / Dota 2 account ID / 玩家ID: pass it directly to player commands. The script auto-converts Steam64 IDs to 32-bit account IDs. - Match ID / 比赛ID / 比赛详情: use `match <match_id>`. - Recent matches / 最近比赛 / 近期战绩: use `recent <account_id>` for a quick view, or `matches <account_id> --limit N` for a filtered list. - Win rate / 胜率 / 排位胜率: use `wl <account_id>` with filters such as `--days N`, `--hero_id N`, or ranked lobby `--lobby_type 7`. - Hero usage / 常用英雄 / 英雄池: use `heroes <account_id>`. - Hero counters / hero matchup / 英雄克制 / 对抗胜率: use `hero_matchups <hero_id>`. - Top players for a hero / 绝活玩家 / 英雄排行榜: use `hero_rankings <hero_id>`. - Pro scene / 职业比赛 / 职业选手 / 战队 / 联赛: use `pro_matches`, `pro_players`, `teams`, `team <team_id>`, or `leagues`. - Live games / 实时比赛 / 正在进行: use `live`. - Game constants / 常量 / 物品 / 模式 / 大厅 / 地区 / 版本: use `constants <resource>`. ## Commands (27 total) ### Player Commands ```bash python scripts/dota2_query.py search <name> # Search for a player (SLOW: 1-2 min) python scripts/dota2_query.py player <account_id> # Player info/rank/winrate python scripts/dota2_query.py wl <id> [--days/--hero_id/--lobby_type] # Win/loss stats python scripts/dota2_query.py recent <id> # Recent ~20 matches python scripts/dota2_query.py matches <id> [--limit/--hero_id/--days] # Full match history python scripts/dota2_query.py heroes <id> [--limit N] # Hero usage stats python scripts/dota2_query.py peers <id> [--limit N] # Frequent teammates python scripts/dota2_query.py totals <id> [filters] # Career totals (kills/assists etc) python scripts/dota2_query.py counts <id> # Categorized stats python scripts/dota2_query.py rankings <id> # Player hero rankings python scripts/dota2_query.py ratings <id> # Rank history python scripts/dota2_query.py refresh <id> # Refresh player data ``` ### Match Commands ```bash python scripts/dota2_query.py match <match_id> # Single match details (10 players) ``` ### Hero Commands ```bash python scripts/dota2_query.py hero_list # All heroes list python scripts/dota2_query.py hero_stats # Global hero stats python scripts/dota2_query.py hero_matchups <hero_id> # Hero matchup winrates python scripts/dota2_query.py hero_rankings <hero_id> # Hero leaderboard (Top players) python scripts/dota2_query.py benchmarks <hero_id> # Hero performance benchmarks ``` ### Global / Pro Commands ```bash python scripts/dota2_query.py pro_players # Pro players list python scripts/dota2_query.py pro_matches [--limit N] # Pro matches python scripts/dota2_query.py public_matches [--min_rank] # Public matches python scripts/dota2_query.py live # Live matches python scripts/dota2_query.py teams [--limit N] # Teams list python scripts/dota2_query.py team <team_id> # Team details+roster+matches python scripts/dota2_query.py leagues # Leagues list python scripts/dota2_query.py constants <resource> # Game constants (heroes/items etc) python scripts/dota2_query.py find_matches --teamA 1,2 --teamB 3,4 # Search by hero lineup ``` ### Common Filters - `--days N` — Last N days - `--hero_id N` — Specific hero ID - `--lobby_type N` — Lobby type (7=Ranked) - `--game_mode N` — Game mode - `--limit N` — Limit results - `--lang zh|en` — Output language (default Chinese) ## Usage Flow ### When user provides a player name: 1. `search <name>` → Get account_id 2. `player <id>` → View basic info 3. `recent <id>` → View recent matches ### Steam ID: *The tool automatically converts 64-bit Steam IDs to 32-bit Account IDs. You can pass either format.* ## Rank Tier | Tier | English | 中文 | |------|---------|------| | 1 | Herald | 先锋 | | 2 | Guardian | 卫士 | | 3 | Crusader | 中军 | | 4 | Archon | 统帅 | | 5 | Legend | 传奇 | | 6 | Ancient | 万古流芳 | | 7 | Divine | 超凡入圣 | | 8 | Immortal | 冠绝一世 | ## Notes 1. Uses only Python standard library - no pip install required 2. Requires network access to the OpenDota API 3. Players need to have "Public Match Data" enabled 4. Built-in Chinese names for 127 heroes 5. Select the output language based on the language used in the user's question, use `--lang en` for English, `--lang zh` for Chinese 6. **`search` command can be slow** because the OpenDota search endpoint may take a while. When invoking this command, remind the user that it may take some time. If the user already knows the player's Dota 2 account ID, suggest using `player <account_id>` directly for faster results

path: README.md
content: <p align="right"><b><a href="./README_EN.md">English</a></b> | 中文</p>

# Dota 2 数据查询 - Agent Skill

基于 [OpenDota API](https://docs.opendota.com/) 的 Dota 2 / 刀塔2 数据查询技能，面向常用的玩家战绩、玩家ID、Steam ID、比赛ID、英雄数据、英雄克制、职业赛事、战队、联赛、实时比赛、段位和胜率查询场景，供 [Claude Code](https://docs.anthropic.com/en/docs/claude-code)/[OpenClaw](https://openclaw.ai) 使用。

## ✨ 功能 (27 个命令)

### 🧑 玩家
| 命令 | 说明 |
|------|------|
| `search <name>` | 搜索玩家 |
| `player <id>` | 段位、MMR、胜率 |
| `wl <id>` | 胜负统计（支持筛选） |
| `recent <id>` | 最近 ~20 场 |
| `matches <id>` | 完整历史 |
| `heroes <id>` | 英雄统计 |
| `peers <id>` | 一起玩的人 |
| `totals <id>` | 生涯累计数据 |
| `counts <id>` | 分类统计 |
| `rankings <id>` | 英雄排名百分位 |
| `ratings <id>` | 段位变化历史 |
| `refresh <id>` | 刷新数据 |

### ⚔️ 比赛
| 命令 | 说明 |
|------|------|
| `match <match_id>` | 10 人完整数据 |

### 🦸 英雄
| 命令 | 说明 |
|------|------|
| `hero_list` | 英雄列表 |
| `hero_stats` | 全局统计 |
| `hero_matchups <hero_id>` | 对抗胜率 |
| `hero_rankings <hero_id>` | Top 玩家 |
| `benchmarks <hero_id>` | 表现基准 |

### 🌍 全局
| 命令 | 说明 |
|------|------|
| `pro_players` | 职业选手 |
| `pro_matches` | 职业比赛 |
| `public_matches` | 公开比赛 |
| `live` | 实时比赛 |
| `teams` | 战队列表 |
| `team <team_id>` | 战队详情+阵容+赛果 |
| `leagues` | 联赛列表 |
| `constants <resource>` | 游戏常量 |
| `find_matches` | 按英雄阵容搜索 |

## 📁 项目结构

```
dota2-stats-skill/
├── SKILL.md                  # Claude Code/OpenClaw 技能描述
├── README.md                 # 本文件
├── README_EN.md              # 英文版
├── scripts/
│   └── dota2_query.py        # Python 查询脚本（27 个常用命令）
└── data/
    ├── translations.json     # 中英双语翻译字典 (仅含界面文本)
    ├── dota_constants.json   # 游戏静态常量映射 (段位、模式、大厅等)
    ├── hero_zh_names.json    # 英雄中文名映射 (127 个)
    └── hero_en_names.json    # (自动生成) 英雄英文名本地缓存
```

## 🚀 使用

### 在 Claude Code 中

将目录放在 `~/.claude/skills/dota2-stats-skill/`，然后直接用自然语言提问：

```
> 查一下 <id> 的 Dota 2 战绩
> 看看影魔对抗谁胜率最低
> 最近有什么职业比赛
```

也可以直接运行脚本：

```bash
python ~/.claude/skills/dota2-stats-skill/scripts/dota2_query.py <command> [args]
```

### 在OpenClaw中

将目录放在 `~/.openclaw/plugin-skills/dota2-stats-skill/`，然后直接用自然语言提问：

```
> 查一下 <id> 的 Dota 2 战绩
> 看看影魔对抗谁胜率最低
> 最近有什么职业比赛
```

也可以直接运行脚本：

```bash
python ~/.openclaw/plugin-skills/dota2-stats-skill/scripts/dota2_query.py <command> [args]
```

### 命令行独立使用

以下短路径适用于当前目录已经在本 skill 根目录时：

```bash
python scripts/dota2_query.py search Miracle
python scripts/dota2_query.py player 105248644
python scripts/dota2_query.py recent 105248644
python scripts/dota2_query.py hero_matchups 11          # 影魔对抗数据
python scripts/dota2_query.py hero_rankings 74          # 祈求者 Top 玩家
python scripts/dota2_query.py pro_matches --limit 10
python scripts/dota2_query.py live
python scripts/dota2_query.py team 8291895              # 查战队
python scripts/dota2_query.py constants items           # 物品数据
```

### 筛选参数

```bash
python scripts/dota2_query.py wl 105248644 --days 30 --lobby_type 7    # 近30天排位
python scripts/dota2_query.py matches 105248644 --hero_id 11 --limit 5 # 影魔最近5场
python scripts/dota2_query.py --lang en player 105248644                # English output
```

## 🔧 技术

- **API**: OpenDota API v31（免费，无需 Key）
- **依赖**: 仅 Python 3.6+ 标准库
- **网络**: 需要能访问 `https://api.opendota.com/api`
- **Headers**: 使用浏览器风格 HTTP Headers，降低 403 风险
- **本地化**: 内置 127 个英雄中文名 + 段位/模式中英双语

## 📄 License

MIT


path: README_EN.md
content: <p align="right">English | <b><a href="./README.md">中文</a></b></p>

# Dota 2 Stats Query - Agent Skill

A Dota 2 / DOTA2 data query skill based on [OpenDota API](https://docs.opendota.com/), focused on common player stats, Steam ID/account ID, match ID, hero data, hero matchups, pro-scene, team, league, live game, rank and win-rate workflows for use with [Claude Code](https://docs.anthropic.com/en/docs/claude-code)/[OpenClaw](https://openclaw.ai).

## ✨ Features (27 commands)

### 🧑 Player
| Command | Description |
|---------|-------------|
| `search <name>` | Search for a player |
| `player <id>` | Rank, MMR, winrate |
| `wl <id>` | Win/loss stats (with filters) |
| `recent <id>` | Recent ~20 matches |
| `matches <id>` | Full history |
| `heroes <id>` | Hero statistics |
| `peers <id>` | Frequent teammates |
| `totals <id>` | Career cumulative data |
| `counts <id>` | Categorized stats |
| `rankings <id>` | Hero ranking percentiles |
| `ratings <id>` | Rank change history |
| `refresh <id>` | Refresh data |

### ⚔️ Match
| Command | Description |
|---------|-------------|
| `match <match_id>` | 10-player complete data |

### 🦸 Hero
| Command | Description |
|---------|-------------|
| `hero_list` | Heroes list |
| `hero_stats` | Global statistics |
| `hero_matchups <hero_id>` | Matchup winrates |
| `hero_rankings <hero_id>` | Top players |
| `benchmarks <hero_id>` | Performance benchmarks |

### 🌍 Global
| Command | Description |
|---------|-------------|
| `pro_players` | Pro players |
| `pro_matches` | Pro matches |
| `public_matches` | Public matches |
| `live` | Live matches |
| `teams` | Teams list |
| `team <team_id>` | Team details+roster+results |
| `leagues` | Leagues list |
| `constants <resource>` | Game constants |
| `find_matches` | Search by hero lineup |

## 📁 Project Structure

```
dota2-stats-skill/
├── SKILL.md                  # Claude Code/OpenClaw skill description
├── README.md                 # Chinese version
├── README_EN.md              # This file (English version)
├── scripts/
│   └── dota2_query.py        # Python query script (27 common commands)
└── data/
    ├── translations.json     # Chinese-English bilingual translation dictionary (UI text)
    ├── dota_constants.json   # Game static constants mapping (ranks, modes, lobbies, etc.)
    ├── hero_zh_names.json    # Hero Chinese name mapping (127 heroes)
    └── hero_en_names.json    # (Auto-generated) Hero English names cache
```

## 🚀 Usage

### In Claude Code

Place the directory in `~/.claude/skills/dota2-stats-skill/`, then ask questions in natural language:

```
> Check <id> Dota 2 stats
> Show Shadow Fiend's worst matchup winrates
> What pro matches are happening recently
```

You can also run the script directly:

```bash
python ~/.claude/skills/dota2-stats-skill/scripts/dota2_query.py <command> [args]
```

### In OpenClaw

Place the directory in `~/.openclaw/plugin-skills/dota2-stats-skill/`, then ask questions in natural language:

```
> Check <id> Dota 2 stats
> Show Shadow Fiend's worst matchup winrates
> What pro matches are happening recently
```

You can also run the script directly:

```bash
python ~/.openclaw/plugin-skills/dota2-stats-skill/scripts/dota2_query.py <command> [args]
```

### Command Line Standalone

The shorter path below assumes the current directory is this skill root:

```bash
python scripts/dota2_query.py search Miracle
python scripts/dota2_query.py player 105248644
python scripts/dota2_query.py recent 105248644
python scripts/dota2_query.py hero_matchups 11          # Shadow Fiend matchups
python scripts/dota2_query.py hero_rankings 74          # Invoker top players
python scripts/dota2_query.py pro_matches --limit 10
python scripts/dota2_query.py live
python scripts/dota2_query.py team 8291895              # Team info
python scripts/dota2_query.py constants items           # Items data
```

### Filter Parameters

```bash
python scripts/dota2_query.py wl 105248644 --days 30 --lobby_type 7    # Last 30 days ranked
python scripts/dota2_query.py matches 105248644 --hero_id 11 --limit 5 # Shadow Fiend last 5
python scripts/dota2_query.py --lang en player 105248644                # English output
```

## 🔧 Technical

- **API**: OpenDota API v31 (free, no key required)
- **Dependencies**: Python 3.6+ standard library only
- **Network**: Requires access to `https://api.opendota.com/api`
- **Headers**: Uses browser-like HTTP headers to reduce 403 risk
- **Localization**: Built-in 127 hero Chinese names + rank/mode bilingual

## 📄 License

MIT


path: scripts/dota2_query.py
content: #!/usr/bin/env python3
"""
Dota 2 Stats Query Tool - Based on OpenDota API (Full Coverage)

Player Commands:
    python scripts/dota2_query.py search <player_name>           Search players by name
    python scripts/dota2_query.py player <account_id>            Player profile & win rate
    python scripts/dota2_query.py wl <account_id> [filters]      Win/Loss stats
    python scripts/dota2_query.py recent <account_id>            Recent ~20 matches
    python scripts/dota2_query.py matches <account_id> [filters] Full match history
    python scripts/dota2_query.py heroes <account_id> [--limit]  Hero usage stats
    python scripts/dota2_query.py peers <account_id> [--limit]   Frequent teammates
    python scripts/dota2_query.py totals <account_id> [filters]  Lifetime totals (kills, assists, etc.)
    python scripts/dota2_query.py counts <account_id>            Counts by category
    python scripts/dota2_query.py rankings <account_id>          Player hero rankings
    python scripts/dota2_query.py ratings <account_id>           Rank tier history
    python scripts/dota2_query.py refresh <account_id>           Refresh player data

Match Commands:
    python scripts/dota2_query.py match <match_id>               Single match detail

Hero Commands:
    python scripts/dota2_query.py hero_list                      All heroes
    python scripts/dota2_query.py hero_stats                     Global hero statistics
    python scripts/dota2_query.py hero_matchups <hero_id>        Hero matchup win rates
    python scripts/dota2_query.py hero_rankings <hero_id>        Top players for a hero
    python scripts/dota2_query.py benchmarks <hero_id>           Hero benchmarks

Global Commands:
    python scripts/dota2_query.py pro_players                    Pro player list
    python scripts/dota2_query.py pro_matches [--limit N]        Recent pro matches
    python scripts/dota2_query.py public_matches [--min_rank N]  Recent public matches
    python scripts/dota2_query.py live                           Live games
    python scripts/dota2_query.py teams [--limit N]              Team list
    python scripts/dota2_query.py team <team_id>                 Team detail & matches
    python scripts/dota2_query.py leagues                        League list
    python scripts/dota2_query.py constants <resource>           Game constants (heroes, items, etc.)
    python scripts/dota2_query.py find_matches [--teamA ids] [--teamB ids]  Find by hero combo

Filters (for wl/matches/totals/counts):
    --days N          Last N days
    --hero_id N       Specific hero
    --lobby_type N    Lobby type (7=Ranked)
    --game_mode N     Game mode
    --limit N         Limit results

Global:
    --lang zh|en      Output language (default: zh)
"""

import sys
import json
import os
import urllib.request
import urllib.parse
import urllib.error
import time
from datetime import datetime, timezone, timedelta

BASE_URL = "https://api.opendota.com/api"

# HTTP headers to avoid 403
REQUEST_HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Accept": "application/json, text/plain, */*",
    "Content-Type": "application/json; charset=utf-8"
}

# ──────────────────────────────────────────────
#  Load translation and hero data from JSON files
# ──────────────────────────────────────────────
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.path.join(BASE_DIR, "data")


def _load_json(filename):
    with open(os.path.join(DATA_DIR, filename), "r", encoding="utf-8") as f:
        return json.load(f)


T = _load_json("translations.json")
C = _load_json("dota_constants.json")
HERO_ZH_NAMES = {int(k): v for k, v in _load_json("hero_zh_names.json").items()}

# Global language, set during startup
LANG = "zh"


def t(key, **kwargs):
    """Look up a translated string by key, with optional format args."""
    s = T[LANG].get(key, T["zh"].get(key, key))
    if kwargs:
        return s.format(**kwargs)
    return s


def get_rank_name(tier):
    """Get localized rank tier name."""
    name_key = f"rank_{tier}"
    if name_key in C[LANG]:
        return C[LANG][name_key]
    s = C[LANG].get("rank_unknown", C["zh"].get("rank_unknown", "Unknown({tier})"))
    return s.format(tier=tier)


def get_mode_name(mode_id):
    """Get localized game mode name. Falls back to zh if not found in current lang."""
    key = f"mode_{mode_id}"
    translated = C[LANG].get(key, C["zh"].get(key, key))
    return translated if translated != key else str(mode_id)


def get_lobby_name(lobby_id):
    """Get localized lobby type name. Falls back to zh if not found in current lang."""
    key = f"lobby_{lobby_id}"
    translated = C[LANG].get(key, C["zh"].get(key, key))
    return translated if translated != key else str(lobby_id)


def get_attr_name(attr):
    """Get localized attribute name. Falls back to zh if not found in current lang."""
    key = f"attr_{attr}"
    translated = C[LANG].get(key, C["zh"].get(key, key))
    return translated if translated != key else attr


def get_atk_name(atk_type):
    """Get localized attack type name."""
    if atk_type == "Melee":
        return C[LANG].get("atk_melee", C["zh"].get("atk_melee", "Melee"))
    return C[LANG].get("atk_ranged", C["zh"].get("atk_ranged", "Ranged"))


# ──────────────────────────────────────────────
#  API helpers
# ──────────────────────────────────────────────

_hero_cache = None


def _send_request(req, timeout=15):
    """Internal function to send request with retry logic for 429."""
    max_retries = 2
    for attempt in range(max_retries + 1):
        try:
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                return json.loads(resp.read().decode("utf-8"))
        except urllib.error.HTTPError as e:
            if e.code == 429 and attempt < max_retries:
                time.sleep(2)
                continue
            print(f"{t('api_fail')} {e.code}", file=sys.stderr)
            if e.code == 404:
                print(f"   {t('not_found')}", file=sys.stderr)
            elif e.code == 429:
                print(f"   {t('rate_limit')}", file=sys.stderr)
            sys.exit(1)
        except urllib.error.URLError as e:
            print(f"{t('network_error')} {e.reason}", file=sys.stderr)
            sys.exit(1)
        except TimeoutError as e:
            print(f"{t('timeout_error')} ({e})", file=sys.stderr)
            sys.exit(1)
        except json.JSONDecodeError as e:
            print(f"{t('json_error')}: {e}", file=sys.stderr)
            sys.exit(1)
        except OSError as e:
            print(f"{t('network_error')} {e}", file=sys.stderr)
            sys.exit(1)


def api_get(endpoint, params=None, timeout=15):
    """Send GET request to OpenDota API."""
    url = f"{BASE_URL}{endpoint}"
    if params:
        filtered = {k: v for k, v in params.items() if v is not None}
        if filtered:
            url += "?" + urllib.parse.urlencode(filtered)

    req = urllib.request.Request(url, headers=REQUEST_HEADERS)
    return _send_request(req, timeout)


def api_post(endpoint):
    """Send POST request to OpenDota API."""
    url = f"{BASE_URL}{endpoint}"
    req = urllib.request.Request(url, method="POST", headers=REQUEST_HEADERS)
    return _send_request(req, 15)


def get_hero_map():
    """Get hero ID → name mapping (cached). Chinese names from HERO_ZH_NAMES, English from API/cache."""
    global _hero_cache
    if _hero_cache is not None:
        return _hero_cache

    cache_file = os.path.join(DATA_DIR, "hero_en_names.json")
    heroes_en = {}
    if os.path.exists(cache_file):
        try:
            with open(cache_file, "r", encoding="utf-8") as f:
                heroes_en = json.load(f)
        except Exception:
            pass

    if not heroes_en:
        heroes = api_get("/heroes")
        heroes_en = {str(h["id"]): h["localized_name"] for h in heroes}
        try:
            with open(cache_file, "w", encoding="utf-8") as f:
                json.dump(heroes_en, f, ensure_ascii=False, indent=2)
        except Exception:
            pass

    if LANG == "zh":
        _hero_cache = {int(k): HERO_ZH_NAMES.get(int(k), v) for k, v in heroes_en.items()}
    else:
        _hero_cache = {int(k): v for k, v in heroes_en.items()}

    return _hero_cache


def format_duration(seconds):
    """Convert seconds to m:ss format."""
    return f"{seconds // 60}:{seconds % 60:02d}"


def format_time(unix_ts):
    """Convert Unix timestamp to readable date string (Local timezone)."""
    if not unix_ts:
        return t("unknown")
    dt = datetime.fromtimestamp(unix_ts, tz=timezone.utc).astimezone()
    return dt.strftime("%Y-%m-%d %H:%M")


def format_time_ago(unix_ts):
    """Convert Unix timestamp to relative time string."""
    if not unix_ts:
        return t("unknown")
    now = time.time()
    diff = now - unix_ts
    if diff < 3600:
        return t("minutes_ago", n=int(diff / 60))
    elif diff < 86400:
        return t("hours_ago", n=int(diff / 3600))
    elif diff < 2592000:
        return t("days_ago", n=int(diff / 86400))
    elif diff < 31536000:
        return t("months_ago", n=int(diff / 2592000))
    else:
        return t("years_ago", n=int(diff / 31536000))


def is_win(player_slot, radiant_win):
    """Determine if the player won based on slot and radiant_win."""
    is_radiant = player_slot < 128
    return (is_radiant and radiant_win) or (not is_radiant and not radiant_win)


def decode_rank_tier(rank_tier):
    """Decode rank_tier integer to human-readable string with stars."""
    if not rank_tier:
        return t("uncalibrated")
    tier = rank_tier // 10
    stars = rank_tier % 10
    name = get_rank_name(tier)
    if tier == 8:
        return name
    return f"{name} {'*' * stars}" if stars else name


# ──────────────────────────────────────────────
#  Command handlers
# ──────────────────────────────────────────────

def cmd_search(args):
    """Search for players by name."""
    if not args:
        print(t("usage_search"))
        sys.exit(1)

    query = " ".join(args)
    print(t("search_waiting"))
    sys.stdout.flush()
    results = api_get("/search", {"q": query}, timeout=120)

    if not results:
        print(t("search_no_result", query=query))
        return

    print(f"\n{t('search_header', query=query, count=len(results))}\n")
    print(f"{t('search_col_id'):<15} {t('search_col_name'):<25} {t('search_col_last'):<20} {t('search_col_sim')}")
    print("-" * 75)
    for p in results[:15]:
        last_match = p.get("last_match_time", t("unknown"))
        if last_match and last_match != t("unknown"):
            last_match = last_match[:10]
        sim = f"{p.get('similarity', 0):.2f}"
        print(f"{p['account_id']:<15} {p.get('personaname', t('unknown')):<25} {last_match:<20} {sim}")


def cmd_player(args):
    """Get player profile info."""
    if not args:
        print(t("usage_player"))
        sys.exit(1)

    account_id = args[0]
    data = api_get(f"/players/{account_id}")
    wl = api_get(f"/players/{account_id}/wl")

    profile = data.get("profile", {})
    rank_tier = data.get("rank_tier")
    leaderboard_rank = data.get("leaderboard_rank")
    mmr = data.get("computed_mmr")

    total = wl["win"] + wl["lose"]
    winrate = (wl["win"] / total * 100) if total > 0 else 0

    print(f"\n{'='*50}")
    print(f"  {t('player_header')}")
    print(f"{'='*50}")
    print(f"  {t('player_name'):<12} {profile.get('personaname', t('unknown'))}")
    print(f"  {t('player_account_id'):<12} {profile.get('account_id', account_id)}")
    print(f"  {t('player_steam_id'):<12} {profile.get('steamid', t('unknown'))}")
    print(f"  {t('player_rank'):<12} {decode_rank_tier(rank_tier)}")
    if leaderboard_rank:
        print(f"  {t('player_leaderboard'):<12} #{leaderboard_rank}")
    if mmr:
        print(f"  {t('player_mmr'):<12} {mmr}")
    print(f"  {t('player_games'):<12} {total}")
    print(f"  {t('player_wl'):<12} {wl['win']}{t('win')} / {wl['lose']}{t('lose')}")
    print(f"  {t('player_winrate'):<12} {winrate:.1f}%")
    print(f"  {t('player_steam_url'):<12} {profile.get('profileurl', '-')}")
    print(f"{'='*50}")


def cmd_wl(args):
    """Get win/loss stats."""
    if not args:
        print(t("usage_wl"))
        sys.exit(1)

    account_id = args[0]
    params = parse_filters(args[1:])
    params["significant"] = "0"
    wl = api_get(f"/players/{account_id}/wl", params)
    total = wl["win"] + wl["lose"]
    winrate = (wl["win"] / total * 100) if total > 0 else 0

    filters = []
    if "date" in params:
        filters.append(t("wl_filter_days", days=params["date"]))
    if "hero_id" in params:
        hero_map = get_hero_map()
        hero_name = hero_map.get(int(params["hero_id"]), f"ID:{params['hero_id']}")
        filters.append(t("wl_filter_hero", hero=hero_name))
    if "lobby_type" in params:
        lt = get_lobby_name(int(params["lobby_type"]))
        filters.append(t("wl_filter_lobby", lobby=lt))

    filter_str = f"（{', '.join(filters)}）" if filters else ""

    print(f"\n  {t('wl_header')}{filter_str}")
    print(f"  {t('wl_wins')} {wl['win']}")
    print(f"  {t('wl_losses')} {wl['lose']}")
    print(f"  {t('wl_total')} {total}")
    print(f"  {t('wl_winrate')} {winrate:.1f}%")


def cmd_recent(args):
    """Get recent matches."""
    if not args:
        print(t("usage_recent"))
        sys.exit(1)

    account_id = args[0]
    hero_map = get_hero_map()
    matches = api_get(f"/players/{account_id}/recentMatches")

    if not matches:
        print(t("no_recent"))
        return

    print(f"\n  {t('recent_header', count=len(matches))}\n")
    print(f"  {t('matches_col_id'):<12} {t('recent_col_hero'):<14} {t('match_mode'):<10} {t('recent_col_result'):<6} {'K/D/A':<10} {t('recent_col_gpm'):<6} {t('recent_col_xpm'):<6} {t('recent_col_duration'):<8} {t('recent_col_date')}")
    print("  " + "-" * 98)

    wins = 0
    for m in matches:
        hero = hero_map.get(m.get("hero_id", 0), t("unknown"))
        won = is_win(m.get("player_slot", 0), m.get("radiant_win"))
        if won:
            wins += 1
        result = t("result_win") if won else t("result_lose")
        mid = str(m.get("match_id", ""))
        mode = get_mode_name(m.get("game_mode", 0))
        kda = f"{m.get('kills', 0)}/{m.get('deaths', 0)}/{m.get('assists', 0)}"
        gpm = str(m.get("gold_per_min", 0))
        xpm = str(m.get("xp_per_min", 0))
        duration = format_duration(m.get("duration", 0))
        date = format_time(m.get("start_time"))

        print(f"  {mid:<12} {hero:<14} {mode:<10} {result:<6} {kda:<10} {gpm:<6} {xpm:<6} {duration:<8} {date}")

    total = len(matches)
    print(f"\n  {t('recent_summary', total=total, wins=wins, losses=total - wins, wr=wins/total*100)}")


def cmd_matches(args):
    """Get match history with pagination and filters."""
    if not args:
        print(t("usage_matches"))
        sys.exit(1)

    account_id = args[0]
    params = parse_filters(args[1:])

    if "limit" not in params:
        params["limit"] = "20"

    params["significant"] = "0"
    hero_map = get_hero_map()
    matches = api_get(f"/players/{account_id}/matches", params)

    if not matches:
        print(t("no_matches"))
        return

    print(f"\n  {t('matches_header', count=len(matches))}\n")
    print(f"  {t('matches_col_id'):<12} {t('recent_col_hero'):<14} {t('match_mode'):<10} {t('recent_col_result'):<6} {'K/D/A':<10} {t('recent_col_duration'):<8} {t('recent_col_date')}")
    print("  " + "-" * 88)

    wins = 0
    for m in matches:
        hero = hero_map.get(m.get("hero_id", 0), t("unknown"))
        won = is_win(m.get("player_slot", 0), m.get("radiant_win"))
        if won:
            wins += 1
        result = t("result_win") if won else t("result_lose")
        mode = get_mode_name(m.get("game_mode", 0))
        kda = f"{m.get('kills', 0)}/{m.get('deaths', 0)}/{m.get('assists', 0)}"
        duration = format_duration(m.get("duration", 0))
        date = format_time(m.get("start_time"))

        print(f"  {str(m.get('match_id', '')):<12} {hero:<14} {mode:<10} {result:<6} {kda:<10} {duration:<8} {date}")

    total = len(matches)
    print(f"\n  {t('matches_summary', total=total, wins=wins, losses=total - wins, wr=wins/total*100)}")


def cmd_heroes(args):
    """Get hero usage stats for a player."""
    if not args:
        print(t("usage_heroes"))
        sys.exit(1)

    account_id = args[0]
    limit = 20

    i = 1
    while i < len(args):
        if args[i] == "--limit" and i + 1 < len(args):
            limit = int(args[i + 1])
            i += 2
        else:
            i += 1

    hero_map = get_hero_map()
    hero_stats = api_get(f"/players/{account_id}/heroes", {"significant": "0"})

    hero_stats = [h for h in hero_stats if h.get("games", 0) > 0]
    hero_stats.sort(key=lambda x: x.get("games", 0), reverse=True)
    hero_stats = hero_stats[:limit]

    if not hero_stats:
        print(t("no_heroes"))
        return

    print(f"\n  {t('heroes_header', limit=min(limit, len(hero_stats)))}\n")
    print(f"  {t('heroes_col_hero'):<18} {t('heroes_col_games'):<8} {t('heroes_col_wins'):<8} {t('heroes_col_winrate'):<10} {t('heroes_col_last')}")
    print("  " + "-" * 65)

    for h in hero_stats:
        hero = hero_map.get(h.get("hero_id", 0), f"ID:{h.get('hero_id', '?')}")
        games = h.get("games", 0)
        win = h.get("win", 0)
        winrate = f"{win / games * 100:.1f}%" if games > 0 else t("n_a")
        last_played = format_time_ago(h.get("last_played"))

        print(f"  {hero:<18} {games:<8} {win:<8} {winrate:<10} {last_played}")


def cmd_match(args):
    """Get single match details."""
    if not args:
        print(t("usage_match"))
        sys.exit(1)

    match_id = args[0]
    hero_map = get_hero_map()
    data = api_get(f"/matches/{match_id}")

    radiant_win = data.get("radiant_win")
    duration = format_duration(data.get("duration", 0))
    start_time = format_time(data.get("start_time"))
    game_mode = get_mode_name(data.get("game_mode", 0))
    r_score = data.get("radiant_score", 0)
    d_score = data.get("dire_score", 0)

    print(f"\n{'='*80}")
    print(f"  {t('match_header')} {match_id}")
    print(f"{'='*80}")
    print(f"  {t('match_mode')}: {game_mode}   |   {t('match_duration')}: {duration}   |   {t('match_date')}: {start_time}")
    print(f"  {t('match_score')}: {t('match_team_radiant')} {r_score} - {d_score} {t('match_team_dire')}   |   {t('match_result_radiant') if radiant_win else t('match_result_dire')}")
    print()

    players = data.get("players", [])
    radiant = [p for p in players if p.get("player_slot", 128) < 128]
    dire = [p for p in players if p.get("player_slot", 0) >= 128]

    def print_team(team_name, team_players):
        print(f"  {'─'*38} {team_name} {'─'*38}")
        print(f"  {t('match_col_player'):<18} {t('heroes_col_hero'):<16} {'K/D/A':<10} {t('recent_col_gpm'):<6} {t('recent_col_xpm'):<6} {t('match_col_damage'):<8} {t('match_col_lh'):<6} {t('match_col_level')}")
        print(f"  {'-'*76}")
        for p in team_players:
            name = p.get("personaname") or t("match_anonymous")
            if len(name) > 15:
                name = name[:14] + "…"
            hero = hero_map.get(p.get("hero_id", 0), t("unknown"))
            kda = f"{p.get('kills', 0)}/{p.get('deaths', 0)}/{p.get('assists', 0)}"
            gpm = str(p.get("gold_per_min", 0))
            xpm = str(p.get("xp_per_min", 0))
            dmg = str(p.get("hero_damage", 0))
            lh = str(p.get("last_hits", 0))
            lvl = str(p.get("level", 0))
            print(f"  {name:<18} {hero:<16} {kda:<10} {gpm:<6} {xpm:<6} {dmg:<8} {lh:<6} {lvl}")
        print()

    print_team(t("match_team_radiant"), radiant)
    print_team(t("match_team_dire"), dire)


def cmd_peers(args):
    """Get frequent teammates."""
    if not args:
        print(t("usage_peers"))
        sys.exit(1)

    account_id = args[0]
    limit = 15

    i = 1
    while i < len(args):
        if args[i] == "--limit" and i + 1 < len(args):
            limit = int(args[i + 1])
            i += 2
        else:
            i += 1

    peers = api_get(f"/players/{account_id}/peers", {"significant": "0"})
    peers = peers[:limit]

    if not peers:
        print(t("no_peers"))
        return

    print(f"\n  {t('peers_header', limit=min(limit, len(peers)))}\n")
    print(f"  {t('peers_col_name'):<20} {t('peers_col_games'):<10} {t('peers_col_wins'):<10} {t('peers_col_winrate'):<10} {t('peers_col_last')}")
    print("  " + "-" * 70)

    for p in peers:
        name = p.get("personaname") or t("match_anonymous")
        if len(name) > 17:
            name = name[:16] + "…"
        games = p.get("with_games", 0)
        win = p.get("with_win", 0)
        winrate = f"{win / games * 100:.1f}%" if games > 0 else t("n_a")
        last = format_time_ago(p.get("last_played"))

        print(f"  {name:<20} {games:<10} {win:<10} {winrate:<10} {last}")


def cmd_hero_list(args):
    """Get full hero list."""
    heroes = api_get("/heroes")
    heroes.sort(key=lambda x: x["id"])

    print(f"\n{t('herolist_header', count=len(heroes))}\n")
    print(f"  {t('herolist_col_id'):<6} {t('herolist_col_name'):<28} {t('herolist_col_attr'):<8} {t('herolist_col_atk'):<8} {t('herolist_col_roles')}")
    print("  " + "-" * 80)

    for h in heroes:
        roles = ", ".join(h.get("roles", []))
        attr = get_attr_name(h.get("primary_attr", ""))
        atk = get_atk_name(h.get("attack_type", ""))
        if LANG == "zh":
            name = HERO_ZH_NAMES.get(h["id"], h["localized_name"])
        else:
            name = h["localized_name"]
        print(f"  {h['id']:<6} {name:<28} {attr:<8} {atk:<8} {roles}")


def cmd_hero_stats(args):
    """Get global hero statistics."""
    heroes = api_get("/heroStats")
    heroes.sort(key=lambda x: x.get("id", 0))

    print(f"\n{t('herostats_header')}\n")
    print(f"  {t('herostats_col_id'):<5} {t('herostats_col_hero'):<24} {t('herostats_col_turbo_picks'):<12} {t('herostats_col_turbo_wins'):<12} {t('herostats_col_turbo_wr')}")
    print("  " + "-" * 65)

    for h in heroes:
        hid = h.get("id", 0)
        tp = h.get("turbo_picks", 0)
        tw = h.get("turbo_wins", 0)
        wr = f"{tw / tp * 100:.1f}%" if tp > 0 else t("n_a")
        if LANG == "zh":
            name = HERO_ZH_NAMES.get(hid, h.get("localized_name", t("unknown")))
        else:
            name = h.get("localized_name", t("unknown"))
        print(f"  {hid:<5} {name:<24} {tp:<12} {tw:<12} {wr}")


def cmd_refresh(args):
    """Refresh player match data."""
    if not args:
        print(t("usage_refresh"))
        sys.exit(1)

    account_id = args[0]
    result = api_post(f"/players/{account_id}/refresh")
    print(f"  {t('refresh_ok', aid=account_id)}")
    print(f"  {t('refresh_resp')} {json.dumps(result, ensure_ascii=False)}")


# ──────────────────────────────────────────────
#  New API endpoint commands
# ──────────────────────────────────────────────

def parse_filters(args):
    """Parse common filter args: --days, --hero_id, --lobby_type, --game_mode, --limit, --offset."""
    params = {}
    i = 0
    while i < len(args):
        if args[i] == "--days" and i + 1 < len(args):
            params["date"] = args[i + 1]; i += 2
        elif args[i] == "--hero_id" and i + 1 < len(args):
            params["hero_id"] = args[i + 1]; i += 2
        elif args[i] == "--lobby_type" and i + 1 < len(args):
            params["lobby_type"] = args[i + 1]; i += 2
        elif args[i] == "--game_mode" and i + 1 < len(args):
            params["game_mode"] = args[i + 1]; i += 2
        elif args[i] == "--limit" and i + 1 < len(args):
            params["limit"] = args[i + 1]; i += 2
        elif args[i] == "--offset" and i + 1 < len(args):
            params["offset"] = args[i + 1]; i += 2
        elif args[i] == "--min_rank" and i + 1 < len(args):
            params["min_rank"] = args[i + 1]; i += 2
        elif args[i] == "--max_rank" and i + 1 < len(args):
            params["max_rank"] = args[i + 1]; i += 2
        elif args[i] == "--teamA" and i + 1 < len(args):
            params["teamA"] = args[i + 1]; i += 2
        elif args[i] == "--teamB" and i + 1 < len(args):
            params["teamB"] = args[i + 1]; i += 2
        else:
            i += 1
    return params


def cmd_totals(args):
    """GET /players/{account_id}/totals - Lifetime totals."""
    if not args:
        print(t("usage_totals")); sys.exit(1)
    account_id = args[0]
    params = parse_filters(args[1:])
    params["significant"] = "0"
    data = api_get(f"/players/{account_id}/totals", params)
    if not data:
        print(t("no_totals")); return
    print(f"\n  {t('totals_header', aid=account_id)}\n")
    print(f"  {t('col_field'):<20} {t('col_total'):<15} {t('col_games'):<10} {t('col_average')}")
    print("  " + "-" * 60)
    for item in data:
        field = item.get("field", "?")
        n = item.get("n", 0)
        s = item.get("sum", 0)
        avg = f"{s/n:.1f}" if n > 0 else t("n_a")
        print(f"  {field:<20} {s:<15.0f} {n:<10} {avg}")


def cmd_counts(args):
    """GET /players/{account_id}/counts - Counts by category."""
    if not args:
        print(t("usage_counts")); sys.exit(1)
    account_id = args[0]
    params = parse_filters(args[1:])
    params["significant"] = "0"
    data = api_get(f"/players/{account_id}/counts", params)
    if not data:
        print(t("no_counts")); return
    print(f"\n  {t('counts_header', aid=account_id)}\n")
    for category, values in data.items():
        print(f"  [{category}]")
        if isinstance(values, dict):
            for k, v in values.items():
                games = v.get("games", 0)
                win = v.get("win", 0)
                wr = f"{win/games*100:.1f}%" if games > 0 else "N/A"
                print(f"    {k}: {games} games, {win} wins ({wr})")
        print()


def cmd_rankings_player(args):
    """GET /players/{account_id}/rankings - Player hero rankings."""
    if not args:
        print(t("usage_rankings")); sys.exit(1)
    account_id = args[0]
    hero_map = get_hero_map()
    data = api_get(f"/players/{account_id}/rankings")
    if not data:
        print(t("no_rankings_data")); return
    data.sort(key=lambda x: x.get("percent_rank", 0), reverse=True)
    print(f"\n  {t('rankings_header', aid=account_id)}\n")
    print(f"  {t('heroes_col_hero'):<20} {t('col_percentile'):<15} {t('col_card')}")
    print("  " + "-" * 50)
    for item in data[:30]:
        hero = hero_map.get(item.get("hero_id", 0), "?")
        pct = f"{item.get('percent_rank', 0)*100:.1f}%"
        card = item.get("card", t("n_a"))
        print(f"  {hero:<20} {pct:<15} {card}")


def cmd_ratings(args):
    """GET /players/{account_id}/ratings - Rank tier history."""
    if not args:
        print(t("usage_ratings")); sys.exit(1)
    account_id = args[0]
    data = api_get(f"/players/{account_id}/ratings")
    if not data:
        print(t("no_ratings")); return
    print(f"\n  {t('ratings_header', aid=account_id)}\n")
    print(f"  {t('col_date'):<20} {t('col_rank_tier'):<15} {t('matches_col_id')}")
    print("  " + "-" * 55)
    for item in data[-20:]:
        date = format_time(item.get("time"))
        rank = decode_rank_tier(item.get("rank_tier"))
        mid = item.get("match_id", t("n_a"))
        print(f"  {date:<20} {rank:<15} {mid}")


def cmd_pro_players(args):
    """GET /proPlayers - List of pro players."""
    data = api_get("/proPlayers")
    if not data:
        print(t("no_pro_players")); return
    limit = 30
    p = parse_filters(args)
    if "limit" in p:
        limit = int(p["limit"])
    print(f"\n  {t('pro_players_header', n=min(limit, len(data)), total=len(data))}\n")
    print(f"  {t('col_name'):<20} {t('col_team'):<18} {t('col_country'):<8} {t('search_col_id')}")
    print("  " + "-" * 60)
    for player in data[:limit]:
        name = player.get("name") or player.get("personaname") or "?"
        team = player.get("team_name") or "-"
        country = player.get("country_code") or "?"
        aid = player.get("account_id", "?")
        print(f"  {name:<20} {team:<18} {country:<8} {aid}")


def cmd_pro_matches(args):
    """GET /proMatches - Recent pro matches."""
    params = parse_filters(args)
    # /proMatches only supports less_than_match_id according to api.json
    api_params = {k: v for k, v in params.items() if k in ["less_than_match_id"]}
    data = api_get("/proMatches", api_params)
    if not data:
        print(t("no_pro_matches")); return
    limit = int(params.get("limit", 20))
    print(f"\n  {t('pro_matches_header')}:\n")
    print(f"  {t('matches_col_id'):<14} {t('match_team_radiant'):<18} {t('match_team_dire'):<18} {t('col_score'):<10} {t('col_duration'):<10} {t('col_league')}")
    print("  " + "-" * 90)
    for m in data[:limit]:
        mid = m.get("match_id", "?")
        rname = (m.get("radiant_name") or "?")[:16]
        dname = (m.get("dire_name") or "?")[:16]
        score = f"{m.get('radiant_score',0)}-{m.get('dire_score',0)}"
        dur = format_duration(m.get("duration", 0))
        league = (m.get("league_name") or "?")[:20]
        print(f"  {mid:<14} {rname:<18} {dname:<18} {score:<10} {dur:<10} {league}")


def cmd_public_matches(args):
    """GET /publicMatches - Recent public matches."""
    params = parse_filters(args)
    # /publicMatches only supports less_than_match_id, min_rank, max_rank
    api_params = {k: v for k, v in params.items() if k in ["less_than_match_id", "min_rank", "max_rank"]}
    data = api_get("/publicMatches", api_params)
    if not data:
        print(t("no_public_matches")); return
    hero_map = get_hero_map()
    print(f"\n  {t('public_matches_header', n=len(data))}\n")
    print(f"  {t('matches_col_id'):<14} {t('col_avg_rank'):<10} {t('col_duration'):<10} {t('col_radiant_heroes')}")
    print("  " + "-" * 80)
    for m in data[:20]:
        mid = m.get("match_id", "?")
        rank = m.get("avg_rank_tier") or "?"
        dur = format_duration(m.get("duration", 0))
        r_heroes = ", ".join(hero_map.get(h, str(h)) for h in (m.get("radiant_team") or [])[:3])
        print(f"  {mid:<14} {str(rank):<10} {dur:<10} {r_heroes}")


def cmd_live(args):
    """GET /live - Currently ongoing live games."""
    data = api_get("/live")
    if not data:
        print(t("no_live")); return
    hero_map = get_hero_map()
    print(f"\n  {t('live_header', n=len(data))}\n")
    for i, game in enumerate(data[:15]):
        players = game.get("players", [])
        avg_mmr = game.get("average_mmr", "?")
        spectators = game.get("spectators", 0)
        gtime = max(0, game.get("game_time", 0))
        r_score = game.get("radiant_score", 0)
        d_score = game.get("dire_score", 0)
        r_team = game.get("team_name_radiant") or t("match_team_radiant")
        d_team = game.get("team_name_dire") or t("match_team_dire")
        print(f"  {t('live_game_line', n=i+1, mmr=avg_mmr, time=format_duration(gtime), r_score=r_score, d_score=d_score, spec=spectators)}")
        radiant = [p for p in players if p.get("team", 0) == 0]
        dire = [p for p in players if p.get("team", 0) == 1]
        r_heroes = ", ".join(hero_map.get(p.get("hero_id", 0), "?") for p in radiant[:5])
        d_heroes = ", ".join(hero_map.get(p.get("hero_id", 0), "?") for p in dire[:5])
        print(f"    {r_team}: {r_heroes}")
        print(f"    {d_team}: {d_heroes}")
        print()


def cmd_teams(args):
    """GET /teams - Team list."""
    params = parse_filters(args)
    # /teams only supports page according to api.json
    api_params = {k: v for k, v in params.items() if k in ["page"]}
    data = api_get("/teams", api_params)
    if not data:
        print(t("no_teams")); return
    limit = int(params.get("limit", 25))
    print(f"\n  {t('teams_header', n=limit)}\n")
    print(f"  {'ID':<10} {t('col_name'):<22} {t('col_tag'):<8} {t('col_rating'):<10} {t('win'):<6} {t('lose'):<6} {t('col_last_match')}")
    print("  " + "-" * 75)
    for team in data[:limit]:
        tid = team.get("team_id", "?")
        name = (team.get("name") or "?")[:20]
        tag = (team.get("tag") or "-")[:6]
        rating = f"{team.get('rating', 0):.0f}"
        wins = team.get("wins", 0)
        losses = team.get("losses", 0)
        last = format_time_ago(team.get("last_match_time"))
        print(f"  {tid:<10} {name:<22} {tag:<8} {rating:<10} {wins:<6} {losses:<6} {last}")


def cmd_team(args):
    """GET /teams/{team_id} + /teams/{team_id}/matches + /teams/{team_id}/players."""
    if not args:
        print(t("usage_team")); sys.exit(1)
    team_id = args[0]
    info = api_get(f"/teams/{team_id}")
    if not info:
        print(t("no_team")); return
    print(f"\n  {t('team_info', name=info.get('name', '?'), tag=info.get('tag', '?'))}")
    print(f"  {t('team_rating_wl', rating=info.get('rating', 0), wins=info.get('wins', 0), losses=info.get('losses', 0))}")
    # Players
    players = api_get(f"/teams/{team_id}/players")
    if players:
        player_list = players if isinstance(players, list) else [players] if isinstance(players, dict) else []
        current = [p for p in player_list if isinstance(p, dict) and p.get("is_current_team_member")]
        if current:
            print(f"\n  {t('team_roster')}:")
            for p in current:
                name = p.get("name") or "?"
                games = p.get("games_played", 0)
                wins = p.get("wins", 0)
                print(f"    {t('team_player_stats', name=name, games=games, wins=wins)}")
    # Recent matches
    matches = api_get(f"/teams/{team_id}/matches")
    if matches:
        print(f"\n  {t('team_recent_matches')}:")
        print(f"  {t('matches_col_id'):<14} {t('col_opponent'):<20} {t('col_result'):<8} {t('col_score'):<10} {t('col_duration')}")
        print("  " + "-" * 65)
        match_list = matches if isinstance(matches, list) else [matches] if isinstance(matches, dict) else []
        for m in match_list[:10]:
            if not isinstance(m, dict):
                continue
            mid = m.get("match_id", "?")
            opp = (m.get("opposing_team_name") or "?")[:18]
            is_rad = m.get("radiant")
            rw = m.get("radiant_win")
            won = (is_rad and rw) or (not is_rad and not rw) if rw is not None else None
            result = "✅ W" if won else ("❌ L" if won is not None else "?")
            score = f"{m.get('radiant_score',0)}-{m.get('dire_score',0)}"
            dur = format_duration(m.get("duration", 0))
            print(f"  {mid:<14} {opp:<20} {result:<8} {score:<10} {dur}")


def cmd_leagues(args):
    """GET /leagues - League list."""
    params = parse_filters(args)
    limit = int(params.get("limit", 30))
    data = api_get("/leagues")
    if not data:
        print(t("no_leagues")); return
    data.sort(key=lambda x: x.get("leagueid", 0), reverse=True)
    print(f"\n  {t('leagues_header', n=min(limit, len(data)), total=len(data))}\n")
    print(f"  {'ID':<10} {t('col_name'):<40} {t('col_tier')}")
    print("  " + "-" * 60)
    for lg in data[:limit]:
        lid = lg.get("leagueid", "?")
        name = (lg.get("name") or "?")[:38]
        tier = lg.get("tier") or "?"
        print(f"  {lid:<10} {name:<40} {tier}")


def cmd_hero_matchups(args):
    """GET /heroes/{hero_id}/matchups - Win rates against other heroes."""
    if not args:
        print(t("usage_hero_matchups")); sys.exit(1)
    hero_id = args[0]
    hero_map = get_hero_map()
    data = api_get(f"/heroes/{hero_id}/matchups")
    if not data:
        print(t("no_hero_matchups")); return
    hero_name = hero_map.get(int(hero_id), f"Hero {hero_id}")
    data.sort(key=lambda x: x.get("games_played", 0), reverse=True)
    print(f"\n  {t('hero_matchups_header', hero=hero_name)}\n")
    print(f"  {t('col_opponent'):<20} {t('col_games'):<10} {t('heroes_col_wins'):<10} {t('heroes_col_winrate')}")
    print("  " + "-" * 55)
    for m in data[:30]:
        opp = hero_map.get(m.get("hero_id", 0), "?")
        games = m.get("games_played", 0)
        wins = m.get("wins", 0)
        wr = f"{wins/games*100:.1f}%" if games > 0 else t("n_a")
        print(f"  {opp:<20} {games:<10} {wins:<10} {wr}")


def cmd_hero_rankings(args):
    """GET /rankings?hero_id={} - Top players for a hero."""
    if not args:
        print(t("usage_hero_rankings")); sys.exit(1)
    hero_id = args[0]
    hero_map = get_hero_map()
    data = api_get("/rankings", {"hero_id": hero_id})
    if not data:
        print(t("no_hero_rankings")); return
    hero_name = hero_map.get(int(hero_id), f"Hero {hero_id}")
    rankings = data.get("rankings", [])
    print(f"\n  {t('hero_rankings_header', hero=hero_name, n=min(20, len(rankings)))}\n")
    print(f"  {'#':<5} {t('col_player'):<25} {'Score':<12} {t('col_rank_tier')}")
    print("  " + "-" * 50)
    for i, r in enumerate(rankings[:20], 1):
        name = r.get("personaname") or "?"
        score = f"{r.get('score', 0):.2f}"
        rank = decode_rank_tier(r.get("rank_tier"))
        print(f"  {i:<5} {name[:23]:<25} {score:<12} {rank}")


def cmd_benchmarks(args):
    """GET /benchmarks?hero_id={} - Hero benchmarks."""
    if not args:
        print(t("usage_benchmarks")); sys.exit(1)
    hero_id = args[0]
    hero_map = get_hero_map()
    data = api_get("/benchmarks", {"hero_id": hero_id})
    if not data:
        print(t("no_benchmarks")); return
    hero_name = hero_map.get(int(hero_id), f"Hero {hero_id}")
    print(f"\n  {t('benchmarks_header', hero=hero_name)}\n")
    result = data.get("result", {})
    for stat_name, values in result.items():
        print(f"  [{stat_name}]")
        if isinstance(values, list):
            for v in values:
                pct = v.get("percentile", 0)
                val = v.get("value", 0)
                print(f"    {pct*100:5.0f}%ile: {val:.1f}")
        print()


def cmd_constants(args):
    """GET /constants/{resource} - Game constants."""
    if not args:
        print(t("usage_constants"))
        print(t("constants_usage_extra"))
        print(t("constants_usage_link"))
        sys.exit(1)
    resource = args[0]
    data = api_get(f"/constants/{resource}")
    if not data:
        print(t("no_constants", resource=resource)); return
    raw = json.dumps(data, indent=2, ensure_ascii=False)
    print(raw[:5000])
    if len(raw) > 5000:
        print(f"\n  ... {t('constants_truncated', n=len(raw))}")


def cmd_find_matches(args):
    """GET /findMatches - Find matches by hero combo."""
    params = parse_filters(args)
    qp = {}
    if "teamA" in params:
        qp["teamA"] = params["teamA"]
    if "teamB" in params:
        qp["teamB"] = params["teamB"]
    if not qp:
        print(t("usage_find_matches"))
        print("  Hero IDs separated by commas. Example: --teamA 11,74 --teamB 1,44")
        sys.exit(1)
    try:
        data = api_get("/findMatches", qp)
    except SystemExit:
        print(f"  {t('find_matches_note')}")
        return
    if not data:
        print(t("no_find_matches")); return
    print(f"\n  {t('find_matches_header', n=len(data))}\n")
    print(f"  {t('matches_col_id'):<14} {t('col_duration'):<10} {t('col_date')}")
    print("  " + "-" * 45)
    for m in data[:15]:
        mid = m.get("match_id", "?")
        dur = format_duration(m.get("duration", 0)) if m.get("duration") else "?"
        start = format_time(m.get("start_time")) if m.get("start_time") else "?"
        print(f"  {mid:<14} {dur:<10} {start}")


# ──────────────────────────────────────────────
#  Main
# ──────────────────────────────────────────────

def main():
    global LANG

    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(1)

    # Extract --lang from anywhere in args
    raw_args = sys.argv[1:]
    filtered_args = []
    i = 0
    while i < len(raw_args):
        a = raw_args[i]
        if a in ("--lang", "-l"):
            i += 1
            if i < len(raw_args):
                LANG = raw_args[i] if raw_args[i] in ("zh", "en") else "zh"
            i += 1
            continue
        if a.startswith("--lang="):
            val = a.split("=", 1)[1]
            LANG = val if val in ("zh", "en") else "zh"
            i += 1
            continue
        if a.startswith("-l="):
            val = a.split("=", 1)[1]
            LANG = val if val in ("zh", "en") else "zh"
            i += 1
            continue
        filtered_args.append(a)
        i += 1

    if not filtered_args:
        print(__doc__)
        sys.exit(1)

    command = filtered_args[0]
    cmd_args = filtered_args[1:]

    # Auto-convert Steam64 ID to 32-bit Account ID
    if cmd_args:
        try:
            val = int(cmd_args[0])
            if val > 76561197960265728:
                cmd_args[0] = str(val - 76561197960265728)
        except ValueError:
            pass

    commands = {
        "search": cmd_search,
        "player": cmd_player,
        "wl": cmd_wl,
        "recent": cmd_recent,
        "matches": cmd_matches,
        "heroes": cmd_heroes,
        "match": cmd_match,
        "peers": cmd_peers,
        "hero_list": cmd_hero_list,
        "hero_stats": cmd_hero_stats,
        "refresh": cmd_refresh,
        # New commands
        "totals": cmd_totals,
        "counts": cmd_counts,
        "rankings": cmd_rankings_player,
        "ratings": cmd_ratings,
        "pro_players": cmd_pro_players,
        "pro_matches": cmd_pro_matches,
        "public_matches": cmd_public_matches,
        "live": cmd_live,
        "teams": cmd_teams,
        "team": cmd_team,
        "leagues": cmd_leagues,
        "hero_matchups": cmd_hero_matchups,
        "hero_rankings": cmd_hero_rankings,
        "benchmarks": cmd_benchmarks,
        "constants": cmd_constants,
        "find_matches": cmd_find_matches,
    }

    if command in ("--help", "-h", "help"):
        print(__doc__)
        sys.exit(0)

    if command not in commands:
        print(f"{t('unknown_cmd')} {command}")
        print(f"{t('available_cmds')} {', '.join(commands.keys())}")
        sys.exit(1)

    commands[command](cmd_args)


if __name__ == "__main__":
    main()


path: data/dota_constants.json
content: {
  "zh": {
    "rank_1": "先锋",
    "rank_2": "卫士",
    "rank_3": "中军",
    "rank_4": "统帅",
    "rank_5": "传奇",
    "rank_6": "万古流芳",
    "rank_7": "超凡入圣",
    "rank_8": "冠绝一世",
    "rank_unknown": "未知({tier})",
    "mode_0": "Unknown",
    "mode_1": "全英雄选择",
    "mode_2": "队长模式",
    "mode_3": "随机征召",
    "mode_4": "单征召",
    "mode_5": "全阵营随机",
    "mode_12": "最少出场",
    "mode_15": "自定义",
    "mode_16": "队长征召",
    "mode_18": "技能征召",
    "mode_22": "排位全选",
    "mode_23": "加速模式",
    "lobby_0": "普通",
    "lobby_1": "练习",
    "lobby_2": "锦标赛",
    "lobby_4": "合作对抗电脑",
    "lobby_5": "排位单排/双排",
    "lobby_6": "排位组排",
    "lobby_7": "排位",
    "lobby_8": "1v1中路单挑",
    "lobby_9": "对战电脑",
    "attr_agi": "敏捷",
    "attr_str": "力量",
    "attr_int": "智力",
    "attr_all": "全能",
    "atk_melee": "近战",
    "atk_ranged": "远程"
  },
  "en": {
    "rank_1": "Herald",
    "rank_2": "Guardian",
    "rank_3": "Crusader",
    "rank_4": "Archon",
    "rank_5": "Legend",
    "rank_6": "Ancient",
    "rank_7": "Divine",
    "rank_8": "Immortal",
    "rank_unknown": "Unknown({tier})",
    "mode_0": "Unknown",
    "mode_1": "All Pick",
    "mode_2": "Captain's Mode",
    "mode_3": "Random Draft",
    "mode_4": "Single Draft",
    "mode_5": "All Random",
    "mode_12": "Least Played",
    "mode_15": "Custom Game",
    "mode_16": "Captain's Draft",
    "mode_18": "Ability Draft",
    "mode_22": "All Pick Ranked",
    "mode_23": "Turbo",
    "lobby_0": "Normal",
    "lobby_1": "Practice",
    "lobby_2": "Tournament",
    "lobby_4": "Co-op vs Bots",
    "lobby_5": "Ranked Solo/Duo",
    "lobby_6": "Ranked Party",
    "lobby_7": "Ranked",
    "lobby_8": "1v1 Mid",
    "lobby_9": "Bot Match",
    "attr_agi": "Agility",
    "attr_str": "Strength",
    "attr_int": "Intelligence",
    "attr_all": "Universal",
    "atk_melee": "Melee",
    "atk_ranged": "Ranged"
  }
}

path: data/hero_en_names.json
content: {
  "1": "Anti-Mage",
  "2": "Axe",
  "3": "Bane",
  "4": "Bloodseeker",
  "5": "Crystal Maiden",
  "6": "Drow Ranger",
  "7": "Earthshaker",
  "8": "Juggernaut",
  "9": "Mirana",
  "10": "Morphling",
  "11": "Shadow Fiend",
  "12": "Phantom Lancer",
  "13": "Puck",
  "14": "Pudge",
  "15": "Razor",
  "16": "Sand King",
  "17": "Storm Spirit",
  "18": "Sven",
  "19": "Tiny",
  "20": "Vengeful Spirit",
  "21": "Windranger",
  "22": "Zeus",
  "23": "Kunkka",
  "25": "Lina",
  "26": "Lion",
  "27": "Shadow Shaman",
  "28": "Slardar",
  "29": "Tidehunter",
  "30": "Witch Doctor",
  "31": "Lich",
  "32": "Riki",
  "33": "Enigma",
  "34": "Tinker",
  "35": "Sniper",
  "36": "Necrophos",
  "37": "Warlock",
  "38": "Beastmaster",
  "39": "Queen of Pain",
  "40": "Venomancer",
  "41": "Faceless Void",
  "42": "Wraith King",
  "43": "Death Prophet",
  "44": "Phantom Assassin",
  "45": "Pugna",
  "46": "Templar Assassin",
  "47": "Viper",
  "48": "Luna",
  "49": "Dragon Knight",
  "50": "Dazzle",
  "51": "Clockwerk",
  "52": "Leshrac",
  "53": "Nature's Prophet",
  "54": "Lifestealer",
  "55": "Dark Seer",
  "56": "Clinkz",
  "57": "Omniknight",
  "58": "Enchantress",
  "59": "Huskar",
  "60": "Night Stalker",
  "61": "Broodmother",
  "62": "Bounty Hunter",
  "63": "Weaver",
  "64": "Jakiro",
  "65": "Batrider",
  "66": "Chen",
  "67": "Spectre",
  "68": "Ancient Apparition",
  "69": "Doom",
  "70": "Ursa",
  "71": "Spirit Breaker",
  "72": "Gyrocopter",
  "73": "Alchemist",
  "74": "Invoker",
  "75": "Silencer",
  "76": "Outworld Destroyer",
  "77": "Lycan",
  "78": "Brewmaster",
  "79": "Shadow Demon",
  "80": "Lone Druid",
  "81": "Chaos Knight",
  "82": "Meepo",
  "83": "Treant Protector",
  "84": "Ogre Magi",
  "85": "Undying",
  "86": "Rubick",
  "87": "Disruptor",
  "88": "Nyx Assassin",
  "89": "Naga Siren",
  "90": "Keeper of the Light",
  "91": "Io",
  "92": "Visage",
  "93": "Slark",
  "94": "Medusa",
  "95": "Troll Warlord",
  "96": "Centaur Warrunner",
  "97": "Magnus",
  "98": "Timbersaw",
  "99": "Bristleback",
  "100": "Tusk",
  "101": "Skywrath Mage",
  "102": "Abaddon",
  "103": "Elder Titan",
  "104": "Legion Commander",
  "105": "Techies",
  "106": "Ember Spirit",
  "107": "Earth Spirit",
  "108": "Underlord",
  "109": "Terrorblade",
  "110": "Phoenix",
  "111": "Oracle",
  "112": "Winter Wyvern",
  "113": "Arc Warden",
  "114": "Monkey King",
  "119": "Dark Willow",
  "120": "Pangolier",
  "121": "Grimstroke",
  "123": "Hoodwink",
  "126": "Void Spirit",
  "128": "Snapfire",
  "129": "Mars",
  "131": "Ringmaster",
  "135": "Dawnbreaker",
  "136": "Marci",
  "137": "Primal Beast",
  "138": "Muerta",
  "145": "Kez",
  "155": "Largo"
}

path: data/hero_zh_names.json
content: {
  "1": "敌法师",
  "2": "斧王",
  "3": "祸乱之源",
  "4": "血魔",
  "5": "水晶室女",
  "6": "卓尔游侠",
  "7": "撼地者",
  "8": "主宰",
  "9": "米拉娜",
  "10": "变体精灵",
  "11": "影魔",
  "12": "幻影长矛手",
  "13": "帕克",
  "14": "屠夫",
  "15": "剃刀",
  "16": "沙王",
  "17": "风暴之灵",
  "18": "斯温",
  "19": "小小",
  "20": "复仇之魂",
  "21": "风行者",
  "22": "宙斯",
  "23": "昆卡",
  "25": "莉娜",
  "26": "莱恩",
  "27": "暗影萨满",
  "28": "斯拉达",
  "29": "潮汐猎人",
  "30": "巫医",
  "31": "巫妖",
  "32": "力丸",
  "33": "谜团",
  "34": "修补匠",
  "35": "狙击手",
  "36": "瘟疫法师",
  "37": "术士",
  "38": "兽王",
  "39": "痛苦女王",
  "40": "剧毒术士",
  "41": "虚空假面",
  "42": "冥魂大帝",
  "43": "死亡先知",
  "44": "幻影刺客",
  "45": "帕格纳",
  "46": "圣堂刺客",
  "47": "冥界亚龙",
  "48": "露娜",
  "49": "龙骑士",
  "50": "戴泽",
  "51": "发条技师",
  "52": "拉席克",
  "53": "先知",
  "54": "噬魂鬼",
  "55": "黑暗贤者",
  "56": "克林克兹",
  "57": "全能骑士",
  "58": "魅惑魔女",
  "59": "哈斯卡",
  "60": "暗夜魔王",
  "61": "育母蜘蛛",
  "62": "赏金猎人",
  "63": "编织者",
  "64": "杰奇洛",
  "65": "蝙蝠骑士",
  "66": "陈",
  "67": "幽鬼",
  "68": "远古冰魄",
  "69": "末日使者",
  "70": "熊战士",
  "71": "裂魂人",
  "72": "矮人直升机",
  "73": "炼金术士",
  "74": "祈求者",
  "75": "沉默术士",
  "76": "殁境毁灭者",
  "77": "狼人",
  "78": "酒仙",
  "79": "暗影恶魔",
  "80": "德鲁伊",
  "81": "混沌骑士",
  "82": "米波",
  "83": "树精卫士",
  "84": "食人魔魔法师",
  "85": "不朽尸王",
  "86": "拉比克",
  "87": "干扰者",
  "88": "司夜刺客",
  "89": "娜迦海妖",
  "90": "光之守卫",
  "91": "艾欧",
  "92": "维萨吉",
  "93": "斯拉克",
  "94": "美杜莎",
  "95": "巨魔战将",
  "96": "半人马战行者",
  "97": "马格纳斯",
  "98": "伐木机",
  "99": "钢背兽",
  "100": "巨牙海民",
  "101": "天怒法师",
  "102": "亚巴顿",
  "103": "上古巨神",
  "104": "军团指挥官",
  "105": "工程师",
  "106": "灰烬之灵",
  "107": "大地之灵",
  "108": "孽主",
  "109": "恐怖利刃",
  "110": "凤凰",
  "111": "神谕者",
  "112": "寒冬飞龙",
  "113": "天穹守望者",
  "114": "齐天大圣",
  "119": "邪影芳灵",
  "120": "石鳞剑士",
  "121": "天涯墨客",
  "123": "森海飞侠",
  "126": "虚无之灵",
  "128": "电炎绝手",
  "129": "玛尔斯",
  "131": "百戏大王",
  "135": "破晓辰星",
  "136": "玛西",
  "137": "原始兽",
  "138": "琼英碧灵",
  "145": "凯",
  "155": "拉尔戈"
}

path: data/translations.json
content: {
  "zh": {
    "api_fail": "API 请求失败: HTTP",
    "not_found": "玩家未找到或数据不可用",
    "rate_limit": "请求频率过高，请稍后再试",
    "network_error": "网络错误:",
    "timeout_error": "请求超时，服务器未在规定时间内响应",
    "json_error": "服务器返回了无效数据",
    "no_recent": "未找到最近比赛记录",
    "no_matches": "未找到比赛记录",
    "no_heroes": "未找到英雄使用记录",
    "no_peers": "未找到一起玩的玩家记录",
    "unknown_cmd": "未知命令:",
    "available_cmds": "可用命令:",
    "search_waiting": "正在搜索玩家数据库，请耐心等待（可能需要 1~2 分钟）...\n提示：如已知玩家 Dota 2 ID，建议直接使用 player <account_id> 查询，速度更快",
    "search_no_result": "未找到匹配「{query}」的玩家",
    "search_header": "搜索「{query}」的结果（共 {count} 个）:",
    "search_col_id": "Account ID",
    "search_col_name": "玩家名",
    "search_col_last": "最后比赛时间",
    "search_col_sim": "相似度",
    "player_header": "玩家资料",
    "player_name": "名称:",
    "player_account_id": "Account ID:",
    "player_steam_id": "Steam ID:",
    "player_rank": "段位:",
    "player_leaderboard": "天梯排名:",
    "player_mmr": "预估 MMR:",
    "player_games": "总场次:",
    "player_wl": "胜/负:",
    "player_winrate": "总胜率:",
    "player_steam_url": "Steam:",
    "wl_header": "胜负统计",
    "wl_wins": "胜场:",
    "wl_losses": "负场:",
    "wl_total": "总场:",
    "wl_winrate": "胜率:",
    "wl_filter_days": "最近{days}天",
    "wl_filter_hero": "英雄:{hero}",
    "wl_filter_lobby": "大厅:{lobby}",
    "recent_header": "最近 {count} 场比赛:",
    "recent_col_hero": "英雄",
    "recent_col_result": "结果",
    "recent_col_gpm": "GPM",
    "recent_col_xpm": "XPM",
    "recent_col_duration": "时长",
    "recent_col_date": "日期",
    "recent_summary": "最近{total}场: {wins}胜 {losses}负 (胜率 {wr:.1f}%)",
    "matches_header": "比赛历史（共 {count} 场）:",
    "matches_col_id": "比赛ID",
    "matches_summary": "共{total}场: {wins}胜 {losses}负 (胜率 {wr:.1f}%)",
    "heroes_header": "英雄使用统计（Top {limit}）:",
    "heroes_col_hero": "英雄",
    "heroes_col_games": "场数",
    "heroes_col_wins": "胜场",
    "heroes_col_winrate": "胜率",
    "heroes_col_last": "最后使用",
    "match_header": "比赛详情 - Match ID:",
    "match_mode": "模式",
    "match_duration": "时长",
    "match_date": "日期",
    "match_score": "比分",
    "match_result_radiant": "天辉胜利",
    "match_result_dire": "夜魇胜利",
    "match_team_radiant": "天辉",
    "match_team_dire": "夜魇",
    "match_col_player": "玩家",
    "match_col_damage": "伤害",
    "match_col_lh": "补刀",
    "match_col_level": "等级",
    "match_anonymous": "匿名",
    "peers_header": "一起玩的玩家（Top {limit}）:",
    "peers_col_name": "玩家名",
    "peers_col_games": "同队场次",
    "peers_col_wins": "同队胜场",
    "peers_col_winrate": "同队胜率",
    "peers_col_last": "最后同场",
    "herolist_header": "Dota 2 英雄列表（共 {count} 个）:",
    "herolist_col_id": "ID",
    "herolist_col_name": "英雄名",
    "herolist_col_attr": "主属性",
    "herolist_col_atk": "攻击类型",
    "herolist_col_roles": "定位",
    "herostats_header": "英雄统计数据:",
    "herostats_col_id": "ID",
    "herostats_col_hero": "英雄",
    "herostats_col_turbo_picks": "加速场",
    "herostats_col_turbo_wins": "加速胜",
    "herostats_col_turbo_wr": "加速胜率",
    "refresh_ok": "已提交刷新请求 (account_id: {aid})",
    "refresh_resp": "响应:",
    "usage_search": "用法: python dota2_query.py search <玩家名>",
    "usage_player": "用法: python dota2_query.py player <account_id>",
    "usage_wl": "用法: python dota2_query.py wl <account_id> [--days N] [--hero_id N] [--lobby_type N]",
    "usage_recent": "用法: python dota2_query.py recent <account_id>",
    "usage_matches": "用法: python dota2_query.py matches <account_id> [--limit N] [--hero_id N] [--days N]",
    "usage_heroes": "用法: python dota2_query.py heroes <account_id> [--limit N]",
    "usage_match": "用法: python dota2_query.py match <match_id>",
    "usage_peers": "用法: python dota2_query.py peers <account_id> [--limit N]",
    "usage_refresh": "用法: python dota2_query.py refresh <account_id>",
    "win": "胜",
    "lose": "负",
    "unknown": "未知",
    "uncalibrated": "未校准",
    "n_a": "N/A",
    "minutes_ago": "{n}分钟前",
    "hours_ago": "{n}小时前",
    "days_ago": "{n}天前",
    "months_ago": "{n}个月前",
    "years_ago": "{n}年前",
    "usage_totals": "用法: python dota2_query.py totals <account_id> [--days N] [--hero_id N]",
    "usage_counts": "用法: python dota2_query.py counts <account_id>",
    "usage_rankings": "用法: python dota2_query.py rankings <account_id>",
    "usage_ratings": "用法: python dota2_query.py ratings <account_id>",
    "usage_team": "用法: python dota2_query.py team <team_id>",
    "usage_hero_matchups": "用法: python dota2_query.py hero_matchups <hero_id>",
    "usage_hero_rankings": "用法: python dota2_query.py hero_rankings <hero_id>",
    "usage_benchmarks": "用法: python dota2_query.py benchmarks <hero_id>",
    "usage_constants": "用法: python dota2_query.py constants <resource>",
    "usage_find_matches": "用法: python dota2_query.py find_matches --teamA 1,2,3 --teamB 4,5,6",
    "no_totals": "未找到生涯数据",
    "no_counts": "未找到分类统计数据",
    "no_rankings_data": "未找到排名数据",
    "no_ratings": "未找到段位历史",
    "no_pro_players": "未找到职业选手",
    "no_pro_matches": "未找到职业比赛",
    "no_public_matches": "未找到公开比赛",
    "no_live": "暂无实时比赛",
    "no_teams": "未找到战队",
    "no_team": "战队未找到",
    "no_leagues": "未找到联赛",
    "no_hero_matchups": "未找到对抗数据",
    "no_hero_rankings": "未找到英雄排行榜",
    "no_benchmarks": "未找到基准数据",
    "no_constants": "资源 '{resource}' 无数据",
    "no_find_matches": "未找到该英雄组合的比赛",
    "totals_header": "生涯数据总计 (account: {aid})",
    "counts_header": "分类统计 (account: {aid})",
    "rankings_header": "英雄排名 (account: {aid})",
    "ratings_header": "段位历史 (account: {aid})",
    "pro_players_header": "职业选手 (显示 {n} / 共 {total})",
    "pro_matches_header": "近期职业比赛",
    "public_matches_header": "近期公开比赛 (显示 {n} 场)",
    "live_header": "实时比赛 ({n} 场)",
    "teams_header": "战队列表 (Top {n})",
    "leagues_header": "联赛列表 (显示 {n} / 共 {total})",
    "hero_matchups_header": "{hero} 的对抗数据",
    "hero_rankings_header": "{hero} 的顶尖玩家 (显示 {n} 位)",
    "benchmarks_header": "{hero} 的表现基准",
    "find_matches_header": "找到 {n} 场比赛",
    "find_matches_note": "提示: findMatches API 暂时不可用或该英雄组合无数据。",
    "constants_usage_extra": "可用资源: heroes, items, abilities, game_mode, lobby_type, region, patch 等",
    "constants_usage_link": "完整列表: https://github.com/odota/dotaconstants/tree/master/build",
    "constants_truncated": "(输出已截断，共 {n} 字符)",
    "team_info": "战队: {name} [{tag}]",
    "team_rating_wl": "积分: {rating:.0f} | 胜: {wins} | 负: {losses}",
    "team_roster": "当前阵容",
    "team_player_stats": "{name}: {games} 场, {wins} 胜",
    "team_recent_matches": "近期比赛",
    "live_game_line": "第{n}场: MMR ~{mmr} | {time} | {r_score}-{d_score} | {spec} 观众",
    "col_field": "字段",
    "col_date": "日期",
    "col_games": "场数",
    "col_duration": "时长",
    "col_total": "总数",
    "col_average": "平均",
    "col_percentile": "百分位",
    "col_card": "卡牌",
    "col_rank_tier": "段位",
    "col_name": "名称",
    "col_team": "战队",
    "col_country": "国家",
    "col_league": "联赛",
    "col_opponent": "对手",
    "col_score": "比分",
    "col_avg_rank": "平均段位",
    "col_radiant_heroes": "天辉英雄",
    "col_tier": "级别",
    "col_tag": "标签",
    "col_rating": "积分",
    "col_last_match": "最近比赛",
    "col_result": "结果",
    "col_player": "玩家",
    "col_rank_col": "排名",
    "result_win": "✅ 胜",
    "result_lose": "❌ 负"
  },
  "en": {
    "api_fail": "API request failed: HTTP",
    "not_found": "Player not found or data unavailable",
    "rate_limit": "Rate limit exceeded, please try again later",
    "network_error": "Network error:",
    "timeout_error": "Request timed out, server did not respond in time",
    "json_error": "Server returned invalid data",
    "no_recent": "No recent matches found",
    "no_matches": "No matches found",
    "no_heroes": "No hero stats found",
    "no_peers": "No peer records found",
    "unknown_cmd": "Unknown command:",
    "available_cmds": "Available commands:",
    "search_waiting": "Searching player database, please wait (may take 1-2 minutes)...\nTip: If you know the player's Dota 2 ID, use player <account_id> for faster results",
    "search_no_result": "No players found matching \"{query}\"",
    "search_header": "Search results for \"{query}\" ({count} found):",
    "search_col_id": "Account ID",
    "search_col_name": "Player Name",
    "search_col_last": "Last Match",
    "search_col_sim": "Similarity",
    "player_header": "Player Profile",
    "player_name": "Name:",
    "player_account_id": "Account ID:",
    "player_steam_id": "Steam ID:",
    "player_rank": "Rank:",
    "player_leaderboard": "Leaderboard:",
    "player_mmr": "Est. MMR:",
    "player_games": "Total Games:",
    "player_wl": "W/L:",
    "player_winrate": "Win Rate:",
    "player_steam_url": "Steam:",
    "wl_header": "Win/Loss Stats",
    "wl_wins": "Wins:",
    "wl_losses": "Losses:",
    "wl_total": "Total:",
    "wl_winrate": "Win Rate:",
    "wl_filter_days": "last {days} days",
    "wl_filter_hero": "hero:{hero}",
    "wl_filter_lobby": "lobby:{lobby}",
    "recent_header": "Recent {count} Matches:",
    "recent_col_hero": "Hero",
    "recent_col_result": "Result",
    "recent_col_gpm": "GPM",
    "recent_col_xpm": "XPM",
    "recent_col_duration": "Duration",
    "recent_col_date": "Date",
    "recent_summary": "Last {total}: {wins}W {losses}L (Win Rate {wr:.1f}%)",
    "matches_header": "Match History ({count} matches):",
    "matches_col_id": "Match ID",
    "matches_summary": "Total {total}: {wins}W {losses}L (Win Rate {wr:.1f}%)",
    "heroes_header": "Hero Stats (Top {limit}):",
    "heroes_col_hero": "Hero",
    "heroes_col_games": "Games",
    "heroes_col_wins": "Wins",
    "heroes_col_winrate": "Win Rate",
    "heroes_col_last": "Last Played",
    "match_header": "Match Detail - Match ID:",
    "match_mode": "Mode",
    "match_duration": "Duration",
    "match_date": "Date",
    "match_score": "Score",
    "match_result_radiant": "Radiant Victory",
    "match_result_dire": "Dire Victory",
    "match_team_radiant": "Radiant",
    "match_team_dire": "Dire",
    "match_col_player": "Player",
    "match_col_damage": "Damage",
    "match_col_lh": "LH",
    "match_col_level": "Lvl",
    "match_anonymous": "Anonymous",
    "peers_header": "Frequent Teammates (Top {limit}):",
    "peers_col_name": "Player",
    "peers_col_games": "Together",
    "peers_col_wins": "Wins",
    "peers_col_winrate": "Win Rate",
    "peers_col_last": "Last Played",
    "herolist_header": "Dota 2 Hero List ({count} heroes):",
    "herolist_col_id": "ID",
    "herolist_col_name": "Name",
    "herolist_col_attr": "Attribute",
    "herolist_col_atk": "Attack",
    "herolist_col_roles": "Roles",
    "herostats_header": "Hero Statistics:",
    "herostats_col_id": "ID",
    "herostats_col_hero": "Hero",
    "herostats_col_turbo_picks": "Turbo Picks",
    "herostats_col_turbo_wins": "Turbo Wins",
    "herostats_col_turbo_wr": "Turbo WR",
    "refresh_ok": "Refresh request submitted (account_id: {aid})",
    "refresh_resp": "Response:",
    "usage_search": "Usage: python dota2_query.py search <player_name>",
    "usage_player": "Usage: python dota2_query.py player <account_id>",
    "usage_wl": "Usage: python dota2_query.py wl <account_id> [--days N] [--hero_id N] [--lobby_type N]",
    "usage_recent": "Usage: python dota2_query.py recent <account_id>",
    "usage_matches": "Usage: python dota2_query.py matches <account_id> [--limit N] [--hero_id N] [--days N]",
    "usage_heroes": "Usage: python dota2_query.py heroes <account_id> [--limit N]",
    "usage_match": "Usage: python dota2_query.py match <match_id>",
    "usage_peers": "Usage: python dota2_query.py peers <account_id> [--limit N]",
    "usage_refresh": "Usage: python dota2_query.py refresh <account_id>",
    "win": "W",
    "lose": "L",
    "unknown": "Unknown",
    "uncalibrated": "Uncalibrated",
    "n_a": "N/A",
    "minutes_ago": "{n} minutes ago",
    "hours_ago": "{n} hours ago",
    "days_ago": "{n} days ago",
    "months_ago": "{n} months ago",
    "years_ago": "{n} years ago",
    "usage_totals": "Usage: python dota2_query.py totals <account_id> [--days N] [--hero_id N]",
    "usage_counts": "Usage: python dota2_query.py counts <account_id>",
    "usage_rankings": "Usage: python dota2_query.py rankings <account_id>",
    "usage_ratings": "Usage: python dota2_query.py ratings <account_id>",
    "usage_team": "Usage: python dota2_query.py team <team_id>",
    "usage_hero_matchups": "Usage: python dota2_query.py hero_matchups <hero_id>",
    "usage_hero_rankings": "Usage: python dota2_query.py hero_rankings <hero_id>",
    "usage_benchmarks": "Usage: python dota2_query.py benchmarks <hero_id>",
    "usage_constants": "Usage: python dota2_query.py constants <resource>",
    "usage_find_matches": "Usage: python dota2_query.py find_matches --teamA 1,2,3 --teamB 4,5,6",
    "no_totals": "No totals data found",
    "no_counts": "No counts data found",
    "no_rankings_data": "No rankings data found",
    "no_ratings": "No ratings history found",
    "no_pro_players": "No pro players found",
    "no_pro_matches": "No pro matches found",
    "no_public_matches": "No public matches found",
    "no_live": "No live games found",
    "no_teams": "No teams found",
    "no_team": "Team not found",
    "no_leagues": "No leagues found",
    "no_hero_matchups": "No matchup data found",
    "no_hero_rankings": "No rankings found",
    "no_benchmarks": "No benchmark data found",
    "no_constants": "No data for resource '{resource}'",
    "no_find_matches": "No matches found for this hero combination",
    "totals_header": "Lifetime Totals (account: {aid})",
    "counts_header": "Counts by Category (account: {aid})",
    "rankings_header": "Hero Rankings (account: {aid})",
    "ratings_header": "Rank History (account: {aid})",
    "pro_players_header": "Pro Players (showing {n} of {total})",
    "pro_matches_header": "Recent Pro Matches",
    "public_matches_header": "Recent Public Matches (showing {n})",
    "live_header": "Live Games ({n} games)",
    "teams_header": "Teams (Top {n})",
    "leagues_header": "Leagues (showing {n} of {total})",
    "hero_matchups_header": "Matchups for {hero}",
    "hero_rankings_header": "Top Players for {hero} (showing {n})",
    "benchmarks_header": "Benchmarks for {hero}",
    "find_matches_header": "Found {n} matches",
    "find_matches_note": "Note: findMatches API may be temporarily unavailable or the hero combo has no data.",
    "constants_usage_extra": "Resources: heroes, items, abilities, game_mode, lobby_type, region, patch, etc.",
    "constants_usage_link": "Full list: https://github.com/odota/dotaconstants/tree/master/build",
    "constants_truncated": "(output truncated, total {n} chars)",
    "team_info": "Team: {name} [{tag}]",
    "team_rating_wl": "Rating: {rating:.0f} | W: {wins} | L: {losses}",
    "team_roster": "Current Roster",
    "team_player_stats": "{name}: {games} games, {wins} wins",
    "team_recent_matches": "Recent Matches",
    "live_game_line": "Game {n}: MMR ~{mmr} | {time} | {r_score}-{d_score} | {spec} spectators",
    "col_field": "Field",
    "col_games": "Games",
    "col_duration": "Duration",
    "col_total": "Total",
    "col_average": "Average",
    "col_percentile": "Percentile",
    "col_card": "Card",
    "col_rank_tier": "Rank Tier",
    "col_name": "Name",
    "col_team": "Team",
    "col_country": "Country",
    "col_league": "League",
    "col_opponent": "Opponent",
    "col_score": "Score",
    "col_avg_rank": "Avg Rank",
    "col_radiant_heroes": "Radiant Heroes",
    "col_tier": "Tier",
    "col_tag": "Tag",
    "col_rating": "Rating",
    "col_last_match": "Last Match",
    "col_result": "Result",
    "col_player": "Player",
    "col_rank_col": "Rank",
    "result_win": "W",
    "result_lose": "L"
  }
}

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)