Skill: juhemcp/juhe-train-tickets (version 1.0.0)

--- name: juhe-train-tickets description: 火车票时刻表查询。查询指定出发站到到达站的火车班次信息，包括车次号、出发/到达时间、历时、票价、余票等。使用场景：用户说"查一下北京到上海的高铁"、"帮我看看明天有哪些火车"、"从成都到重庆的动车几点出发"、"G25次列车几点到"、"下午有没有从广州到深圳的车"等。通过聚合数据（juhe.cn）API实时查询，支持按车次类型、出发时间段筛选，免注册每天10次调用。 homepage: https://www.juhe.cn/docs/api/id/817 metadata: {"openclaw":{"emoji":"🚄","requires":{"bins":["python3"],"env":["JUHE_TRAIN_KEY"]},"primaryEnv":"JUHE_TRAIN_KEY"}} --- # 火车票时刻表查询 > 数据由 **[聚合数据](https://www.juhe.cn)** 提供 — 国内领先的数据服务平台，提供天气、快递、身份证、手机号、IP查询等 200+ 免费/低价 API。 查询指定出发站至到达站的火车时刻表：**车次号、出发/到达时间、历时、坐席票价、余票数量**。 --- ## 前置配置：获取 API Key 1. 前往 [聚合数据官网](https://www.juhe.cn) 免费注册账号 2. 进入 [火车订票查询 API](https://www.juhe.cn/docs/api/id/817) 页面，点击「申请使用」 3. 审核通过后在「我的API」中获取 AppKey 4. 配置 Key（**三选一**）： ```bash # 方式一：环境变量（推荐，一次配置永久生效） export JUHE_TRAIN_KEY=你的AppKey # 方式二：.env 文件（在脚本目录创建） echo "JUHE_TRAIN_KEY=你的AppKey" > scripts/.env # 方式三：每次命令行传入 python scripts/train_lookup.py --key 你的AppKey 北京 上海 2026-03-25 ``` > 免费额度：每天 300 次调用，覆盖日常使用完全够用。 --- ## 使用方法 ### 基本查询 ```bash python scripts/train_lookup.py 北京 上海 2026-03-25 ``` 输出示例： ``` 🚄 北京 → 上海 2026-03-25 共找到 12 个班次 +-------+--------+--------+--------+--------+----------------+ | 车次 | 出发时间 | 到达时间 | 历时 | 可预订 | 备注 | +-------+--------+--------+--------+--------+----------------+ | G1 | 09:00 | 14:28 | 05:28 | ✓ | 复兴号 智能动车组 | | G3 | 10:00 | 15:32 | 05:32 | ✓ | | | ... | ... | ... | ... | ... | | +-------+--------+--------+--------+--------+----------------+ ``` ### 筛选高铁/动车 ```bash # 只看高铁和城际（G） python scripts/train_lookup.py 北京 上海 2026-03-25 --filter G # 只看动车（D） python scripts/train_lookup.py 北京 上海 2026-03-25 --filter D # 多种车型组合（高铁+动车） python scripts/train_lookup.py 北京 上海 2026-03-25 --filter GD ``` 可用车次类型代码： | 代码 | 说明 | |------|------| | G | 高铁/城际 | | D | 动车 | | Z | 直达特快 | | T | 特快 | | K | 快速 | | O | 其他 | | F | 复兴号 | | S | 智能动车组 | ### 按出发时间段筛选 ```bash # 只看上午出发（06:00-12:00） python scripts/train_lookup.py 北京 上海 2026-03-25 --time 上午 # 只看下午出发（12:00-18:00） python scripts/train_lookup.py 北京 上海 2026-03-25 --time 下午 # 只看晚上出发（18:00-24:00） python scripts/train_lookup.py 北京 上海 2026-03-25 --time 晚上 # 只看凌晨出发（00:00-06:00） python scripts/train_lookup.py 北京 上海 2026-03-25 --time 凌晨 ``` ### 查看票价详情 ```bash # 显示各坐席票价 python scripts/train_lookup.py 北京 上海 2026-03-25 --price ``` 输出示例（含票价）： ``` 🚄 G1 北京南 → 上海虹桥 09:00 → 14:28 历时: 05:28 [复兴号] 商务座: ¥1748 一等座: ¥933 二等座: ¥553 ``` ### 直接调用 API（无需脚本） ``` GET https://apis.juhe.cn/fapigw/train/query?key=YOUR_KEY&search_type=1&departure_station=北京&arrival_station=上海&date=2026-03-25 ``` --- ## AI 使用指南 当用户询问火车时刻表相关信息时，按以下步骤操作： 1. **提取查询信息** — 从用户消息中识别出发站、到达站、日期（默认今天或明天） 2. **确认日期** — 接口仅支持 15 天内的日期，超出范围告知用户 3. **调用脚本** — 执行查询，根据用户需求决定是否加 `--filter`、`--time`、`--price` 参数 4. **展示结果** — 用表格汇总班次信息；若用户关心票价，附上票价详情 ### 参数说明 | 参数 | 必填 | 说明 | 示例 | |------|------|------|------| | 出发站 | 是 | 站点名称 | 北京、上海南 | | 到达站 | 是 | 站点名称 | 上海、广州东 | | 日期 | 是 | YYYY-MM-DD 格式，15天内 | 2026-03-25 | | `--filter` | 否 | 车次类型筛选，如 G、D、GD | G | | `--time` | 否 | 出发时段：凌晨/上午/下午/晚上 | 上午 | | `--price` | 否 | 显示票价详情（默认不显示） | — | | `--all` | 否 | 显示所有班次（含不可预订） | — | ### 返回字段说明 | 字段 | 含义 | 示例 | |------|------|------| | `train_no` | 车次号 | G25 | | `departure_station` | 出发站全称 | 北京南 | | `arrival_station` | 到达站全称 | 苏州北 | | `departure_time` | 出发时间 | 18:04 | | `arrival_time` | 到达时间 | 22:32 | | `duration` | 历时 | 04:28 | | `enable_booking` | 12306可预订 | Y / N | | `prices` | 坐席票价列表 | [{seat_name, price, num}] | | `train_flags` | 列车标签 | 复兴号、智能动车组 | ### 错误处理 | 情况 | 处理方式 | |------|----------| | `error_code` 10001/10002 | API Key 无效，引导用户至 [聚合数据](https://www.juhe.cn/docs/api/id/817) 重新申请 | | `error_code` 10012 | 当日免费次数已用尽，建议升级套餐 | | `error_code` 281701 | 查询失败（站点名称可能有误，或该日期无班次） | | 日期超出 15 天 | 告知用户接口限制，建议查询 15 天内的日期 | | 无结果 | 告知用户该路线当日无符合条件的班次，建议调整筛选条件 | | 网络超时 | 重试一次，仍失败则告知网络问题 | --- ## 脚本位置 `scripts/train_lookup.py` — 封装了 API 调用、参数校验、表格/票价输出和错误处理。 --- ## 关于聚合数据 [聚合数据（juhe.cn）](https://www.juhe.cn) 是国内专业的 API 数据服务平台，提供包括： - **交通出行**：火车时刻查询、航班动态、快递追踪 - **生活服务**：天气预报、万年历、节假日查询 - **身份核验**：手机号归属地、身份证实名验证 - **金融数据**：汇率、股票、黄金价格 - **网络工具**：IP查询、DNS解析 注册即可免费使用，适合个人开发者和企业接入。

