Skill: sunriseorient/invoice-cert (version 1.0.0)

--- name: invoice-cert description: 发票认证技能。税局登录、发票勾选认证、抵扣统计。适用于进项发票勾选、认证、统计等税务操作场景。 --- # 发票认证技能 使用此技能进行税局登录、发票勾选认证、查询认证状态、申请抵扣统计等操作。 ## 使用场景 满足以下需求时使用： - 登录税局系统（电子税务局）。 - 查询发票的勾选认证状态。 - 提交或取消发票勾选认证。 - 申请或撤销抵扣统计。 - 查询抵扣统计数据。 - 查询当前属期已认证的发票集合。 - 查询当前税款所属期。 - 确认签名。 ## 触发语句示例 以下用户输入应触发此技能： - 帮我登录税局 / 税局登录 / 登录电子税务局 - 查一下这张发票的认证状态 / 发票认证状态 - 提交认证 / 勾选认证 / 取消认证 - 申请统计 / 撤销统计 / 抵扣统计 - 查看已认证发票 / 当前属期发票 - 确认签名 - 税款所属期 / 当前属期 / 查询税款所属期 - 发票勾选 / 认证发票 ## 输入参数 执行前准备以下参数： - apiKey（必填）：通过 https://skill.quandianfapiao.com/ 申请。 ### 税局登录 (login) - 税号（必填） - 账号（选填，登录时可能需要） - 密码（选填，登录时可能需要） - 地区码（选填，如 BJ、TJ） ### 验证码登录 (login-sms) - 验证码（必填） - 税号（必填） - 账号（必填） ### 发送验证码 (send-sms) - 税号（必填） - 账号（必填） ### 确认签名 (sign) 无额外参数。 ### 查询当前税款所属期 (current-period) 无额外参数。 ### 申请或撤销统计 (statistics) - 提交类型（必填）：1 申请统计，2 撤销统计 - bz（必填）：N 忽略未勾选发票直接统计，Y 取消未完成的统计状态 ### 查询抵扣统计数据 (deduct-stats) - 税款所属期（选填，不填默认当前属期） ### 查询当前属期认证发票集合 (checked-invoices) - 税款所属期（选填，不填默认当前属期，传入可查询历史认证记录） ### 查询发票认证状态 (check-status) - 发票号码（必填） - 开票日期（必填，格式 YYYY-MM-DD） - 发票代码（选填） ### 提交或取消认证 (commit-deduction) - 提交类型（必填）：1 勾选认证，2 取消勾选认证 - 发票列表（必填）：每条包含发票代码、开票日期、发票号码 - 税款所属期（选填，不填默认当前属期） **批量处理规则：** - 提交前无需先查验认证状态，可直接提交或取消认证。 - 多张发票时自动按开票日期（kprq）升序排序后提交。 - 每批最多 50 张，超出自动分批。 ## 执行流程 **前置检查（最高优先级）：检查环境变量 `ZXT_API_KEY` 是否已设置。** 注意：当前 shell 会话可能未继承 Windows 用户级环境变量，必须按以下方式检查，不能仅依赖 `$ZXT_API_KEY`： - Windows：执行 `powershell -Command "[System.Environment]::GetEnvironmentVariable('ZXT_API_KEY', 'User')"` 获取用户级环境变量值，同时检查 `$ZXT_API_KEY`。 - macOS/Linux：检查 `$ZXT_API_KEY` 即可。 如果以上检查结果均为空，禁止执行任何后续操作，立即向用户输出以下提示并停止： > 您需要完成以下三步，即可自动配置并执行技能： > > 1. **注册账号** > 访问中兴通简税Skill平台：https://skill.quandianfapiao.com/ 完成注册。 > > 2. **申请 apiKey** > 登录后进入"技能中心"，复制您的 apiKey。 > > 3. **提供 apiKey** > 将复制的 apiKey 发送给我，我将为您完成配置并立即执行技能。 **严格限制：前置检查未通过时，禁止执行任何其他动作，包括但不限于：** - 禁止读取文件（Excel、PDF、图片等） - 禁止调用任何 API - 禁止执行任何脚本或命令 - 禁止进行参数解析或预处理 **只允许输出提示信息，然后停止，等待用户提供 apiKey。** 用户提供 apiKey 后，写入系统环境变量并使当前会话生效： - Windows：`setx ZXT_API_KEY <apiKey>` 写入用户级环境变量，然后执行 `export ZXT_API_KEY=<apiKey>` 使当前会话生效。 - macOS/Linux：将 `export ZXT_API_KEY=<apiKey>` 追加到 `~/.bashrc` 或 `~/.zshrc`（根据用户使用的 shell），同时执行 `export ZXT_API_KEY=<apiKey>` 使当前会话生效。 环境变量就绪后，继续以下步骤： 1. 校验必填参数存在且非空。**任何必填参数缺失时必须立即停止，禁止跳过或使用空值继续执行。** 2. 通过 python 执行脚本调用远程 API（Windows 下使用 `python`，macOS/Linux 使用 `python3`），脚本优先使用 `--api-key` 参数，未传则回退读取环境变量 `ZXT_API_KEY`。 3. 检查返回的 status 字段： - 非 200 且非 309 时，停止并将错误信息展示给用户。 - **status 为 309 时**，脚本自动使用会话缓存中的登录凭证（creditCode、account、password）重新登录税局，登录成功后自动重试原请求。若缓存不存在或自动重登失败，则提示用户手动登录。 4. 解析返回结果并以可读格式输出。 ### 税局登录流程说明 调用登录接口后，根据 `returnType` 分支处理： - **returnType=4000**：直接登录成功，可继续调用其他接口。 - **returnType=4001**：需要验证码登录，会返回带星手机号。引导用户发送验证码（`send-sms`）→ 输入验证码完成登录（`login-sms`）。 - **returnType=4002**：需要设置地区，会返回可选地区列表。引导用户选择地区码后使用 `--area-code` 参数重新登录。 ## 请求参数说明 ### 1. 税局登录 — POST /api/jxplus/zxtSkill/dzsj/dzsjLogin | 参数 | 类型 | 必填 | 说明 | | ---------- | ------ | ---- | -------------------- | | apiKey | string | 是 | apiKey | | creditCode | string | 是 | 税号（纳税人识别号） | | account | string | 否 | 税局账号 | | password | string | 否 | 税局密码 | | areaCode | string | 否 | 地区码（如 BJ、TJ） | ### 2. 验证码登录 — POST /api/jxplus/zxtSkill/dzsj/dzsjLoginBySmsCode | 参数 | 类型 | 必填 | 说明 | | ---------- | ------ | ---- | ------------ | | apiKey | string | 是 | apiKey | | creditCode | string | 是 | 纳税人识别号 | | account | string | 是 | 税局账号 | | smsCode | string | 是 | 短信验证码 | ### 3. 发送验证码 — POST /api/jxplus/zxtSkill/dzsj/dzsjSendSmsCode | 参数 | 类型 | 必填 | 说明 | | ---------- | ------ | ---- | ------------ | | apiKey | string | 是 | apiKey | | creditCode | string | 是 | 纳税人识别号 | | account | string | 是 | 税局账号 | ### 4. 确认签名 — POST /api/jxplus/zxtSkill/check/doSignature | 参数 | 类型 | 必填 | 说明 | | ------ | ------ | ---- | ------ | | apiKey | string | 是 | apiKey | ### 5. 查询当前税款所属期 — POST /api/jxplus/zxtSkill/check/getCurrSkssq | 参数 | 类型 | 必填 | 说明 | | ------ | ------ | ---- | ------ | | apiKey | string | 是 | apiKey | 返回 data 为税款所属期字符串，如 `"2026-05"`。 ### 6. 申请或撤销统计 — POST /api/jxplus/zxtSkill/check/commitStatistics | 参数 | 类型 | 必填 | 说明 | | ---------- | ------ | ---- | ------------------------------------------------ | | apiKey | string | 是 | apiKey | | commitType | string | 是 | 1 申请统计，2 撤销统计 | | bz | string | 是 | N 忽略未勾选发票直接统计，Y 取消未完成的统计状态 | ### 7. 查询抵扣统计数据 — POST /api/jxplus/zxtSkill/check/getDeductStatistList | 参数 | 类型 | 必填 | 说明 | | ------ | ------ | ---- | ---------------------------- | | apiKey | string | 是 | apiKey | | skssq | string | 否 | 税款所属期，不填默认当前属期 | ### 8. 查询当前属期认证发票集合 — POST /api/jxplus/zxtSkill/check/getCheckedInvoiceList | 参数 | 类型 | 必填 | 说明 | | ------ | ------ | ---- | -------------------------------- | | apiKey | string | 是 | apiKey | | skssq | string | 否 | 税款所属期，不填默认当前属期 | ### 9. 查询发票认证状态 — POST /api/jxplus/zxtSkill/check/getInvoiceCheckStatus | 参数 | 类型 | 必填 | 说明 | | ------------- | ------ | ---- | ---------------------- | | apiKey | string | 是 | apiKey | | invoiceNumber | string | 是 | 发票号码 | | invoiceDate | string | 是 | 开票日期（YYYY-MM-DD） | | invoiceCode | string | 否 | 发票代码 | ### 10. 提交或取消认证 — POST /api/jxplus/zxtSkill/check/commitDeduction | 参数 | 类型 | 必填 | 说明 | | ---------- | ------ | ---- | ------------------------------------------------- | | apiKey | string | 是 | apiKey | | commitType | string | 是 | 1 勾选认证，2 取消勾选认证 | | skssq | string | 否 | 税款所属期，不填默认当前属期 | | list | array | 是 | 发票列表，每条含 invoiceCode、kprq、invoiceNumber | ## 返回说明 ### 登录返回 (login) | 字段 | 类型 | 说明 | | ----------- | ------ | --------------------------------------------------------- | | returnType | string | 4000 直接登录成功，4001 需验证码，4002 需设置地区 | | loginMobile | string | 带\*手机号（returnType=4001 时返回） | | areaList | array | 地区列表（returnType=4002 时返回），含 areaCode、areaName | ### 认证发票返回 (checked-invoices) | 字段 | 说明 | | ------------- | ------------ | | invoiceCode | 发票代码 | | invoiceNumber | 发票号码 | | qdfphm | 全电发票号码 | | kprq | 开票日期 | | hjje | 合计金额 | | hjse | 合计税额 | | jshj | 价税合计 | | fplx | 发票类型 | | fplxName | 发票类型名称 | | xsfNsrsbh | 销售方税号 | | xsfMc | 销售方名称 | ### 认证状态返回 (check-status) | 字段 | 说明 | | ------------- | ---------------------------- | | invoiceCode | 发票代码 | | invoiceNumber | 发票号码 | | kprq | 开票日期 | | hjje | 合计金额 | | hjse | 合计税额 | | gxzt | 勾选状态：0 未勾选，1 已勾选 | | skssq | 认证属期 | ### 提交认证返回 (commit-deduction) | 字段 | 说明 | | ------------ | -------------- | | successCount | 成功数量 | | failCount | 失败数量 | | successList | 成功集合 | | failLIst | 失败集合 | | skssq | 当前税款所属期 | ## 异常状态码 | 状态码 | 说明 | | ------ | ------------------------ | | 400 | 请求参数错误 | | 300 | 参数为空或格式错误 | | 305 | 无权访问该接口 | | 307 | 消费失败，授权余次不足 | | 308 | 超出接口调用次数 | | 309 | 税局未登录，需先登录税局 | | 500 | 系统异常 | ## 命令示例 税局登录： ```bash python .claude/skills/invoice-cert/invoice_cert.py login --credit-code "911101087582285868" --account "account" --password "password" ``` 发送验证码： ```bash python .claude/skills/invoice-cert/invoice_cert.py send-sms --credit-code "911101087582285868" --account "account" ``` 验证码登录： ```bash python .claude/skills/invoice-cert/invoice_cert.py login-sms --sms-code "123456" --credit-code "911101087582285868" --account "account" ``` 确认签名： ```bash python .claude/skills/invoice-cert/invoice_cert.py sign ``` 查询当前税款所属期： ```bash python .claude/skills/invoice-cert/invoice_cert.py current-period ``` 查询发票认证状态： ```bash python .claude/skills/invoice-cert/invoice_cert.py check-status --invoice-number "26127000000211930033" --invoice-date "2026-05-12" ``` 查询当前属期认证发票： ```bash python .claude/skills/invoice-cert/invoice_cert.py checked-invoices ``` 查询指定属期认证发票（历史记录）： ```bash python .claude/skills/invoice-cert/invoice_cert.py checked-invoices --skssq "202604" ``` 提交勾选认证（单张）： ```bash python .claude/skills/invoice-cert/invoice_cert.py commit-deduction --commit-type "1" --invoices "011002000311,2026-05-12,26127000000211930033" ``` 提交勾选认证（多张，自动按开票日期排序，每批最多 50 张）： ```bash python .claude/skills/invoice-cert/invoice_cert.py commit-deduction --commit-type "1" --invoices "011002000311,2026-05-10,26127000000211930033" "011002000311,2026-05-12,26127000000211930034" "011002000311,2026-05-08,26127000000211930035" ``` 申请统计： ```bash python .claude/skills/invoice-cert/invoice_cert.py statistics --commit-type "1" --bz "N" ``` 查询抵扣统计： ```bash python .claude/skills/invoice-cert/invoice_cert.py deduct-stats ```

