Skill: juhemcp/juhe-mobile-locate (version 1.0.0)

--- name: juhe-mobile-locate description: 手机号码归属地查询。输入手机号码或手机号前7位，查询归属地省份/城市、区号、邮编、运营商（移动/联通/电信）、卡类型等信息。使用场景：用户说"查一下13812345678是哪里的号码"、"这个手机号是哪个运营商的"、"帮我查下这个号的归属地"、"138开头的号码是移动还是联通"等。通过聚合数据（juhe.cn）API实时查询，免注册每天10次调用。 homepage: https://www.juhe.cn/docs/api/id/11 metadata: {"openclaw":{"emoji":"📱","requires":{"bins":["python3"],"env":["JUHE_MOBILE_KEY"]},"primaryEnv":"JUHE_MOBILE_KEY"}} --- # 手机号码归属地查询 > 数据由 **[聚合数据](https://www.juhe.cn)** 提供 — 国内领先的数据服务平台，提供天气、快递、身份证、手机号、IP查询等 200+ 免费/低价 API。 输入手机号码，查询：**归属地省份/城市、区号、邮编、运营商（移动/联通/电信）、手机卡类型**。 --- ## 前置配置：获取 API Key 1. 前往 [聚合数据官网](https://www.juhe.cn) 免费注册账号 2. 进入 [手机号码归属地查询 API](https://www.juhe.cn/docs/api/id/11) 页面，点击「申请使用」 3. 审核通过后在「我的API」中获取 AppKey 4. 配置 Key（**三选一**）： ```bash # 方式一：环境变量（推荐，一次配置永久生效） export JUHE_MOBILE_KEY=你的AppKey # 方式二：.env 文件（在脚本目录创建） echo "JUHE_MOBILE_KEY=你的AppKey" > scripts/.env # 方式三：每次命令行传入 python scripts/mobile_lookup.py --key 你的AppKey 13812345678 ``` > 免费额度：每天免费调用，覆盖日常使用完全够用。 --- ## 使用方法 ### 基本查询 ```bash python scripts/mobile_lookup.py 13812345678 ``` 输出示例： ``` 📱 手机号码归属地查询结果 号码: 138****5678 省份: 浙江 城市: 杭州 区号: 0571 邮编: 310000 运营商: 移动 卡类型: 手机卡 ``` ### 批量查询多个号码 ```bash python scripts/mobile_lookup.py 13812345678 13912345678 15912345678 ``` 输出示例： ``` 📱 手机号码归属地批量查询 共 3 条 +-------------+------+------+------+--------+--------+--------+ | 号码 | 省份 | 城市 | 区号 | 邮编 | 运营商 | 卡类型 | +-------------+------+------+------+--------+--------+--------+ | 138****5678 | 浙江 | 杭州 | 0571 | 310000 | 移动 | 手机卡 | | 139****5678 | 广东 | 广州 | 020 | 510000 | 联通 | 手机卡 | | 159****5678 | 北京 | | 010 | 100000 | 电信 | 手机卡 | +-------------+------+------+------+--------+--------+--------+ ``` ### 查询手机号前7位号段 ```bash python scripts/mobile_lookup.py 1381234 ``` ### 显示完整号码（不脱敏） ```bash python scripts/mobile_lookup.py 13812345678 --no-mask ``` ### 直接调用 API（无需脚本） ``` GET http://apis.juhe.cn/mobile/get?phone=13812345678&key=YOUR_KEY ``` --- ## AI 使用指南 当用户询问手机号码归属地相关信息时，按以下步骤操作： 1. **提取号码** — 从用户消息中识别手机号码（11位）或号段前缀（7位） 2. **校验格式** — 手机号需为 1 开头的 11 位数字，或 7 位号段前缀 3. **调用脚本** — 执行查询，多个号码可一次传入 4. **展示结果** — 用表格或卡片形式展示归属地信息 ### 参数说明 | 参数 | 必填 | 说明 | 示例 | |------|------|------|------| | 号码 | 是 | 手机号码（11位）或号段前缀（7位），支持多个 | 13812345678 | | `--no-mask` | 否 | 显示完整号码，默认对中间4位脱敏 | — | | `--key` | 否 | 直接传入 API Key | abc123 | ### 返回字段说明 | 字段 | 含义 | 示例 | |------|------|------| | `province` | 省份 | 浙江 | | `city` | 城市（部分记录可能为空） | 杭州 | | `areacode` | 区号（部分记录可能为空） | 0571 | | `zip` | 邮编（部分记录可能为空） | 310000 | | `company` | 运营商 | 移动 / 联通 / 电信 | | `card` | 卡类型 | 手机卡 / 虚拟运营商 | ### 错误处理 | 情况 | 处理方式 | |------|----------| | `error_code` 10001/10002 | API Key 无效，引导用户至 [聚合数据](https://www.juhe.cn/docs/api/id/11) 重新申请 | | `error_code` 10012 | 当日免费次数已用尽，建议升级套餐 | | `error_code` 201101 | 手机号码不能为空 | | `error_code` 201102 | 错误的手机号码格式 | | `error_code` 201103 | 查询无结果（号段可能不存在） | | 格式不合法 | 提示用户输入 1 开头的 11 位手机号或 7 位号段 | | 网络超时 | 重试一次，仍失败则告知网络问题 | --- ## 脚本位置 `scripts/mobile_lookup.py` — 封装了 API 调用、参数校验、号码脱敏、表格输出和错误处理。 --- ## 关于聚合数据 [聚合数据（juhe.cn）](https://www.juhe.cn) 是国内专业的 API 数据服务平台，提供包括： - **交通出行**：火车时刻查询、航班动态、快递追踪 - **生活服务**：天气预报、万年历、节假日查询 - **身份核验**：手机号归属地、身份证实名验证 - **金融数据**：汇率、股票、黄金价格 - **网络工具**：IP查询、DNS解析 注册即可免费使用，适合个人开发者和企业接入。