path: scripts/train_lookup.py
content: #!/usr/bin/env python3
"""
火车票时刻表查询脚本 — 由聚合数据 (juhe.cn) 提供数据支持
查询指定出发站到到达站的火车班次、时刻、票价信息

用法:
    python train_lookup.py <出发站> <到达站> <日期>
    python train_lookup.py 北京 上海 2026-03-25
    python train_lookup.py 北京 上海 2026-03-25 --filter G
    python train_lookup.py 北京 上海 2026-03-25 --time 上午 --price

API Key 配置（任选其一，优先级从高到低）:
    1. 环境变量: export JUHE_TRAIN_KEY=your_api_key
    2. 脚本同目录的 .env 文件: JUHE_TRAIN_KEY=your_api_key
    3. 直接传参: python train_lookup.py --key your_api_key 北京 上海 2026-03-25

免费申请 API Key: https://www.juhe.cn/docs/api/id/817
"""

import sys
import os
import json
import urllib.request
import urllib.parse
from pathlib import Path
from datetime import datetime, timedelta

API_URL = "https://apis.juhe.cn/fapigw/train/query"
REGISTER_URL = "https://www.juhe.cn/docs/api/id/817"

VALID_FILTERS = {"G", "D", "Z", "T", "K", "O", "F", "S"}
VALID_TIME_RANGES = {"凌晨", "上午", "下午", "晚上"}