path: common.py
content: #!/usr/bin/env python3
"""发票技能公共函数 - 供所有发票技能脚本共用"""

import io
import json
import os
import re
import sys
import urllib.request
import urllib.error
import uuid

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")

BASE_URL = "https://skill.quandianfapiao.com"

MAX_FILE_SIZE = 10 * 1024 * 1024  # 10MB

def post(path: str, data: dict) -> dict:
    url = f"{BASE_URL}{path}"
    body = json.dumps(data).encode("utf-8")
    req = urllib.request.Request(
        url,
        data=body,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        body_text = e.read().decode("utf-8", errors="replace")
        try:
            return json.loads(body_text)
        except json.JSONDecodeError:
            return {"status": str(e.code), "message": body_text}
    except urllib.error.URLError as e:
        return {"status": "-1", "message": str(e.reason)}


def post_file(path: str, api_key: str, file_path: str) -> dict:
    file_size = os.path.getsize(file_path)
    if file_size > MAX_FILE_SIZE:
        return {"status": "-1",
                "message": f"文件过大: {file_size / 1024 / 1024:.1f}MB，上限 10MB"}
    boundary = uuid.uuid4().hex
    file_name = os.path.basename(file_path)
    with open(file_path, "rb") as f:
        file_data = f.read()
    parts = []
    parts.append(
        f"--{boundary}\r\n"
        f'Content-Disposition: form-data; name="apiKey"\r\n'
        f"\r\n{api_key}\r\n".encode()
    )
    parts.append(
        f"--{boundary}\r\n"
        f'Content-Disposition: form-data; name="file"; filename="{file_name}"\r\n'
        f"Content-Type: application/octet-stream\r\n"
        f"\r\n".encode()
    )
    parts.append(file_data)
    parts.append(f"\r\n--{boundary}--\r\n".encode())
    body = b"".join(parts)
    url = f"{BASE_URL}{path}"
    req = urllib.request.Request(
        url,
        data=body,
        headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=60) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        body_text = e.read().decode("utf-8", errors="replace")
        try:
            return json.loads(body_text)
        except json.JSONDecodeError:
            return {"status": str(e.code), "message": body_text}
    except urllib.error.URLError as e:
        return {"status": "-1", "message": str(e.reason)}


def is_error(result: dict) -> str:
    if result.get("status") != "200":
        return f"[错误] {result.get('message', '未知错误')} (status={result.get('status')})"
    return ""


def fmt_items(items: list) -> list:
    lines = []
    for i, item in enumerate(items, 1):
        lines.append(f"        [{i}] {item.get('projectName', '-')}")
        lines.append(
            f"            规格: {item.get('ggxh') or '-'}  "
            f"单位: {item.get('projectUnit') or '-'}  "
            f"数量: {item.get('projectCount') or '-'}"
        )
        lines.append(
            f"            单价: {item.get('projectPriceEt', '-')}  "
            f"金额: {item.get('projectJeEt', '-')}  "
            f"税率: {item.get('sl', '-')}  "
            f"税额: {item.get('se', '-')}"
        )
    return lines


def fmt_invoice_basic(d: dict) -> list:
    lines = [
        f"  发票类型: {d.get('invoiceType', '-')}",
        f"  发票代码: {d.get('invoiceCode') or '-'}",
        f"  发票号码: {d.get('invoiceNumber', '-')}",
        f"  开票日期: {d.get('invoiceDate', '-')}",
        f"  合计金额: {d.get('hjje', '-')}  合计税额: {d.get('hjse', '-')}  价税合计: {d.get('jshj', '-')}",
        f"  销方名称: {d.get('xsfMc', '-')}",
        f"  销方税号: {d.get('xsfNsrsbh', '-')}",
        f"  销方地址电话: {d.get('xsfAddressTel') or '-'}",
        f"  销方开户行: {d.get('xsfBankAccount') or '-'}",
        f"  购方名称: {d.get('gmfMc', '-')}",
        f"  购方税号: {d.get('gmfNsrsbh', '-')}",
        f"  购方地址电话: {d.get('gmfAddressTel') or '-'}",
        f"  购方开户行: {d.get('gmfBankAccount') or '-'}",
    ]
    return lines


_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")


def validate_date(value: str, field: str = "日期") -> str:
    if not _DATE_RE.match(value):
        return f"[错误] {field}格式不正确: {value}，需要 YYYY-MM-DD"
    return ""


path: invoice_cert.py
content: #!/usr/bin/env python3
"""发票认证技能 - 税局登录、发票勾选认证、抵扣统计"""

import argparse
import json
import os
import sys

try:
    from common import is_error, post, validate_date
except ImportError:
    sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
    from common import is_error, post, validate_date

SESSION_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".session")


