Skill: huhao19871026/bijieserv-express (version 1.1.0)

--- name: bijie-express-tracking description: Fast package tracking and logistics timeline lookup for 2000+ couriers with BijieServ. Use for 查快递, 快递查询, 查物流, 物流轨迹, 快递到哪了, 包裹追踪, track package, where is my package, courier tracking, 顺丰查询, 圆通查询, 中通查询, 韵达查询, 申通查询, 极兔查询, 京东物流查询, EMS查询. --- # Bijie Express Skill Fast express tracking, package lookup, and logistics timeline query through BijieServ. Use this skill for package tracking, courier tracking, shipping status lookup, logistics timeline lookup, and Chinese courier queries such as 顺丰, 圆通, 中通, 韵达, 申通, 极兔, 京东物流, and EMS. Return a concise user-facing summary with: - current package status - latest logistics update - recent tracking timeline - ETA when available - masked sensitive data This skill is written to be easy for both Codex-style agents and OpenClaw-style agents to follow: - Prefer deterministic execution over free-form API reconstruction. - Reuse the local script when possible. - Ask the user for missing information only when needed. ## When To Use Use this skill when the user wants to: - check a package status - view logistics updates or delivery timeline - identify the courier and query a tracking number - track common domestic or international carriers supported by BijieServ Typical requests: - `查一下 SF13156789012` - `帮我查这个快递到哪了` - `track package 123456789` - `查询圆通/顺丰/中通/韵达/申通/极兔/京东/EMS` ## Source Of Truth Prefer these local files in this order: 1. `scripts/express.py`: canonical query logic, headers, parameter handling, masking, and response formatting. 2. `references/company-codes.md`: courier code lookup table when carrier mapping is uncertain. 3. This file: behavioral guidance for when to run, what to ask, and how to present results. If this file conflicts with the script, follow the script. ## Execution Policy - Prefer running the local script instead of manually composing raw HTTP requests. - Treat the script behavior as authoritative for endpoint, headers, payload, and error handling. - Do not promise capabilities that are not implemented locally. - Mask phone numbers or other sensitive details before showing results to the user. ## Inputs Required: - tracking number Optional: - courier name or courier code - phone number if the carrier requires extra verification - origin or destination hints when needed ## Recommended Flow 1. Extract the tracking number and optional courier name from the user request. 2. Validate the tracking number at a basic level. A practical baseline is 6 to 32 characters, matching the local script. 3. If the user provided a courier, map it to a company code. 4. If the user did not provide a courier: - first infer from common prefixes such as `SF`, `YT`, `ZT`, `YD`, `ST`, `JT`, `JD` - if still unclear, consult `references/company-codes.md` - if still unknown, ask the user which courier it is 5. Run the local query logic from `scripts/express.py`. 6. Return a concise, user-friendly summary: - courier name - tracking number - current status - latest update - recent timeline - ETA if available 7. Apply privacy masking before showing the result. ## Output Style Keep the response compact and practical. Prefer: - current status first - latest logistics event second - recent timeline after that - ETA only when the API provides it Good structure: - `状态`: 已签收 / 派送中 / 运输中 / 异常件 / 退回中 - `最新轨迹`: the most recent meaningful tracking update - `时间线`: a short list of recent nodes - `预计送达`: only if present ## Privacy Rules - Mask mobile numbers like `138****1234`. - Avoid exposing full recipient address details. - If the upstream response contains sensitive text, sanitize it before presenting it. ## Error Handling Use these handling rules: - Invalid tracking number: tell the user the number appears malformed and ask them to re-check it. - Unknown courier: ask the user to confirm the courier name. - No tracking result: explain that the parcel may not be shipped yet, the number may be wrong, or the carrier may not have updated tracking. - Rate limiting or lockout: explain that queries are too frequent and suggest trying again later. - Timeout or request failure: tell the user the query failed temporarily and suggest retrying. ## Courier Mapping Notes Common mappings: - `SF` -> `shunfeng` - `YT` -> `yuantong` - `ZT` -> `zhongtong` - `YD` -> `yunda` - `ST` -> `shentong` - `JT` -> `jtexpress` - `JD` -> `jd` - `EMS` -> `ems` For a broader list, use `references/company-codes.md`. ## OpenClaw Compatibility Notes To keep this skill compatible with OpenClaw-style agents: - keep behavior instructions explicit and step-based - prefer local files over hidden assumptions - avoid requiring agent-specific internal memory - do not rely on rich UI formatting to make the result understandable - keep the frontmatter short and searchable If OpenClaw loads only the skill text and not repository code automatically, the agent should explicitly inspect: - `scripts/express.py` - `references/company-codes.md` ## Search And Discovery Notes To improve discoverability on skill hubs such as ClawHub: - put the core task words near the top of the file: package tracking, express tracking, logistics query, courier tracking, 查快递, 查物流 - keep the `name` and `description` closely aligned with what users actually search for - mention both brand and task intent so users can find the skill by either `BijieServ` or `快递查询` - include both Chinese and English task phrases because users may search in either language - avoid vague marketing language that does not help the retrieval system understand the skill Useful search intents covered by this skill: - 查快递 - 快递查询 - 查物流 - 物流轨迹 - 快递到哪了 - 包裹追踪 - track package - where is my package - courier tracking - shipping status ## Notes - Send requests to `https://www.bijieserv.com/api/method/express_app.open.v1.query.exec`, not `skill.bijieserv.com`. - Keep `Origin` and `Referer` aligned with `https://www.bijieserv.com/` when following the local script behavior. - Do not claim per-user quota tracking, shared cache behavior, or automatic compliance enforcement unless those behaviors are actually implemented in code.