FILTER_DESC = {
    "G": "高铁/城际",
    "D": "动车",
    "Z": "直达特快",
    "T": "特快",
    "K": "快速",
    "O": "其他",
    "F": "复兴号",
    "S": "智能动车组",
}


def load_api_key(cli_key: str = None) -> str:
    """按优先级加载 API Key"""
    if cli_key:
        return cli_key

    env_key = os.environ.get("JUHE_TRAIN_KEY", "").strip()
    if env_key:
        return env_key

    env_file = Path(__file__).parent / ".env"
    if env_file.exists():
        for line in env_file.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if line.startswith("JUHE_TRAIN_KEY="):
                val = line.split("=", 1)[1].strip().strip('"').strip("'")
                if val:
                    return val

    return ""


def validate_date(date_str: str) -> str | None:
    """验证日期格式并检查是否在15天内，返回错误描述或 None"""
    try:
        target = datetime.strptime(date_str, "%Y-%m-%d").date()
    except ValueError:
        return f"日期格式错误，请使用 YYYY-MM-DD 格式，例如：{datetime.now().strftime('%Y-%m-%d')}"

    today = datetime.now().date()
    if target < today:
        return f"日期 {date_str} 已过期，请查询今天或未来的日期"
    if target > today + timedelta(days=15):
        return f"日期 {date_str} 超出可查询范围（仅支持 15 天内），最远可查询至 {(today + timedelta(days=15)).strftime('%Y-%m-%d')}"
    return None


def query_trains(
    departure: str,
    arrival: str,
    date: str,
    api_key: str,
    filter_types: str = "",
    time_range: str = "",
    enable_booking: str = "1",
) -> dict:
    """调用聚合数据 API 查询列车时刻"""
    params = {
        "key": api_key,
        "search_type": "1",
        "departure_station": departure,
        "arrival_station": arrival,
        "date": date,
        "filter": filter_types,
        "enable_booking": enable_booking,
        "departure_time_range": time_range,
    }
    url = f"{API_URL}?{urllib.parse.urlencode(params)}"

    try:
        with urllib.request.urlopen(url, timeout=15) as resp:
            data = json.loads(resp.read().decode("utf-8"))
    except Exception as e:
        return {"success": False, "error": f"网络请求失败: {e}"}

    error_code = data.get("error_code", -1)
    if error_code == 0:
        return {"success": True, "trains": data.get("result", [])}

    reason = data.get("reason", "查询失败")
    hint = ""
    if error_code in (10001, 10002):
        hint = f"\n   请检查 API Key 是否正确，免费申请：{REGISTER_URL}"
    elif error_code == 10012:
        hint = "\n   今日免费调用次数已用尽，请升级套餐"
    elif error_code == 281701:
        hint = "\n   站点名称可能有误，或该日期/路线暂无可用班次"

    return {
        "success": False,
        "error": f"{reason}{hint}",
        "error_code": error_code,
    }


def format_flags(flags: list) -> str:
    """格式化列车标签"""
    if not flags:
        return ""
    return " ".join(f"[{f}]" for f in flags)


def format_price_detail(prices: list) -> str:
    """格式化票价信息（单行）"""
    if not prices:
        return ""
    parts = []
    for p in prices:
        seat = p.get("seat_name", "")
        price = p.get("price", "")
        num = p.get("num", "")
        if price:
            num_str = f"({num}张)" if num and num != "无" and num != "-" else ""
            parts.append(f"{seat}: ¥{price}{num_str}")
    return "  |  ".join(parts) if parts else ""