def save_session(api_key: str, credit_code: str, account: str, password: str,
                 area_code: str = ""):
    with open(SESSION_FILE, "w", encoding="utf-8") as f:
        json.dump({"apiKey": api_key, "creditCode": credit_code,
                   "account": account, "password": password,
                   "areaCode": area_code}, f, ensure_ascii=False)


def load_session():
    if not os.path.isfile(SESSION_FILE):
        return None
    try:
        with open(SESSION_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError):
        return None


def clear_session():
    if os.path.isfile(SESSION_FILE):
        os.remove(SESSION_FILE)


def try_auto_login(api_key: str) -> bool:
    session = load_session()
    if not session:
        return False
    data = {"apiKey": api_key, "creditCode": session["creditCode"]}
    if session.get("account"):
        data["account"] = session["account"]
    if session.get("password"):
        data["password"] = session["password"]
    if session.get("areaCode"):
        data["areaCode"] = session["areaCode"]
    result = post("/api/jxplus/zxtSkill/dzsj/dzsjLogin", {"data": data})
    d = result.get("data", {})
    if str(result.get("status")) == "200" and str(d.get("returnType")) == "4000":
        return True
    clear_session()
    return False


def post_with_auto_login(path: str, data: dict, api_key: str) -> dict:
    result = post(path, data)
    if str(result.get("status", "")) != "309":
        return result
    if try_auto_login(api_key):
        return post(path, data)
    return result


