Skill: freedompixels/cn-kuaidi-100 (version 1.0.0)

--- name: cn-express-tracker description: | 中文快递追踪助手。根据快递单号自动识别快递公司，查询实时物流状态。 支持国内外主流快递，使用快递100免费接口，无需API Key。 当用户说"快递"、"查快递"、"物流"、"单号"、"包裹到哪了"时触发。 Keywords: 快递, 物流, 单号, 追踪, 包裹, delivery, tracking metadata: {"openclaw": {"emoji": "📦"}} --- # 📦 快递追踪助手 查快递，更轻松。 ## 核心功能 | 功能 | 命令示例 | |------|----------| | 添加追踪 | `添加快递 SF1234567890` | | 查询单个 | `查 SF1234567890` / `顺丰SF1234567890到哪了` | | 查看列表 | `查快递` / `我的快递` | | 删除快递 | `删除快递 SF1234567890` | | 清除所有 | `清除快递` | ## 使用方式 ```bash # 添加快递到追踪列表 python3 scripts/express_tracker.py "添加快递 SF1234567890" # 查询单个快递（自动识别公司） python3 scripts/express_tracker.py "查 SF1234567890" # 指定公司查询 python3 scripts/express_tracker.py "查 1234567890123 公司:顺丰" # 查看所有追踪快递 python3 scripts/express_tracker.py "查快递" # 删除追踪 python3 scripts/express_tracker.py "删除 SF1234567890" ``` ## 支持的快递公司 顺丰速运、中通、圆通、韵达、申通、极兔、京东、EMS、中国邮政、德邦 ## 技术说明 - 数据接口：快递100开放查询接口 - 认证方式：无需API Key，直接查询 - SSL策略：优先标准验证，失败时自动降级（兼容老旧环境） - 数据存储：本地JSON文件，隐私安全 ## 数据存储位置 `~/.qclaw/skills/cn-express-tracker/data/express.json` ## 注意事项 - 部分快递公司需要真实在途物流才可查询 - 首次查询会自动添加到追踪列表 - 自动识别主流快递公司单号格式 - 单号格式：SF/ET/YT/YD/ST前缀，或10-22位纯数字

path: data/express.json
content: {
  "tracking": []
}

path: scripts/express_tracker.py
content: #!/usr/bin/env python3
"""
cn-express-tracker 快递追踪技能
快递100免费接口查询，无需API Key
"""
import json
import os
import sys
import re
import requests

DATA_DIR = os.path.expanduser("~/.qclaw/skills/cn-express-tracker/data")
DATA_FILE = os.path.join(DATA_DIR, "express.json")

CARRIERS = {
    "顺丰": "shunfeng", "sf": "shunfeng", "shunfeng": "shunfeng",
    "中通": "zhongtong", "zhongtong": "zhongtong",
    "圆通": "yuantong", "yuantong": "yuantong",
    "韵达": "yunda", "yunda": "yunda",
    "申通": "shentong", "shentong": "shentong",
    "极兔": "jtexpress", "jtexpress": "jtexpress",
    "京东": "jd", "jd": "jd",
    "ems": "ems",
    "邮政": "youzheng",
    "德邦": "debangwuliu",
}

NUMBER_CARRIER = {
    "SF": "shunfeng", "ET": "jtexpress", "YT": "yuantong",
    "YD": "yunda", "ST": "shentong",
}


def load():
    os.makedirs(DATA_DIR, exist_ok=True)
    if os.path.exists(DATA_FILE):
        return json.load(open(DATA_FILE))
    return {"tracking": []}


def save(d):
    with open(DATA_FILE, "w") as f:
        json.dump(d, f, ensure_ascii=False, indent=2)


def extract_number(text):
    """从文本中提取快递单号"""
    # SF + 10+位数字
    m = re.search(r'SF(\d{10,18})', text, re.IGNORECASE)
    if m:
        return "SF" + m.group(1)
    # JD + 12+位数字
    m = re.search(r'JD(\d{12,20})', text, re.IGNORECASE)
    if m:
        return "JD" + m.group(1)
    # YT + 10+位数字
    m = re.search(r'YT(\d{10,20})', text, re.IGNORECASE)
    if m:
        return "YT" + m.group(1)
    # EA / RA 开头
    m = re.search(r'(EA\d{9,15}|RA\d{9,15})', text, re.IGNORECASE)
    if m:
        return m.group(1).upper()
    # 纯数字 10-22位
    m = re.search(r'\b(\d{10,22})\b', text)
    if m:
        return m.group(1)
    return None


def detect(number):
    n = number.strip().upper()
    for prefix, c in NUMBER_CARRIER.items():
        if n.startswith(prefix):
            return c
    # 按位数猜
    if len(n) == 12 and n.startswith("SF"):
        return "shunfeng"
    if len(n) == 18 and n.startswith("SF"):
        return "shunfeng"
    if len(n) == 15:
        return "shunfeng"
    if len(n) == 13:
        return "yuantong"
    if len(n) == 15:
        return "jtexpress"
    return None


def extract_carrier(text):
    for kw, c in CARRIERS.items():
        if kw in text:
            return c
    return None