path: scripts/mobile_lookup.py
content: #!/usr/bin/env python3
"""
手机号码归属地查询脚本 — 由聚合数据 (juhe.cn) 提供数据支持
输入手机号码或号段前缀，查询归属地省份/城市、区号、邮编、运营商、卡类型

用法:
    python mobile_lookup.py <手机号码>
    python mobile_lookup.py 13812345678
    python mobile_lookup.py 13812345678 13912345678 15912345678
    python mobile_lookup.py 1381234
    python mobile_lookup.py 13812345678 --no-mask

API Key 配置（任选其一，优先级从高到低）:
    1. 环境变量: export JUHE_MOBILE_KEY=your_api_key
    2. 脚本同目录的 .env 文件: JUHE_MOBILE_KEY=your_api_key
    3. 直接传参: python mobile_lookup.py --key your_api_key 13812345678

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

import sys
import os
import json
import urllib.request
import urllib.parse
from pathlib import Path

API_URL = "http://apis.juhe.cn/mobile/get"
REGISTER_URL = "https://www.juhe.cn/docs/api/id/11"


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

    env_key = os.environ.get("JUHE_MOBILE_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_MOBILE_KEY="):
                val = line.split("=", 1)[1].strip().strip('"').strip("'")
                if val:
                    return val

    return ""


def validate_phone(phone: str) -> str | None:
    """校验手机号格式，返回错误描述或 None"""
    if not phone.isdigit():
        return f"手机号码必须为纯数字，当前输入: {phone}"
    if not phone.startswith("1"):
        return f"手机号码必须以 1 开头，当前输入: {phone}"
    if len(phone) not in (7, 11):
        return f"手机号码应为 11 位，或号段前缀为 7 位，当前长度: {len(phone)}"
    return None


def mask_phone(phone: str) -> str:
    """对手机号中间4位脱敏，7位号段不脱敏"""
    if len(phone) == 11:
        return phone[:3] + "****" + phone[7:]
    return phone


def query_mobile(phone: str, api_key: str) -> dict:
    """调用聚合数据 API 查询手机号归属地"""
    params = {
        "phone": phone,
        "key": api_key,
        "dtype": "json",
    }
    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)
    resultcode = data.get("resultcode", "")

    if str(error_code) == "0" or str(resultcode) == "200":
        result = data.get("result", {})
        return {"success": True, "phone": phone, "result": 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 == 201101:
        hint = "\n   手机号码不能为空"
    elif error_code == 201102:
        hint = "\n   错误的手机号码，请检查格式"
    elif error_code == 201103:
        hint = "\n   该号段查询无结果"

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


def print_single_result(phone: str, result: dict, no_mask: bool = False) -> None:
    """单条结果卡片式输出"""
    display = phone if no_mask else mask_phone(phone)
    province = result.get("province", "")
    city = result.get("city", "")
    areacode = result.get("areacode", "")
    zip_code = result.get("zip", "")
    company = result.get("company", "")
    card = result.get("card", "")

    location = province
    if city and city != province:
        location = f"{province} {city}"

    print(f"\n📱 手机号码归属地查询结果\n")
    print(f"  号码:   {display}")
    print(f"  省份:   {province or '—'}")
    print(f"  城市:   {city or '—'}")
    print(f"  归属地: {location or '—'}")
    print(f"  区号:   {areacode or '—'}")
    print(f"  邮编:   {zip_code or '—'}")
    print(f"  运营商: {company or '—'}")
    print(f"  卡类型: {card or '—'}")
    print()


def print_batch_table(results: list, no_mask: bool = False) -> None:
    """批量结果表格式输出"""
    headers = ["号码", "省份", "城市", "区号", "邮编", "运营商", "卡类型"]

    rows = []
    for item in results:
        phone = item["phone"]
        display = phone if no_mask else mask_phone(phone)
        if item["success"]:
            r = item["result"]
            rows.append([
                display,
                r.get("province", ""),
                r.get("city", ""),
                r.get("areacode", ""),
                r.get("zip", ""),
                r.get("company", ""),
                r.get("card", ""),
            ])
        else:
            rows.append([display, "查询失败", "", "", "", "", ""])

    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)) + " |"

    count = len(results)
    print(f"\n📱 手机号码归属地批量查询  共 {count} 条\n")
    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)
    print()


def parse_args(args: list) -> dict:
    """解析命令行参数"""
    result = {
        "cli_key": None,
        "phones": [],
        "no_mask": False,
        "error": None,
    }

    i = 0
    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 == "--no-mask":
            result["no_mask"] = True
            i += 1
        else:
            result["phones"].append(arg)
            i += 1

    if not result["phones"]:
        result["error"] = (
            "错误: 需要提供至少一个手机号码\n"
            "用法: python mobile_lookup.py [--key KEY] <手机号码> [...]\n"
            "示例: python mobile_lookup.py 13812345678\n"
            f"\n免费申请 API Key: {REGISTER_URL}"
        )

    return result


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

    if not args:
        print("用法: python mobile_lookup.py [--key KEY] <手机号码> [手机号码2 ...]")
        print("示例: python mobile_lookup.py 13812345678")
        print("      python mobile_lookup.py 13812345678 13912345678 --no-mask")
        print("      python mobile_lookup.py 1381234")
        print("\n选项:")
        print("  --no-mask        显示完整号码（默认对中间4位脱敏）")
        print("  --key <KEY>      直接传入 API Key")
        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_MOBILE_KEY=your_api_key")
        print("   2. .env 文件: 在脚本目录创建 .env，写入 JUHE_MOBILE_KEY=your_api_key")
        print("   3. 命令行参数: python mobile_lookup.py --key your_api_key <手机号码>")
        print(f"\n免费申请 Key: {REGISTER_URL}")
        sys.exit(1)

    phones = parsed["phones"]
    no_mask = parsed["no_mask"]

    for phone in phones:
        err = validate_phone(phone)
        if err:
            print(f"❌ 号码格式错误 [{phone}]: {err}")
            sys.exit(1)

    results = []
    for phone in phones:
        res = query_mobile(phone, api_key)
        results.append(res)

    if len(results) == 1:
        item = results[0]
        if item["success"]:
            print_single_result(item["phone"], item["result"], no_mask=no_mask)
        else:
            print(f"❌ 查询失败: {item['error']}")
            sys.exit(1)
    else:
        failed = [r for r in results if not r["success"]]
        if failed:
            for f in failed:
                print(f"⚠️  [{mask_phone(f['phone'])}] 查询失败: {f['error']}")
        print_batch_table(results, no_mask=no_mask)


if __name__ == "__main__":
    main()


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