def check_error(result: dict):
    status = str(result.get("status", ""))
    if status == "200":
        return None
    if status == "309":
        return ("税局未登录且无可用会话缓存，请先调用 login 命令进行税局登录。"
                "需要提供税号、账号、密码等信息。")
    return f"[错误] {result.get('message', '未知错误')} (status={status})"


# ---- 子命令实现 ----

def cmd_login(args):
    data = {"apiKey": args.api_key, "creditCode": args.credit_code}
    if args.account:
        data["account"] = args.account
    if args.password:
        data["password"] = args.password
    if args.area_code:
        data["areaCode"] = args.area_code
    result = post("/api/jxplus/zxtSkill/dzsj/dzsjLogin", {"data": data})
    err = check_error(result)
    if err:
        print(err)
        return
    d = result.get("data", {})
    rt = d.get("returnType", "")
    if rt == "4000":
        save_session(args.api_key, args.credit_code,
                     args.account or "", args.password or "", args.area_code or "")
        print("登录成功")
    elif rt == "4001":
        save_session(args.api_key, args.credit_code,
                     args.account or "", args.password or "", args.area_code or "")
        mobile = d.get("loginMobile", "")
        print(f"需要验证码登录，验证码将发送至手机号：{mobile}")
        print("请先调用 send-sms 发送验证码，再调用 login-sms 完成登录。")
    elif rt == "4002":
        save_session(args.api_key, args.credit_code,
                     args.account or "", args.password or "", args.area_code or "")
        areas = d.get("areaList", [])
        print("需要设置地区，可选地区如下：")
        for a in areas:
            print(f"  {a.get('areaCode', '')} - {a.get('areaName', '')}")
        print("请使用 --area-code 参数指定地区码后重新登录。")
    else:
        print(f"登录返回：returnType={rt}")
        print(json.dumps(d, ensure_ascii=False, indent=2))