def print_train_table(trains: list, show_price: bool = False) -> None:
    """以表格形式输出班次列表"""
    if not trains:
        print("  （无符合条件的班次）")
        return

    if show_price:
        for t in trains:
            flags_str = format_flags(t.get("train_flags", []))
            flags_display = f"  {flags_str}" if flags_str else ""
            print(
                f"  🚄 {t['train_no']:6s}  "
                f"{t['departure_station']} {t['departure_time']} → "
                f"{t['arrival_station']} {t['arrival_time']}  "
                f"历时: {t['duration']}  "
                f"{'✓ 可订' if t.get('enable_booking') == 'Y' else '✗ 不可订'}"
                f"{flags_display}"
            )
            price_str = format_price_detail(t.get("prices", []))
            if price_str:
                print(f"         {price_str}")
        return

    headers = ["车次", "出发站", "出发时间", "到达站", "到达时间", "历时", "可预订", "标签"]

    rows = []
    for t in trains:
        flags_str = " ".join(t.get("train_flags", []))
        rows.append([
            t.get("train_no", ""),
            t.get("departure_station", ""),
            t.get("departure_time", ""),
            t.get("arrival_station", ""),
            t.get("arrival_time", ""),
            t.get("duration", ""),
            "✓" if t.get("enable_booking") == "Y" else "✗",
            flags_str,
        ])

    col_widths = []
    for i, h in enumerate(headers):
        col_widths.append(sum(2 if ord(c) > 127 else 1 for c in h))
    for row in rows:
        for i, cell in enumerate(row):
            width = sum(2 if ord(c) > 127 else 1 for c in str(cell))
            col_widths[i] = max(col_widths[i], width)

    def pad(text, width):
        actual = sum(2 if ord(c) > 127 else 1 for c in str(text))
        return str(text) + " " * max(0, width - actual)

    sep = "+-" + "-+-".join("-" * w for w in col_widths) + "-+"
    header_row = "| " + " | ".join(pad(h, col_widths[i]) for i, h in enumerate(headers)) + " |"

    print(sep)
    print(header_row)
    print(sep)
    for row in rows:
        print("| " + " | ".join(pad(cell, col_widths[i]) for i, cell in enumerate(row)) + " |")
    print(sep)


def parse_args(args: list) -> dict:
    """解析命令行参数"""
    result = {
        "cli_key": None,
        "departure": None,
        "arrival": None,
        "date": None,
        "filter": "",
        "time": "",
        "show_price": False,
        "show_all": False,
        "error": None,
    }

    i = 0
    positional = []

    while i < len(args):
        arg = args[i]
        if arg == "--key":
            if i + 1 < len(args):
                result["cli_key"] = args[i + 1]
                i += 2
            else:
                result["error"] = "错误: --key 后需要提供 API Key 值"
                return result
        elif arg == "--filter":
            if i + 1 < len(args):
                result["filter"] = args[i + 1].upper()
                i += 2
            else:
                result["error"] = "错误: --filter 后需要提供车次类型代码（如：G、D、GD）"
                return result
        elif arg == "--time":
            if i + 1 < len(args):
                result["time"] = args[i + 1]
                i += 2
            else:
                result["error"] = "错误: --time 后需要提供时段（凌晨/上午/下午/晚上）"
                return result
        elif arg == "--price":
            result["show_price"] = True
            i += 1
        elif arg == "--all":
            result["show_all"] = True
            i += 1
        else:
            positional.append(arg)
            i += 1

    if len(positional) < 3:
        result["error"] = (
            "错误: 需要提供出发站、到达站和日期\n"
            f"用法: python train_lookup.py [--key KEY] <出发站> <到达站> <日期> [选项]\n"
            f"示例: python train_lookup.py 北京 上海 {datetime.now().strftime('%Y-%m-%d')}\n"
            f"\n免费申请 API Key: {REGISTER_URL}"
        )
        return result

    result["departure"] = positional[0]
    result["arrival"] = positional[1]
    result["date"] = positional[2]

    if result["filter"]:
        invalid = [c for c in result["filter"] if c not in VALID_FILTERS]
        if invalid:
            result["error"] = (
                f"错误: 无效的车次类型代码 {''.join(invalid)}，"
                f"可用代码: {', '.join(sorted(VALID_FILTERS))}"
            )
            return result

    if result["time"] and result["time"] not in VALID_TIME_RANGES:
        result["error"] = (
            f"错误: 无效的时段 '{result['time']}'，"
            f"可用时段: {', '.join(VALID_TIME_RANGES)}"
        )
        return result

    return result