path: _meta.json
content: ownerId: kn77mwnkw0by7y2mt0t5hxnsbx83h8pc
slug: bijieserv-express
version: 1.0.0
publishedAt: 1774367423768

path: package-lock.json
content: {
  "name": "bijie-express",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "bijie-express",
      "version": "1.0.0",
      "license": "MIT",
      "dependencies": {
        "clawhub": "^0.9.0"
      }
    },
    "node_modules/@ark/schema": {
      "version": "0.56.0",
      "resolved": "https://r.cnpmjs.org/@ark/schema/-/schema-0.56.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@ark/util": "0.56.0"
      }
    },
    "node_modules/@ark/util": {
      "version": "0.56.0",
      "resolved": "https://r.cnpmjs.org/@ark/util/-/util-0.56.0.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/@clack/core": {
      "version": "1.2.0",
      "resolved": "https://r.cnpmjs.org/@clack/core/-/core-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "fast-wrap-ansi": "^0.1.3",
        "sisteransi": "^1.0.5"
      }
    },
    "node_modules/@clack/prompts": {
      "version": "1.2.0",
      "resolved": "https://r.cnpmjs.org/@clack/prompts/-/prompts-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@clack/core": "1.2.0",
        "fast-string-width": "^1.1.0",
        "fast-wrap-ansi": "^0.1.3",
        "sisteransi": "^1.0.5"
      }
    },
    "node_modules/ansi-regex": {
      "version": "6.2.2",
      "resolved": "https://r.cnpmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
      }
    },
    "node_modules/arkregex": {
      "version": "0.0.5",
      "resolved": "https://r.cnpmjs.org/arkregex/-/arkregex-0.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@ark/util": "0.56.0"
      }
    },
    "node_modules/arktype": {
      "version": "2.2.0",
      "resolved": "https://r.cnpmjs.org/arktype/-/arktype-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@ark/schema": "0.56.0",
        "@ark/util": "0.56.0",
        "arkregex": "0.0.5"
      }
    },
    "node_modules/chalk": {
      "version": "5.6.2",
      "resolved": "https://r.cnpmjs.org/chalk/-/chalk-5.6.2.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": "^12.17.0 || ^14.13 || >=16.0.0"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/clawhub": {
      "version": "0.9.0",
      "resolved": "https://r.cnpmjs.org/clawhub/-/clawhub-0.9.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "@clack/prompts": "^1.1.0",
        "arktype": "^2.2.0",
        "commander": "^14.0.3",
        "fflate": "^0.8.2",
        "ignore": "^7.0.5",
        "json5": "^2.2.3",
        "mime": "^4.1.0",
        "ora": "^9.3.0",
        "p-retry": "^7.1.1",
        "semver": "^7.7.4",
        "undici": "^7.24.0"
      },
      "bin": {
        "clawdhub": "bin/clawdhub.js",
        "clawhub": "bin/clawdhub.js"
      },
      "engines": {
        "node": ">=20"
      }
    },
    "node_modules/cli-cursor": {
      "version": "5.0.0",
      "resolved": "https://r.cnpmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "restore-cursor": "^5.0.0"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/cli-spinners": {
      "version": "3.4.0",
      "resolved": "https://r.cnpmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=18.20"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/commander": {
      "version": "14.0.3",
      "resolved": "https://r.cnpmjs.org/commander/-/commander-14.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=20"
      }
    },
    "node_modules/fast-string-truncated-width": {
      "version": "1.2.1",
      "resolved": "https://r.cnpmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-1.2.1.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/fast-string-width": {
      "version": "1.1.0",
      "resolved": "https://r.cnpmjs.org/fast-string-width/-/fast-string-width-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "fast-string-truncated-width": "^1.2.0"
      }
    },
    "node_modules/fast-wrap-ansi": {
      "version": "0.1.6",
      "resolved": "https://r.cnpmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.1.6.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "fast-string-width": "^1.1.0"
      }
    },
    "node_modules/fflate": {
      "version": "0.8.2",
      "resolved": "https://r.cnpmjs.org/fflate/-/fflate-0.8.2.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/get-east-asian-width": {
      "version": "1.5.0",
      "resolved": "https://r.cnpmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/ignore": {
      "version": "7.0.5",
      "resolved": "https://r.cnpmjs.org/ignore/-/ignore-7.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">= 4"
      }
    },
    "node_modules/is-interactive": {
      "version": "2.0.0",
      "resolved": "https://r2.cnpmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/is-network-error": {
      "version": "1.3.1",
      "resolved": "https://r.cnpmjs.org/is-network-error/-/is-network-error-1.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=16"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/is-unicode-supported": {
      "version": "2.1.0",
      "resolved": "https://r.cnpmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/json5": {
      "version": "2.2.3",
      "resolved": "https://r.cnpmjs.org/json5/-/json5-2.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "bin": {
        "json5": "lib/cli.js"
      },
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/log-symbols": {
      "version": "7.0.1",
      "resolved": "https://r.cnpmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "is-unicode-supported": "^2.0.0",
        "yoctocolors": "^2.1.1"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/mime": {
      "version": "4.1.0",
      "resolved": "https://r.cnpmjs.org/mime/-/mime-4.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        "https://github.com/sponsors/broofa"
      ],
      "bin": {
        "mime": "bin/cli.js"
      },
      "engines": {
        "node": ">=16"
      }
    },
    "node_modules/mimic-function": {
      "version": "5.0.1",
      "resolved": "https://r.cnpmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/onetime": {
      "version": "7.0.0",
      "resolved": "https://r.cnpmjs.org/onetime/-/onetime-7.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "mimic-function": "^5.0.0"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/ora": {
      "version": "9.3.0",
      "resolved": "https://r.cnpmjs.org/ora/-/ora-9.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "chalk": "^5.6.2",
        "cli-cursor": "^5.0.0",
        "cli-spinners": "^3.2.0",
        "is-interactive": "^2.0.0",
        "is-unicode-supported": "^2.1.0",
        "log-symbols": "^7.0.1",
        "stdin-discarder": "^0.3.1",
        "string-width": "^8.1.0"
      },
      "engines": {
        "node": ">=20"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/p-retry": {
      "version": "7.1.1",
      "resolved": "https://r.cnpmjs.org/p-retry/-/p-retry-7.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "is-network-error": "^1.1.0"
      },
      "engines": {
        "node": ">=20"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/restore-cursor": {
      "version": "5.1.0",
      "resolved": "https://r.cnpmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "onetime": "^7.0.0",
        "signal-exit": "^4.1.0"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/semver": {
      "version": "7.7.4",
      "resolved": "https://r.cnpmjs.org/semver/-/semver-7.7.4.tgz",
      "integrity": [REDACTED_SECRET],
      "bin": {
        "semver": "bin/semver.js"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/signal-exit": {
      "version": "4.1.0",
      "resolved": "https://r.cnpmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=14"
      },
      "funding": {
        "url": "https://github.com/sponsors/isaacs"
      }
    },
    "node_modules/sisteransi": {
      "version": "1.0.5",
      "resolved": "https://r2.cnpmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
      "integrity": [REDACTED_SECRET]
    },
    "node_modules/stdin-discarder": {
      "version": "0.3.1",
      "resolved": "https://r.cnpmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/string-width": {
      "version": "8.2.0",
      "resolved": "https://r.cnpmjs.org/string-width/-/string-width-8.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "get-east-asian-width": "^1.5.0",
        "strip-ansi": "^7.1.2"
      },
      "engines": {
        "node": ">=20"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/strip-ansi": {
      "version": "7.2.0",
      "resolved": "https://r.cnpmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dependencies": {
        "ansi-regex": "^6.2.2"
      },
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
      }
    },
    "node_modules/undici": {
      "version": "7.24.7",
      "resolved": "https://r.cnpmjs.org/undici/-/undici-7.24.7.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=20.18.1"
      }
    },
    "node_modules/yoctocolors": {
      "version": "2.1.2",
      "resolved": "https://r.cnpmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    }
  }
}


path: package.json
content: {
  "name": "bijie-express",
  "version": "1.0.0",
  "description": "Free package tracking for 2000+ couriers (SF, YTO, ZTO, Yunda, STO, JT, JD, EMS). Returns logistics status, location, transit nodes & ETA. 必捷免费快递查询，支持国内外2000+快递公司，自动识别单号，返回包裹状态、物流轨迹、预计到达时间。",
  "author": "bijieserv",
  "license": "MIT",
  "dependencies": {
    "clawhub": "^0.9.0"
  }
}


path: scripts/express.py
content: #!/usr/bin/env python3
"""
必捷免费快递查询 SKILL
功能：免费实时查询快递物流轨迹，支持2000+快递公司
"""

import os
import re
import json
import requests
from datetime import datetime
from typing import Dict, List, Optional, Tuple

class BJExpress:
    """必捷免费快递查询器"""
    
    # API 地址
    API_URL = "https://www.bijieserv.com/api/method/express_app.open.v1.query.exec"
    
    # 快递公司编码映射（常用）
    EXPRESS_COMPANIES = {
        'yuantong': '圆通速递',
        'zhongtong': '中通快递',
        'shunfeng': '顺丰速运',
        'yunda': '韵达快递',
        'shentong': '申通快递',
        'jd': '京东物流',
        'jtexpress': '极兔速递',
        'ems': 'EMS',
        'debangkuaidi': '德邦快递',
        'danniao': '菜鸟速递',
        'youzhengguonei': '邮政快递包裹',
        'dhl': 'DHL',
        'fedex': 'FedEx',
        'ups': 'UPS',
        'tnt': 'TNT'
    }
    
    # 物流状态映射
    STATE_MAP = {
        '0': ('在途', '快件在运输途中'),
        '1': ('已揽收', '快递公司已揽收'),
        '2': ('疑难', '快件存在异常情况'),
        '3': ('已签收', '快件已签收'),
        '4': ('退签', '快件已退签'),
        '5': ('派送中', '快件正在派件中'),
        '6': ('退回', '快件正在返回发货人途中'),
        '7': ('转投', '快件转给其他快递公司'),
        '8': ('清关', '快件正在清关'),
        '10': ('待清关', '快件等待清关'),
        '11': ('清关中', '快件正在清关流程中'),
        '12': ('已清关', '快件已完成清关'),
        '13': ('清关异常', '清关过程中出现异常'),
        '14': ('拒签', '收件人拒绝签收')
    }
    
    # 默认请求头（必需）
    DEFAULT_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/javascript, */*; q=0.01',
        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
        'Origin': 'https://www.bijieserv.com',
        'Referer': 'https://www.bijieserv.com/',
        'Connection': 'keep-alive',
    }
    
    def __init__(self):
        """初始化"""
        self.query_history = {}  # 查询历史，用于锁单检测
    
    def validate_tracking_number(self, num: str) -> bool:
        """验证快递单号格式"""
        if not num:
            return False
        return 6 <= len(num) <= 32
    
    def validate_phone(self, phone: str) -> bool:
        """验证手机号格式"""
        if not phone:
            return True  # 选填，不验证
        return bool(re.match(r'^1[3-9]\d{9}$', phone))
    
    def check_lockout(self, num: str) -> Tuple[bool, str]:
        """
        检查是否可能锁单
        
        Returns:
            (是否安全, 提示信息)
        """
        if num in self.query_history:
            last_query = self.query_history[num]
            elapsed = (datetime.now() - last_query).total_seconds()
            if elapsed < 1800:  # 30分钟
                remaining = 1800 - elapsed
                return False, f"⚠️ 该单号 {int(remaining/60)} 分钟前已查询过，频繁查询可能导致锁单。建议 {int(remaining/60)} 分钟后再查。"
        return True, ""
    
    def map_company_code(self, company_name: str) -> str:
        """
        映射快递公司名称到编码
        
        Args:
            company_name: 快递公司名称（如"圆通"、"顺丰"）
        
        Returns:
            快递公司编码
        """
        company_name = company_name.lower().strip()
        
        # 直接匹配
        if company_name in self.EXPRESS_COMPANIES:
            return company_name
        
        # 模糊匹配
        mapping = {
            '圆通': 'yuantong', 'yt': 'yuantong',
            '中通': 'zhongtong', 'zt': 'zhongtong',
            '顺丰': 'shunfeng', 'sf': 'shunfeng',
            '韵达': 'yunda', 'yd': 'yunda',
            '申通': 'shentong', 'st': 'shentong',
            '京东': 'jd', 'jd': 'jd',
            '极兔': 'jtexpress', 'jt': 'jtexpress',
            'ems': 'ems', '邮政': 'youzhengguonei',
            '德邦': 'debangkuaidi', 'db': 'debangkuaidi',
            '菜鸟': 'danniao',
            'dhl': 'dhl',
            'fedex': 'fedex', '联邦': 'fedex',
            'ups': 'ups',
            'tnt': 'tnt'
        }
        
        return mapping.get(company_name, company_name)
    
    def get_company_name(self, code: str) -> str:
        """获取快递公司名称"""
        return self.EXPRESS_COMPANIES.get(code, code)
    
    def desensitize_text(self, text: str) -> str:
        """
        脱敏处理：隐藏手机号中间4位
        
        Args:
            text: 原始文本
        
        Returns:
            脱敏后的文本
        """
        if not text:
            return text
        
        # 匹配手机号 (11位数字)
        phone_pattern = r'(1[3-9]\d)(\d{4})(\d{4})'
        text = re.sub(phone_pattern, r'\1****\3', text)
        
        # 匹配固话 (区号-号码)
        tel_pattern = r'(\d{3,4}-)(\d{3})(\d{4})'
        text = re.sub(tel_pattern, r'\1\2****', text)
        
        return text
    
    def query(self, num: str, com: str, phone: str = '', 
              from_addr: str = '', to_addr: str = '',
              resultv2: str = '4') -> Tuple[bool, Dict or str]:
        """
        查询快递物流信息
        
        Args:
            num: 快递单号
            com: 快递公司编码
            phone: 手机号（顺丰/中通必填，但API要求参数存在）
            from_addr: 出发地（参数可为空）
            to_addr: 目的地（参数可为空）
            resultv2: 功能扩展标识 0/1/4/8
        
        Returns:
            (成功状态, 结果数据或错误信息)
        """
        # 参数校验
        if not self.validate_tracking_number(num):
            return False, "❌ 快递单号格式不正确（应为6-32位字符）"
        
        if not com:
            return False, "❌ 请选择快递公司"
        
        # 锁单检测
        safe, warning = self.check_lockout(num)
        if not safe:
            return False, warning
        
        # 构建请求参数（所有参数都要有，值可以为空）
        payload = {
            'com': com,
            'num': num,
            'phone': phone or '',  # 可为空
            '_from': from_addr or '',  # 可为空
            'to': to_addr or '',  # 可为空
            'show': '0',
            'order': 'desc',
            'lang': 'zh',
            'resultv2': resultv2,
            'needCourierInfo': 'true'
        }
        
        try:
            response = requests.post(
                self.API_URL,
                data=payload,
                headers=self.DEFAULT_HEADERS,
                timeout=15
            )
            
            # 记录查询时间
            self.query_history[num] = datetime.now()
            
            if response.status_code == 200:
                result = response.json()
                msg_obj = result.get('message', {})
                
                if not msg_obj.get('success'):
                    code = msg_obj.get('code', -1)
                    msg_text = msg_obj.get('message', '未知错误')
                    
                    if "频繁" in msg_text:
                        retry_time = msg_obj.get('retry_after', 600)
                        return False, f"⚠️ 查询过于频繁，请 {retry_time//60} 分钟后再试。"
                    elif "锁单" in msg_text:
                        return False, "⚠️ 该单号被锁定，请至少等待 30 分钟后再次查询。"
                    elif "格式不正确" in msg_text:
                        return False, "❌ 快递单号格式不正确（应为6-32位字符）。"
                    else:
                        return False, f"❌ 查询失败：{msg_text}"
                
                data = msg_obj.get('data', {})
                if not data or data.get('status') != '200':
                    return False, "❌ 未查询到物流信息，请确认单号是否正确或包裹是否已发货。"
                
                return True, data
            else:
                return False, f"❌ HTTP 错误: {response.status_code}"
                
        except requests.exceptions.Timeout:
            return False, "❌ 请求超时，请稍后重试"
        except requests.exceptions.RequestException as e:
            return False, f"❌ 请求异常: {str(e)}"
    
    def format_result(self, data: Dict) -> str:
        """
        格式化查询结果为 Markdown
        
        Args:
            data: API返回的数据
        
        Returns:
            Markdown格式字符串
        """
        if not data:
            return "❌ 未获取到物流信息"
        
        nu = data.get('nu', '')
        com_code = data.get('com', '')
        state = data.get('state', '0')
        ischeck = data.get('ischeck', '0')
        tracks = data.get('data', [])
        
        # 获取状态信息
        state_info = self.STATE_MAP.get(state, ('未知', '未知状态'))
        
        lines = []
        lines.append("="*60)
        lines.append("📦 物流追踪结果")
        lines.append("="*60)
        lines.append(f"快递单号: {nu}")
        lines.append(f"快递公司: {self.get_company_name(com_code)}")
        lines.append(f"当前状态: {state_info[0]}")
        lines.append(f"是否签收: {'✅ 已签收' if ischeck == '1' else '❌ 未签收'}")
        
        # 时效预测
        if 'arrivalTime' in data:
            arrival = data['arrivalTime']
            lines.append(f"预计到达: {arrival.get('arrivalTime', '-')}")
        
        lines.append("")
        lines.append(f"🕐 最新物流轨迹 (共{len(tracks)}条)")
        lines.append("-"*60)
        
        # 物流轨迹
        if tracks:
            for i, track in enumerate(tracks[:10], 1):  # 最多显示10条
                time_str = track.get('ftime', track.get('time', ''))
                context = self.desensitize_text(track.get('context', ''))
                status = track.get('status', '')
                location = track.get('location', '')
                
                lines.append(f"\n{i}. [{time_str}] {status}")
                lines.append(f"   {context}")
                if location:
                    lines.append(f"   📍 {location}")
        
        lines.append("")
        lines.append("="*60)
        
        return "\n".join(lines)

# 便捷函数
def quick_query(company: str, num: str, phone: str = '', 
                from_addr: str = '', to_addr: str = '',
                resultv2: str = '4') -> str:
    """
    快速查询快递
    
    Args:
        company: 快递公司名称或编码
        num: 快递单号
        phone: 手机号（可选）
        from_addr: 出发地（可选）
        to_addr: 目的地（可选）
        resultv2: 功能扩展标识
    
    Returns:
        格式化后的查询结果
    """
    express = BJExpress()
    
    # 映射快递公司编码
    com_code = express.map_company_code(company)
    
    success, result = express.query(
        num=num,
        com=com_code,
        phone=phone,
        from_addr=from_addr,
        to_addr=to_addr,
        resultv2=resultv2
    )
    
    if success:
        return express.format_result(result)
    else:
        return result

if __name__ == '__main__':
    import argparse
    
    parser = argparse.ArgumentParser(description='必捷免费快递查询')
    parser.add_argument('company', help='快递公司（如：圆通、顺丰）')
    parser.add_argument('num', help='快递单号')
    parser.add_argument('--phone', default='', help='手机号（可选）')
    parser.add_argument('--from', dest='from_addr', default='', help='出发地（可选）')
    parser.add_argument('--to', dest='to_addr', default='', help='目的地（可选）')
    parser.add_argument('--resultv2', default='4', choices=['0', '1', '4', '8'], 
                        help='功能扩展：0=基础, 1=行政解析, 4=高级状态, 8=时效预测')
    
    args = parser.parse_args()
    
    result = quick_query(
        company=args.company,
        num=args.num,
        phone=args.phone,
        from_addr=args.from_addr,
        to_addr=args.to_addr,
        resultv2=args.resultv2
    )
    
    print(result)


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