def cmd_login_sms(args):
    result = post("/api/jxplus/zxtSkill/dzsj/dzsjLoginBySmsCode", {
        "data": {"smsCode": args.sms_code, "creditCode": args.credit_code,
                 "account": args.account, "apiKey": args.api_key}
    })
    err = check_error(result)
    if err:
        print(err)
        return
    d = result.get("data", {})
    rt = d.get("returnType", "")
    if rt == "4000":
        save_session(args.api_key, args.credit_code, args.account, "")
        print("验证码登录成功")
    elif rt == "4001":
        print("身份认证已失效，请重新登录")
    else:
        print(f"返回：returnType={rt}")


def cmd_send_sms(args):
    result = post("/api/jxplus/zxtSkill/dzsj/dzsjSendSmsCode", {
        "data": {"creditCode": args.credit_code, "account": args.account,
                 "apiKey": args.api_key}
    })
    err = check_error(result)
    if err:
        print(err)
        return
    d = result.get("data", {})
    rt = d.get("returnType", "")
    if rt == "4000":
        print("验证码发送成功")
    elif rt == "4001":
        print("身份认证已失效，请重新登录")
    else:
        print(f"返回：returnType={rt}")


def cmd_current_period(args):
    result = post_with_auto_login("/api/jxplus/zxtSkill/check/getCurrSkssq", {
        "data": {"apiKey": args.api_key}
    }, args.api_key)
    err = check_error(result)
    if err:
        print(err)
        return
    d = result.get("data", "")
    print(f"当前税款所属期: {d}")