def main():
    args = sys.argv[1:]

    if not args:
        today = datetime.now().strftime("%Y-%m-%d")
        print("用法: python train_lookup.py [--key KEY] <出发站> <到达站> <日期> [选项]")
        print(f"示例: python train_lookup.py 北京 上海 {today}")
        print(f"      python train_lookup.py 广州 深圳 {today} --filter G --time 上午 --price")
        print(f"\n选项:")
        print(f"  --filter <代码>  车次类型筛选（G=高铁, D=动车, Z=直达特快, T=特快, K=快速）")
        print(f"  --time   <时段>  出发时段筛选（凌晨/上午/下午/晚上）")
        print(f"  --price          显示各坐席票价详情")
        print(f"  --all            显示所有班次（含不可预订）")
        print(f"\n免费申请 API Key: {REGISTER_URL}")
        sys.exit(1)

    parsed = parse_args(args)
    if parsed["error"]:
        print(parsed["error"])
        sys.exit(1)

    [REDACTED_SECRET]"cli_key"])
    if not [REDACTED_SECRET]"❌ 未找到 API Key，请通过以下方式之一配置：")
        print("   1. 环境变量: export JUHE_TRAIN_KEY=your_api_key")
        print("   2. .env 文件: 在脚本目录创建 .env，写入 JUHE_TRAIN_KEY=your_api_key")
        print("   3. 命令行参数: python train_lookup.py --key your_api_key <出发站> <到达站> <日期>")
        print(f"\n免费申请 Key（每天300次免费调用）: {REGISTER_URL}")
        sys.exit(1)

    date_err = validate_date(parsed["date"])
    if date_err:
        print(f"❌ {date_err}")
        sys.exit(1)

    departure = parsed["departure"]
    arrival = parsed["arrival"]
    date = parsed["date"]
    filter_types = parsed["filter"]
    time_range = parsed["time"]
    show_price = parsed["show_price"]
    enable_booking = "2" if parsed["show_all"] else "1"

    filter_desc = ""
    if filter_types:
        descs = [FILTER_DESC.get(c, c) for c in filter_types]
        filter_desc = f"  车型: {'/'.join(descs)}"
    time_desc = f"  时段: {time_range}" if time_range else ""
    booking_desc = "  含不可预订" if parsed["show_all"] else ""

    print(f"\n🚄 查询 {departure} → {arrival}  日期: {date}{filter_desc}{time_desc}{booking_desc}\n")

    result = query_trains(
        departure=departure,
        arrival=arrival,
        date=date,
        [REDACTED_SECRET],
        filter_types=filter_types,
        time_range=time_range,
        enable_booking=enable_booking,
    )

    if not result["success"]:
        print(f"❌ 查询失败: {result['error']}")
        sys.exit(1)

    trains = result["trains"]
    count = len(trains)

    if count == 0:
        print("未找到符合条件的班次。")
        print("建议：")
        print("  · 检查站点名称是否正确（如：北京南、上海虹桥）")
        print("  · 尝试去掉 --filter 或 --time 参数放宽筛选条件")
        print("  · 添加 --all 参数查看不可预订的班次")
        sys.exit(0)

    print(f"共找到 {count} 个班次：\n")
    print_train_table(trains, show_price=show_price)
    print()

    if not show_price and trains:
        print("提示：添加 --price 参数可查看各坐席票价详情")


if __name__ == "__main__":
    main()


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