Skill: sunriseorient/invoice-ocr-pro (version 1.0.0)

--- name: invoice-ocr description: 发票识别技能。上传发票文件进行 OCR 识别，返回发票票面信息。适用于从发票图片或文件中提取结构化数据的场景。 --- # 发票识别技能 使用此技能通过上传发票文件进行 OCR 识别，自动提取发票票面结构化信息。 ## 使用场景 满足以下需求时使用： - 上传发票图片或文件（PDF/OFD/JPEG/JPG/PNG/XML）进行 OCR 识别。 - 从发票图片中提取发票号码、金额、销购方等结构化信息。 - 批量处理发票文件的自动识别。 ## 触发语句示例 以下用户输入应触发此技能： - 帮我识别这张发票 / 发票识别 / 识别发票 - 这张发票里有什么信息 - 提取发票信息 / 发票 OCR - 读取发票内容 - 这个发票文件帮我识别一下 - 识别发票文件 ## 输入参数 执行前准备以下参数： - apiKey（必填）：通过 https://skill.quandianfapiao.com/ 申请。 ### 发票识别 (ocr) - 发票文件路径（必填，支持 PDF、OFD、JPEG、JPG、PNG、XML） ## 执行流程 **前置检查（最高优先级）：检查环境变量 `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 时停止并将错误信息展示给用户，禁止重试或忽略。 4. 解析返回结果并以可读格式输出。 ## 请求参数说明 ### 发票识别 — POST /api/jxplus/zxtSkill/discern/invoiceDiscern 请求头：`Content-Type: multipart/form-data` | 参数 | 类型 | 必填 | 说明 | | ------ | ------ | ---- | -------------------------------- | | apiKey | string | 是 | apiKey | | file | file | 是 | 发票文件（pdf/ofd/jpeg/jpg/png/xml） | ## 返回说明 ### 外层返回字段 | 字段 | 类型 | 说明 | | ------- | ------ | -------------------- | | status | string | 返回接口状态码 | | message | string | 返回接口状态描述信息 | | data | object | 业务数据体 | ### data 业务数据体 | 字段 | 类型 | 说明 | | -------------- | ------ | ------------------ | | invoiceType | string | 发票种类 | | invoiceCode | string | 发票代码 | | invoiceNumber | string | 发票号码 | | invoiceDate | string | 开票日期 | | hjje | string | 合计金额 | | hjse | string | 合计税额 | | jshj | string | 价税合计 | | xsfMc | string | 销售方名称 | | xsfNsrsbh | string | 销售方纳税人识别号 | | xsfAddressTel | string | 销售方地址、电话 | | xsfBankAccount | string | 销售方开户行及账号 | | gmfMc | string | 购买方名称 | | gmfNsrsbh | string | 购买方税号 | | gmfAddressTel | string | 购买方地址、电话 | | gmfBankAccount | string | 购买方开户行及账号 | | payee | string | 收款人 | | reviewer | string | 复核人 | | invoicer | string | 开票人 | | machineNo | string | 机器编号 | | checkCode | string | 校验码 | | passwordArea | string | 密码区 | | remark | string | 备注 | | item | List | 发票明细列表 | | otherInfo | string | 其他信息 | ### item 明细数据体 | 字段 | 类型 | 说明 | | -------------- | ------ | -------- | | projectName | string | 项目名称 | | ggxh | string | 规格型号 | | projectUnit | string | 单位 | | projectCount | string | 数量 | | projectPriceEt | string | 单价 | | projectJeEt | string | 金额 | | sl | string | 税率 | | se | string | 税额 | ## 异常状态码 | 状态码 | 说明 | | ------ | ---------------------- | | 400 | 请求参数错误 | | 300 | 参数为空或格式错误 | | 305 | 无权访问该接口 | | 307 | 消费失败，授权余次不足 | | 308 | 超出接口调用次数 | | 500 | 系统异常 | ## 接口请求示例 ### 发票识别 — POST /api/jxplus/zxtSkill/discern/invoiceDiscern 请求方式：`multipart/form-data`，包含 `apiKey` 字段和 `file` 文件字段。 成功返回： ```json { "status": "200", "message": "成功", "data": { "invoiceType": "82", "invoiceCode": "数电票", "invoiceNumber": "23622000000012981281", "invoiceDate": "2023-10-18", "hjje": "34.65", "hjse": "0.35", "jshj": "35.00", "xsfMc": "金昌市安行征途汽车运输有限公司", "xsfNsrsbh": "91620302MA71XY3022", "xsfAddressTel": "", "xsfBankAccount": "", "gmfMc": "北京中兴通融资产管理股份有限公司", "gmfNsrsbh": "911101087582285868", "gmfAddressTel": "", "gmfBankAccount": "", "payee": null, "reviewer": null, "invoicer": "张玉芳", "machineNo": null, "checkCode": "23622000000012981281", "passwordArea": null, "remark": "车人1 00****00 2023-10-16 武威- 金川机场- 长途汽车\n价税合计（大写） 叁拾伍圆整 （小写）¥35.00\n线路：武威-金川机场日期：2023-10-1608:30\n", "otherInfo": null, "item": [ { "projectName": "*运输服务*客运服务", "ggxh": null, "projectUnit": null, "projectCount": "34.65", "projectPriceEt": "1", "projectJeEt": "34.65", "sl": "1%", "se": "0.35" } ] } } ``` 异常返回： ```json { "status": "400", "message": "文件不能为空", "data": "" } ``` ## 命令示例 发票识别： ```bash python .claude/skills/invoice-ocr/invoice_ocr.py ocr --file "E:\\invoices\\test.pdf" ```

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_ocr.py
content: #!/usr/bin/env python3
"""发票识别技能 - 通过上传发票文件进行 OCR 识别"""

import argparse
import os
import sys

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


def ocr(api_key: str, file_path: str) -> dict:
    if not os.path.isfile(file_path):
        return {"status": "-1", "message": f"文件不存在: {file_path}"}
    ext = os.path.splitext(file_path)[1].lower()
    if ext not in (".pdf", ".ofd", ".jpeg", ".jpg", ".png", ".xml"):
        return {"status": "-1", "message": f"不支持的文件类型: {ext}，仅支持 pdf/ofd/jpeg/jpg/png/xml"}
    return post_file("/api/jxplus/zxtSkill/discern/invoiceDiscern", api_key, file_path)


def fmt_result(result: dict) -> str:
    err = is_error(result)
    if err:
        return err
    d = result.get("data", {})
    lines = ["识别结果:"]
    lines.extend(fmt_invoice_basic(d))
    lines.extend([
        f"  收款人: {d.get('payee') or '-'}",
        f"  复核人: {d.get('reviewer') or '-'}",
        f"  开票人: {d.get('invoicer') or '-'}",
        f"  机器编号: {d.get('machineNo') or '-'}",
        f"  校验码: {d.get('checkCode') or '-'}",
        f"  密码区: {d.get('passwordArea') or '-'}",
        f"  备注: {d.get('remark') or '-'}",
    ])
    items = d.get("item") or d.get("items")
    if items:
        lines.append("  明细项目:")
        lines.extend(fmt_items(items))
    return "\n".join(lines)


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_ocr = sub.add_parser("ocr", help="上传发票文件进行识别")
    p_ocr.add_argument("--file", required=True, help="发票文件路径 (pdf/ofd/jpeg/jpg/png/xml)")

    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 == "ocr":
        result = ocr(args.api_key, args.file)
        print(fmt_result(result))


if __name__ == "__main__":
    main()


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