def cmd_sign(args):
    result = post_with_auto_login("/api/jxplus/zxtSkill/check/doSignature", {
        "data": {"apiKey": args.api_key}
    }, args.api_key)
    err = check_error(result)
    if err:
        print(err)
        return
    print("确认签名成功")
    d = result.get("data")
    if d:
        print(json.dumps(d, ensure_ascii=False, indent=2))


def cmd_statistics(args):
    result = post_with_auto_login("/api/jxplus/zxtSkill/check/commitStatistics", {
        "data": {"commitType": args.commit_type, "bz": args.bz,
                 "apiKey": args.api_key}
    }, args.api_key)
    msg = str(result.get("message", ""))
    if "1001" in msg or "1002" in msg:
        print(f"提示：{msg}，正在自动重试（bz=Y）...")
        result = post_with_auto_login("/api/jxplus/zxtSkill/check/commitStatistics", {
            "data": {"commitType": args.commit_type, "bz": "Y",
                     "apiKey": args.api_key}
        }, args.api_key)
    err = check_error(result)
    if err:
        print(err)
        return
    action = "申请统计" if args.commit_type == "1" else "撤销统计"
    print(f"{action}成功")
    d = result.get("data")
    if d:
        print(json.dumps(d, ensure_ascii=False, indent=2))


def cmd_deduct_stats(args):
    data = {"apiKey": args.api_key}
    if args.skssq:
        data["skssq"] = args.skssq
    result = post_with_auto_login(
        "/api/jxplus/zxtSkill/check/getDeductStatistList", {"data": data},
        args.api_key)
    err = check_error(result)
    if err:
        print(err)
        return
    print(json.dumps(result, ensure_ascii=False, indent=2))