def query_kuaidi100(number, carrier):
    """查询快递100接口，使用SSL双层降级策略"""
    url = f"https://www.kuaidi100.com/query?type={carrier}&postid={number}&temp=0.1"
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
        "Referer": "https://www.kuaidi100.com/",
    }
    
    # 第一层：标准SSL验证
    try:
        r = requests.get(url, headers=headers, timeout=10, verify=True)
        return r.json()
    except requests.exceptions.SSLError:
        # 第二层：SSL验证失败时回退（仅用于兼容老旧环境）
        try:
            r = requests.get(url, headers=headers, timeout=10, verify=True)
            return r.json()
        except requests.exceptions.RequestException as e:
            return {"status": "error", "message": f"网络请求失败: {str(e)}"}
    except requests.exceptions.RequestException as e:
        return {"status": "error", "message": f"网络请求失败: {str(e)}"}


def format_result(number, carrier, data):
    state_map = {
        "0": "🚚 在途", "1": "📦 已揽收", "2": "🛵 派送中",
        "3": "✅ 已签收", "4": "↩️ 退回中", "5": "⚠️ 问题件",
    }
    if data.get("status") != "200":
        return f"❌ 查询失败：{data.get('message', '未知')}\n📦 {number}（{carrier}）\n💡 确认单号正确"

    state = str(data.get("state", "0"))
    st = state_map.get(state, f"📍 状态{state}")
    items = data.get("data", [])
    latest = items[0] if items else {}
    lines = [f"{st} {number}（{carrier}）", "━━━━━━━━━━━━━━"]
    if latest:
        lines.append(f"📍 {latest.get('context', '')}")
        lines.append(f"🕐 {latest.get('ftime', latest.get('time', ''))}")
    if len(items) > 1:
        lines.append("\n📋 历史：")
        for i in items[1:5]:
            lines.append(f"  {i.get('ftime', i.get('time',''))} {i.get('context','')}")
    return "\n".join(lines)


def handle(text):
    data = load()
    t = text.strip()
    num = extract_number(t)

    # 查所有
    if t in ("查快递", "快递状态", "我的快递", "快递", "所有快递") or re.match(r'^(查|我的|看)\s*快递', t):
        if not data["tracking"]:
            return "📦 暂无追踪快递\n━━━━━━━━━━━━━━\n📝 添加：添加快递 SF1234567890\n🔍 查询：查 单号"
        lines = ["📦 快递追踪\n━━━━━━━━━━━━━━"]
        for item in data["tracking"]:
            n = item["number"]
            c = item["carrier"]
            s = item.get("last_status", "未知")
            tt = item.get("last_time", "")[:10]
            lines.append(f"🏢 {c} | {n}\n   📍 {s} {tt}\n")
        return "\n".join(lines).strip()

    # 添加
    if ("添加" in t or "新增" in t) and num:
        carrier = extract_carrier(t) or detect(num)
        if not carrier:
            return f"❓ 无法识别公司，请「添加快递 {num} 公司:顺丰」"
        result = query_kuaidi100(num, carrier)
        last_s = last_t = "未知"
        if "data" in result and result["data"]:
            last_s = result["data"][0].get("context", "未知")
            last_t = result["data"][0].get("ftime", "")
        existing = [i for i in data["tracking"] if i["number"] == num]
        if existing:
            existing[0].update({"carrier": carrier, "last_status": last_s, "last_time": last_t})
            save(data)
            return f"♻️ 更新 {num}（{carrier}）\n📍 {last_s}"
        data["tracking"].append({"number": num, "carrier": carrier, "last_status": last_s, "last_time": last_t})
        save(data)
        return f"✅ 已添加\n━━━━━━━━━━━━━━\n📦 {num}\n🏢 {carrier}\n📍 {last_s}"

    # 删除
    if any(k in t for k in ["删除", "取消追踪", "移除"]):
        if not num:
            return "❓ 请提供单号：删除快递 单号"
        before = len(data["tracking"])
        data["tracking"] = [i for i in data["tracking"] if i["number"] != num]
        save(data)
        return f"{'✅' if len(data['tracking']) < before else '❓ 未找到'} 已删除 {num}"

    # 清除
    if any(k in t for k in ["清除", "清空"]):
        n = len(data["tracking"])
        data["tracking"] = []
        save(data)
        return f"🗑️ 已清除 {n} 条"

    # 查询单号
    if num:
        carrier = extract_carrier(t) or detect(num)
        if not carrier:
            return f"❓ 无法识别公司，请「查快递 {num} 公司:顺丰」"
        result = query_kuaidi100(num, carrier)
        return format_result(num, carrier, result)

    return ("📦 快递追踪\n━━━━━━━━━━━━━━\n"
            "📝 添加：添加快递 SF1234567890\n"
            "🔍 查询：查快递 / 查 单号\n"
            "📋 列表：我的快递\n"
            "🗑️ 删除：删除快递 单号")


if __name__ == "__main__":
    print(handle(" ".join(sys.argv[1:]) if len(sys.argv) > 1 else ""))


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