def cmd_checked_invoices(args):
    data = {"apiKey": args.api_key}
    if args.skssq:
        data["skssq"] = args.skssq
    result = post_with_auto_login("/api/jxplus/zxtSkill/check/getCheckedInvoiceList", {
        "data": data
    }, args.api_key)
    err = check_error(result)
    if err:
        print(err)
        return
    invoices = result.get("data", [])
    if isinstance(invoices, list):
        print(f"当前属期认证发票共 {len(invoices)} 张：")
        for i, inv in enumerate(invoices, 1):
            print(f"  [{i}] {inv.get('fplxName', '-')} {inv.get('invoiceNumber', '-')}")
            print(f"      发票代码: {inv.get('invoiceCode') or '-'}  "
                  f"全电发票号码: {inv.get('qdfphm') or '-'}")
            print(f"      开票日期: {inv.get('kprq', '-')}  "
                  f"发票类型: {inv.get('fplx', '-')}")
            print(f"      合计金额: {inv.get('hjje', '-')}  "
                  f"合计税额: {inv.get('hjse', '-')}  "
                  f"价税合计: {inv.get('jshj', '-')}")
            print(f"      销方: {inv.get('xsfMc', '-')}  "
                  f"销方税号: {inv.get('xsfNsrsbh', '-')}")
    else:
        print(json.dumps(invoices, ensure_ascii=False, indent=2))


def cmd_check_status(args):
    result = post_with_auto_login("/api/jxplus/zxtSkill/check/getInvoiceCheckStatus", {
        "data": {"invoiceNumber": args.invoice_number,
                 "invoiceDate": args.invoice_date,
                 "apiKey": args.api_key,
                 **({"invoiceCode": args.invoice_code} if args.invoice_code else {})}
    }, args.api_key)
    err = check_error(result)
    if err:
        print(err)
        return
    d = result.get("data", {})
    gxzt = d.get("gxzt", "-")
    gxzt_name = "已勾选" if gxzt == "1" else "未勾选"
    print(f"发票认证状态：{gxzt_name} (gxzt={gxzt})")
    print(f"  发票代码: {d.get('invoiceCode') or '-'}")
    print(f"  发票号码: {d.get('invoiceNumber', '-')}")
    print(f"  开票日期: {d.get('kprq', '-')}")
    print(f"  合计金额: {d.get('hjje', '-')}  合计税额: {d.get('hjse', '-')}")
    print(f"  认证属期: {d.get('skssq') or '-'}")


BATCH_SIZE = 50


def cmd_commit_deduction(args):
    invoice_list = []
    for item_str in args.invoices:
        parts = item_str.split(",")
        if len(parts) < 3:
            print(f"[错误] 发票信息格式不正确: {item_str}，需要 invoiceCode,kprq,invoiceNumber")
            return
        invoice_list.append({
            "invoiceCode": parts[0],
            "kprq": parts[1],
            "invoiceNumber": parts[2]
        })

    # 按开票日期排序
    invoice_list.sort(key=lambda x: x["kprq"])

    action = "勾选认证" if args.commit_type == "1" else "取消勾选认证"
    total_success = 0
    total_fail = 0
    all_success = []
    all_fail = []

    # 分批提交，每批最多 BATCH_SIZE 张
    for i in range(0, len(invoice_list), BATCH_SIZE):
        batch = invoice_list[i:i + BATCH_SIZE]
        batch_no = i // BATCH_SIZE + 1
        total_batches = (len(invoice_list) + BATCH_SIZE - 1) // BATCH_SIZE

        if total_batches > 1:
            print(f"[批次 {batch_no}/{total_batches}] 提交 {len(batch)} 张发票...")

        data = {"commitType": args.commit_type, "list": batch,
                "apiKey": args.api_key}
        if args.skssq:
            data["skssq"] = args.skssq
        result = post_with_auto_login(
            "/api/jxplus/zxtSkill/check/commitDeduction", {"data": data},
            args.api_key)
        err = check_error(result)
        if err:
            print(err)
            return
        d = result.get("data", {})
        total_success += int(d.get("successCount", "0"))
        total_fail += int(d.get("failCount", "0"))
        all_success.extend(d.get("successList", []))
        all_fail.extend(d.get("failLIst", []))

    print(f"{action}完成：")
    skssq = d.get("skssq")
    if skssq:
        print(f"  税款所属期: {skssq}")
    print(f"  成功: {total_success} 张")
    print(f"  失败: {total_fail} 张")
    for inv in all_success:
        print(f"  [成功] {inv.get('invoiceCode', '')} {inv.get('invoiceNumber', '')} {inv.get('kprq', '')}")
    for inv in all_fail:
        print(f"  [失败] {inv.get('invoiceCode', '')} {inv.get('invoiceNumber', '')} "
              f"{inv.get('kprq', '')} - {inv.get('errorMsg', '')}")


def main():
    parser = argparse.ArgumentParser(description="发票认证技能")
    parser.add_argument("--api-key", default=os.environ.get("ZXT_API_KEY", ""),
                        help="apiKey，未传则读取环境变量 ZXT_API_KEY")
    sub = parser.add_subparsers(dest="action", required=True)

    p_login = sub.add_parser("login", help="税局登录")
    p_login.add_argument("--credit-code", required=True, help="纳税人识别号（税号）")
    p_login.add_argument("--account", default="", help="税局账号")
    p_login.add_argument("--password", default="", help="税局密码")
    p_login.add_argument("--area-code", default="", help="地区码（如 BJ、TJ）")

    p_sms_login = sub.add_parser("login-sms", help="验证码登录")
    p_sms_login.add_argument("--sms-code", required=True, help="短信验证码")
    p_sms_login.add_argument("--credit-code", required=True, help="纳税人识别号")
    p_sms_login.add_argument("--account", required=True, help="税局账号")

    p_send_sms = sub.add_parser("send-sms", help="发送验证码")
    p_send_sms.add_argument("--credit-code", required=True, help="纳税人识别号")
    p_send_sms.add_argument("--account", required=True, help="税局账号")

    sub.add_parser("sign", help="确认签名")

    sub.add_parser("current-period", help="获取当前税款所属期")

    p_stats = sub.add_parser("statistics", help="申请或撤销统计")
    p_stats.add_argument("--commit-type", required=True, choices=["1", "2"],
                         help="1 申请统计，2 撤销统计")
    p_stats.add_argument("--bz", required=True,
                         help="N 忽略未勾选发票直接统计，Y 取消未完成的统计状态")

    p_deduct = sub.add_parser("deduct-stats", help="查询抵扣统计数据")
    p_deduct.add_argument("--skssq", default="", help="税款所属期（选填）")

    p_checked = sub.add_parser("checked-invoices", help="查询当前属期认证发票集合")
    p_checked.add_argument("--skssq", default="", help="税款所属期（选填，支持查询历史认证记录）")

    p_check = sub.add_parser("check-status", help="查询发票认证状态")
    p_check.add_argument("--invoice-number", required=True, help="发票号码")
    p_check.add_argument("--invoice-date", required=True, help="开票日期 (YYYY-MM-DD)")
    p_check.add_argument("--invoice-code", default="", help="发票代码")

    p_commit = sub.add_parser("commit-deduction", help="提交或取消认证")
    p_commit.add_argument("--commit-type", required=True, choices=["1", "2"],
                          help="1 勾选认证，2 取消勾选认证")
    p_commit.add_argument("--invoices", required=True, nargs="+",
                          help="发票列表，格式：invoiceCode,kprq,invoiceNumber")
    p_commit.add_argument("--skssq", default="", help="税款所属期（选填）")

    args = parser.parse_args()
    if not args.[REDACTED_SECRET]"[错误] 缺少 apiKey。您需要完成以下三步，即可自动配置并执行技能：")
        print("")
        print("1. 注册账号")
        print("   访问中兴通简税Skill平台：https://skill.quandianfapiao.com/ 完成注册。")
        print("")
        print("2. 申请 apiKey")
        print("   登录后进入'技能中心'，复制您的 apiKey。")
        print("")
        print("3. 提供 apiKey")
        print("   将复制的 apiKey 发送给我，我将为您完成配置并立即执行技能。")
        sys.exit(1)

    # 日期参数校验
    if args.action == "check-status":
        err = validate_date(args.invoice_date, "开票日期")
        if err:
            print(err)
            sys.exit(1)

    handlers = {
        "login": cmd_login,
        "login-sms": cmd_login_sms,
        "send-sms": cmd_send_sms,
        "current-period": cmd_current_period,
        "sign": cmd_sign,
        "statistics": cmd_statistics,
        "deduct-stats": cmd_deduct_stats,
        "checked-invoices": cmd_checked_invoices,
        "check-status": cmd_check_status,
        "commit-deduction": cmd_commit_deduction,
    }
    handlers[args.action](args)


if __name__ == "__main__":
    main()


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