Skill: scnet-sugon/expense-invoice-ocr (version 1.0.5)

--- name: expense_invoice_ocr description: 支持识别企业财务报销场景的常见票据，包括增值税发票、增值税卷票、出租车发票、火车票、航空电子客票行程单、机动车销售统一发票、定额发票、过路过桥费发票、医疗发票、税收完税证明、船票、非税票据、通用机打发票、汽车票识别。 version: 1.0.4 author: SCNet license: MIT tags: - OCR - 证件识别 - 发票识别 - 文字提取 required_env_vars: - SCNET_API_KEY optional_env_vars: - SCNET_API_BASE primary_credential: SCNET_API_KEY dependencies: - python3 - requests input: - ocrType : 识别类型，可选值见下文 - filePath : 待识别图片的本地路径 output: 结构化的 JSON 数据，包含识别结果和置信度 --- # Sugon-Scnet 通用 OCR 技能 本技能封装了 Sugon-Scnet 企业财务报销 OCR 服务，通过单一接口即可调用 14 种识别能力，高效提取文字及票据信息。 ## 功能特性 - **财务票据**：覆盖增值税发票、增值税卷票、出租车发票、火车票、航空电子客票行程单、机动车销售统一发票、定额发票、过路过桥费发票、医疗发票、税收完税证明、船票、非税票据、通用机打发票、汽车票识别，自动提取关键字段。 ## 前置配置 > **⚠️ 重要**：使用前需要申请 Scnet API Token ### 申请 API Token 1. 访问 [Scnet 官网](https://www.scnet.cn) 注册/登录 2. 在控制台申请 API 密钥（格式：[REDACTED_SECRET]） 3. 复制密钥备用 ### 配置 Token **手动配置（推荐）** 1. 在技能目录下创建 `config/.env` 文件，内容如下： ```ini # ===== Sugon-Scnet OCR API 配置 ===== # 申请地址：https://www.scnet.cn SCNET_API_KEY=your_scnet_api_key_here # API 基础地址（一般无需修改） SCNET_API_BASE=https://api.scnet.cn/api/llm/v1 ``` 2. 添加：`SCNET_API_KEY=你的密钥` 3. 设置文件权限为 600（仅所有者可读写） **⚠️ 安全警告**：切勿将 API Key 直接粘贴到聊天对话中，否则可能被记录或泄露。 ### Token 更新 Token 过期后调用会返回 401 或 403 错误。更新方法：重新申请 Token 并替换 config/.env 中的 SCNET_API_KEY。 ### 依赖安装 本技能需要 Python 3.6+ 和 requests 库。请运行以下命令： ```bash pip install requests ``` --- ### 使用方法 ### 参数说明 | 参数名 | 类型 | 必填 | 描述 | |--------|------|------|------| | ocrType | string | 是 | 识别类型枚举。必须为以下之一：<br>• VAT_INVOICE（增值税发票）<br>• VAT_ROLL_INVOICE（增值税卷票）<br>• TAXI_INVOICE（出租车发票）<br>• TRAIN_TICKET（火车票）<br>• AIRPORT_TICKET（航空运输电子客票行程单）<br>• VEHICLE_SALE_INVOICE（机动车销售统一发票）<br>• QUOTA_INVOICE（定额发票）<br>• TOLL_INVOICE（过路过桥费发票）<br>• MEDICAL_INVOICE（医疗发票）<br>• TAX_CERTIFICATE（税收完税证明）<br>• NON_TAX_BILL（非税票据）<br>• GENERAL_MACHINE_INVOICE（通用机打发票）<br>• SHIP_TICKET（船票）<br>• BUS_TICKET（汽车票） | | filePath | string | 是 | 待识别图片的本地绝对路径。支持 jpg、png、pdf 等常见格式。 | ### 命令行调用示例 ```bash python .claude/skills/expense_invoice_ocr/scripts/main.py VAT_INVOICE /path/to/invoice.jpg ``` ### 在 AI 对话中使用 用户可以说： - “提取这张发票的信息，路径是 /Users/name/Downloads/fapiao.png” AI 会根据 description 中的关键词自动触发本技能。 ### AI 调用建议 为避免触发 API 速率限制（10 QPS），请串行调用本技能，即等待前一个识别完成后再发起下一个请求。 如果使用 OpenClaw 的 exec 工具，建议设置 timeout 或 yieldMs 参数，让命令同步执行，避免多个命令同时运行导致并发。 ### 配置选项 编辑 `config/.env` 文件： | 变量名 | 默认值 | 说明 | |--------|--------|------| | SCNET_API_KEY | 必需 | Scnet API 密钥 | | SCNET_API_BASE | https://api.scnet.cn/api/llm/v1 | API 基础地址（一般无需修改） | ### 输出 - 标准输出：识别结果的 JSON 数据，结构与 API 文档一致，位于 `data` 字段内。 - 识别结果位于 data[0].result[0].elements 中，具体字段取决于 ocrType。 - 错误信息：如果发生错误，会输出以 `错误:` 开头的友好提示。 ### 注意事项 - 本技能调用的 OCR API 有 10 QPS 的速率限制。 - 如果遇到 429 错误，请等待 2-3 秒后重试，不要连续发起请求。 - 建议在调用前确保图片已准备就绪，避免因网络问题导致重复调用。 ### 故障排除 | 问题 | 解决方案 | |------|----------| | 配置文件不存在 | 创建 config/.env 并填入 Token（参考前置配置） | | API Key 无效/过期 | 重新申请 Token 并更新 `.env` 文件 | | 文件不存在 | 检查提供的文件路径是否正确 | | 网络连接失败 | 检查网络连接或防火墙设置 | | 不支持的文件类型 | 确保文件扩展名为允许的类型（参考 API 文档） | | 401/403/Unauthorized | Token 无效或过期，重新申请并配置 | | 429 Too Many Requests | 请求过于频繁，技能会自动等待并重试（最多 3 次）。若持续失败，请降低调用频率或联系服务方提高限额。 |

path: assets/templates/fields-summary.md
content: # 各识别类型的字段说明（elements 内容）

根据 ocrType 不同，返回的 `elements` 对象包含以下字段：

## VAT_INVOICE (增值税发票)
- `title`: 发票名称
- `invoiceCode`: 发票代码
- `invoiceNo`: 发票号码
- `printedCode`: 机打代码
- `printedNo`: 机打号码
- `checkCode`: 校验码
- `machineCode`: 机器编号
- `invoiceDate`: 开票日期
- `passwordArea`: 密码区
- `buyerName`: 购方名称
- `buyerCode`: 购方纳税人识别号
- `buyerAddressAndPhone`: 购方地址及电话
- `buyerBankAndAccount`: 购方开户行及账号
- `sellerName`: 销售方名称
- `sellerCode`: 销售方纳税人识别号
- `sellerAddressAndPhone`: 销售方地址及电话
- `sellerBankAndAccount`: 销售方开户行及账号
- `preTaxTotalAmount`: 税前合计金额
- `totalTaxAmount`: 合计税额
- `totalAmountUpper`: 价税合计(大写)
- `totalAmountLower`: 价税合计(小写)
- `invoiceForm`: 联次
- `remarks`: 备注
- `payee`: 收款人
- `checker`: 复核
- `drawer`: 开票人
- `goodsDetails`: 发票商品明细
  - `goodsName`: 货物服务名称
  - `specification`: 规格
  - `unit`: 单位
  - `quantity`: 数量
  - `unitPrice`: 单价
  - `itemAmount`: 金额
  - `taxRate`: 税率
  - `taxAmount`: 税额

## VAT_ROLL_INVOICE (增值税卷票)
- `title`: 发票名称
- `invoiceCode`: 发票代码
- `invoiceNo`: 发票号码
- `printedNo`: 机打号码
- `machineCode`: 机器编号
- `invoiceDate`: 开票日期
- `payee`: 收款员
- `buyerName`: 购方名称
- `buyerCode`: 购方纳税人识别号
- `sellerName`: 销售方名称
- `sellerCode`: 销售方纳税人识别号
- `totalAmountUpper`: 价税合计(大写)
- `totalAmountLower`: 价税合计(小写)
- `checkCode`: 校验码
- `goodsDetails`: 发票商品明细
  - `goodsName`: 项目
  - `unitPrice`: 单价
  - `quantity`: 数量
  - `itemAmount`: 金额

## TAXI_INVOICE (出租车发票)
- `title`: 标题
- `invoiceCode`: 发票代码
- `invoiceNo`: 发票号码
- `vehicleNo`: 车号
- `certificateNo`: 证号
- `date`: 日期
- `boardingTime`: 上车时间
- `alightingTime`: 下车时间
- `amount`: 金额
- `actualAmount`: 实收金额

## TRAIN_TICKET (火车票)
- `title`: 标题
- `ticketNo`: 车票编号
- `departStation`: 起始站
- `destinationStation`: 终止站
- `trainNo`: 车次
- `departDate`: 发车日期
- `departTime`: 发车时间
- `seatPostion`: 座位号
- `seatNo`: 座次
- `ticketPrice`: 票价
- `passengerName`: 旅客姓名
- `identifyId`: 身份证号
- `invoiceNo`: 发票号码
- `invoiceDate`: 开票日期
- `preTaxAmount`: 税前金额
- `taxRate`: 税率
- `taxAmount`: 税额
- `elecTicketNo`: 电子客票号
- `originInvoiceNo`: 原发票号码
- `buyerName`: 购买方名称
- `socialCreditCode`: 统一社会信用代码
- `refundTag`: 退票标识
- `replaceTag`: 换开标识
- `otherInfo`: 其他信息

## AIRPORT_TICKET (航空运输电子客票行程单)
- `title`: 标题
- `domesticTag`: 国内国际标识
- `gpBillNo`: GP单号
- `issueStatus`: 开具状态
- `invoiceNo`: 发票号码
- `serialNumber`: 印刷序号
- `passengerName`: 旅客姓名
- `identifyIdNo`: 身份证号码
- `endorsement`: 签注
- `openMark`: OPEN标示
- `refundMark`: 退票费标示
- `ticketPrice`: 票价
- `fuleDischarge`: 燃油附加费
- `civilAviationFund`: 民航发展基金
- `taxRate`: 增值税税率
- `taxAmount`: 增值税税额
- `otherTaxes`: 其他税费
- `totalAmount`: 合计金额
- `ticketNo`: 电子客票号码
- `checkCode`: 验证码
- `reminderInfo`: 提示信息
- `insuranceCharge`: 保险费
- `salesOutletCode`: 销售网点代号
- `issueUnit`: 填开单位
- `issueDate`: 填开日期
- `buyerName`: 购买方名称
- `taxPayerCode`: 纳税人识别号
- `airTransportRoutes`: 行程信息
  - `departPlace`: 始发地
  - `destinationPlace`: 目的地
  - `carrier`: 承运人
  - `flightNo`: 航班号
  - `seatLevel`: 座位等级
  - `departDate`: 航班日期
  - `departTime`: 航班时间
  - `ticketLevel`: 客票级别
  - `ticketEffectiveDate`: 客票生效日期
  - `ticketExpirationDate`: 客票失效日期
  - `freeLuggage`: 免费行李

## VEHICLE_SALE_INVOICE (机动车销售统一发票)
- `title`: 发票名称
- `invoiceForm`: 发票联次
- `invoiceCode`: 发票代码
- `invoiceNo`: 发票号码
- `issueDate`: 开票日期
- `printedCode`: 机打代码
- `printedNo`: 机打号码
- `machineCode`: 机器编号
- `taxControlCode`: 税控码
- `buyerName`: 购方名称
- `buyerTaxId`: 购方纳税人识别号
- `buyerCode`: 购买方身份证或组织机构代码
- `vehicleType`: 车辆类型
- `brandModel`: 厂牌型号
- `originalPlace`: 产地
- `qualifiedNo`: 合格证号
- `importCertificateNo`: 进口证明书号
- `commodityInspectionNo`: 商检单号
- `engineNo`: 发动机号码
- `vehicleIdentificationNo`: 车辆识别代号
- `totalAmountUpper`: 价税合计(大写)
- `totalAmountLower`: 价税合计(小写)
- `sellerName`: 销售方名称
- `sellerTaxId`: 销售方纳税人识别号
- `sellerAddressAndPhone`: 销售方地址及电话
- `sellerBankAndAccount`: 销售方开户行及账号
- `taxRate`: 增值税税率
- `taxAmount`: 增值税税额
- `taxAuthorityName`: 主管税务机关名称
- `taxAuthorityCode`: 主管税务机关代码
- `preTaxAmount`: 不含税价
- `taxPaymentVoucher`: 完税凭证号码
- `tonnage`: 吨位
- `maxCapacity`: 限乘人数
- `drawer`: 开票人
- `remark`: 备注

## QUOTA_INVOICE (定额发票)
- `title`: 发票名称
- `invoiceCode`: 发票代码
- `invoiceNo`: 发票代码
- `amountUpper`: 发票号码
- `amountLower`: 开票日期

## TOLL_INVOICE (过路过桥费发票)
- `title`: 标题
- `invoiceCode`: 发票代码
- `invoiceNo`: 发票代码
- `invoiceDate`: 开票日期
- `entranceLocation`: 入口
- `exitLocation`: 出口
- `exitTime`: 出口时间
- `totalAmountLower`: 合计金额(小写)

## MEDICAL_INVOICE (医疗发票)
- `title`: 标题
- `invoiceCode`: 发票代码
- `invoiceNo`: 发票号码
- `invoiceDate`: 开票日期
- `checkCode`: 校验码
- `payerName`: 交款人
- `payerAccount`: 交款人统一社会信用代码
- `payeeName`: 收款单位
- `totalAmountUpper`: 合计金额(大写)
- `totalAmountLower`: 合计金额(小写)

## TAX_PAYMENT_CERTIFICATE (税收完税证明)
- `title`: 标题
- `verifyCode`: 验证码
- `billNo`: 票据号码
- `fillDate`: 填发日期
- `taxAuthority`: 税务机关
- `taxCode`: 纳税人识别号
- `taxPayerName`: 纳税人名称
- `totalAmountUpper`: 合计金额(大写)
- `totalAmountLower`: 合计金额(小写)
- `issuer`: 填票人
- `remarks`: 备注
- `taxItems`: 纳税明细
  - `originBillNo`: 原凭证号
  - `taxType`: 税种
  - `itemName`: 品目名称
  - `taxPeriod`: 税款所属时期
  - `treasuryDate`: 入(退)库日期
  - `actualPayAmount`: 实缴(退)金额

## NON_TAX_INVOICE (非税票据)
- `title`: 标题
- `invoiceCode`: 发票代码
- `invoiceNo`: 发票号码
- `payerName`: 交款人
- `payerSocialCreditCode`: 交款人统一社会信用代码
- `checkCode`: 校验码
- `invoiceDate`: 开票日期
- `totalAmountUpper`: 合计金额(大写)
- `totalAmountLower`: 合计金额(小写)
- `payeeCompany`: 收款单位
- `checker`: 复核人
- `payee`: 收款人
- `itemDetails`: 项目明细
  - `itemCode`: 项目编码
  - `itemName`: 项目名称
  - `itemUnit`: 单位
  - `itemCount`: 数量
  - `itemCharge`: 标准
  - `itemAmt`: 金额
  - `itemRemark`: 备注

## GENERAL_MACHINE_INVOICE (通用机打发票)
- `title`: 标题
- `invoiceCode`: 发票代码
- `invoiceNo`: 发票号码
- `invoiceDate`: 开票日期
- `checkCode`: 验证码
- `buyerName`: 购方名称
- `buyerCode`: 购方纳税人识别号
- `buyerAddressAndPhone`: 购方地址及电话
- `buyerBankAndAccount`: 购方开户行及账号
- `sellerName`: 销售方名称
- `sellerCode`: 销售方纳税人识别号
- `sellerAddressAndPhone`: 销售方地址及电话
- `sellerBankAndAccount`: 销售方开户行及账号
- `preTaxTotalAmount`: 税前合计金额
- `totalTaxAmount`: 合计税额
- `totalAmountUpper`: 价税合计(大写)
- `totalAmountLower`: 价税合计(小写)
- `remarks`: 备注
- `payee`: 收款人
- `checker`: 复核
- `drawer`: 开票人
- `goodsDetails`: 发票商品明细
  - `goodsName`: 货物服务名称
  - `specification`: 规格型号
  - `unit`: 单位
  - `quantity`: 数量
  - `unitPrice`: 单价
  - `itemAmount`: 金额
  - `taxRate`: 税率
  - `taxAmount`: 税额

## SHIP_TICKET (船票)
- `title`: 标题
- `invoiceCode`: 发票代码
- `invoiceNo`: 发票号码
- `departureDate`: 开船日期
- `departureTime`: 开船时间
- `departStation`: 起始站
- `destinationStation`: 终止站
- `totalAmountLower`: 合计金额(小写)
- `passengerName`: 旅客姓名

## BUS_TICKET (汽车票)
- `title`: 标题
- `invoiceCode`: 发票代码
- `invoiceNo`: 发票号码
- `invoiceDate`: 开票日期
- `departureDate`: 开车日期
- `departureTime`: 开车时间
- `departStation`: 起始站
- `destinationStation`: 终止站
- `totalAmountLower`: 合计金额(小写)
- `passengerName`: 旅客姓名


path: CHANGELOG.md
content: # 变更日志

## [1.0.0] - 2025-03-12

### 新增
- 初始版本发布
- 支持 10 种 OCR 识别类型：通用文字、身份证、银行卡、营业执照、增值税发票、增值税卷票、出租车发票、火车票、航空行程单、机动车销售统一发票
- 从 `.env` 文件读取 API Key
- 友好的配置检测和 Token 过期提示
- 输出识别结果为 JSON 格式

## [1.0.1] - 2025-04-10

### 优化
重试机制：
- 支持优化 14 种 OCR 识别类型：支持增值税发票、增值税卷票、出租车发票、火车票、航空电子客票行程单、机动车销售统一发票、定额发票、过路过桥费发票、医疗发票、税收完税证明、船票、非税票据、通用机打发票、汽车票识别，自动提取票据关键信息。

## [1.0.2] - 2025-04-13

### 优化
- update channelTag

## [1.0.3] - 2025-04-15

### 优化
- update ClawHub 的安全扫描

## [1.0.4] - 2025-04-16

### 优化
- update ClawHub 的安全扫描V1

## [1.0.5] - 2025-04-16

### 优化
- update ClawHub 的安全扫描V2

path: LICENSE.txt
content: MIT License

Copyright (c) 2025 Sugon-Scnet OCR Skill Contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

path: README.md
content: # expense_invoice_ocr



## Getting started

To make it easy for you to get started with GitLab, here's a list of recommended next steps.

Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!

## Add your files

* [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
* [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:

```
cd existing_repo
git remote add origin https://github.com/SCNet-sugon/expense_invoice_ocr.git
git branch -M main
git push -uf origin main
```

## Integrate with your tools

* [Set up project integrations](http://10.19.0.66:10068/sugon/sugon-scnet-ocr/-/settings/integrations)

## Collaborate with your team

* [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
* [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
* [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
* [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
* [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)

## Test and Deploy

Use the built-in continuous integration in GitLab.

* [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/)
* [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
* [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
* [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
* [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)

***

# Editing this README

When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.

## Suggestions for a good README

Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.

## Name
Choose a self-explaining name for your project.

## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.

## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.

## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.

## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.

## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.

## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.

## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.

## Contributing
State if you are open to contributions and what your requirements are for accepting them.

For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.

You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.

## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.

## License
For open source projects, say how it is licensed.

## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.


path: references/api-docs.md
content: # Sugon-Scnet OCR API 文档摘要

## 接口地址
`POST https://api.scnet.cn/api/llm/v1/ocr/recognize`

## 请求头
- `Content-Type: multipart/form-data`
- `Authorization: Bearer <你的 API Key>`

## 请求参数（表单）
| 参数名  | 类型 | 必填 | 描述                                   |
| ------- | ---- | ---- | -------------------------------------- |
| file    | File | 是   | 需要识别的图片文件                     |
| ocrType | str  | 是   | 识别类型枚举，详见 SKILL.md 参数说明   |

## 响应结构
```json
{
    "code": "0",
    "msg": "success",
    "data":[ 
        {
            "traceId": "202604010000017",
            "originalFilename": "增值税发票示例.jpg",
            "cosPath": "/ocr/202604/01/增值税发票示例.jpg",
            "result": [
                {
                    "status": 200,
                    "originFilename": "增值税发票示例.jpg",
                    "cosPath": "/ocr/202604/01/增值税发票示例.jpg",
                    "fileIndex": 1,
                    "cutIndex": 0,
                    "coordinate": [],
                    "classifyCode": "VAT_INVOICE",
                    "confidence": 0.996,
                    "elements": {
                      "title": "厦门增值税专用发票",
                      "invoiceCode": "3502210221",
                      "invoiceNo": "04727777",
                      "printedCode": "3502210221",
                      "printedNo": "04727777",
                      "checkCode": "",
                      "machineCode": "499099900999",
                      "invoiceDate": "20220707",
                      "passwordArea": "03+6//8820/+688-+3>5<45-56>703+6//8820/+688-+3>5<45-56>703+6//8820/+688-+3>5<45-56>703+6//8820/+688-+3>5<45-56>7",
                      "buyerName": "中国思味银行股份有限公司厦门市分行",
                      "buyerCode": "913502008520082008",
                      "buyerAddressAndPhone": "厦门市思明区思明道05号05922105922",
                      "buyerBankAndAccount": "厦门分行营业部35350035003500013500",
                      "sellerName": "中国厦门厦门有限公司厦门分公司",
                      "sellerCode": "91350200720072007D",
                      "sellerAddressAndPhone": "厦门市思明路25号10000",
                      "sellerBankAndAccount": "思明厦门思明支行4100020009200020009",
                      "preTaxTotalAmount": "39622.64",
                      "totalTaxAmount": "2377.36",
                      "totalAmountUpper": "肆万贰仟圆整",
                      "totalAmountLower": "42000.00",
                      "invoiceForm": "第三联:发票联",
                      "remarks": "备注示例",
                      "payee": "张三",
                      "checker": "李四",
                      "drawer": "李明",
                      "goodsDetails": {
                        "goodsName": "*电信服务*增值电信服务",
                        "specification": "无",
                        "unit": "项",
                        "quantity": "1",
                        "unitPrice": "39622.64",
                        "itemAmount": "39622.64",
                        "taxRate": "6%",
                        "taxAmount": "2377.36"
                      }
                    },
                    "stamps": []
                }
            ]
        }
    ]
}
```
## 错误码
- `401 / 403: Token 无效或过期`
- `其他 4xx/5xx: 请检查请求参数或联系服务商`
- `业务错误码（如 code 非 0）：见返回的 msg 字段`

## 注意事项
- `支持单张图片、PDF 或多页压缩包（自动解压识别）`
- `识别结果位于 data[0].result[0].elements 中`
- `不同 ocrType 返回的 elements 字段不同，详见 assets/templates/fields-summary.md`


path: scripts/main.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Sugon-Scnet 财务单据 OCR 技能主脚本
接收命令行参数：ocrType filePath
输出：识别结果的 JSON
"""

import os
import sys
import json
import time
import requests
import mimetypes
from pathlib import Path

# 获取技能根目录（脚本所在目录的上一级）
SKILL_ROOT = Path(__file__).parent.parent.absolute()
ENV_FILE = SKILL_ROOT / "config" / ".env"

# --- 新增：重试配置 ---
MAX_RETRIES = 3            # 最大重试次数
RETRY_BACKOFF_FACTOR = 2   # 退避因子，每次重试等待时间翻倍
INITIAL_RETRY_DELAY = 1    # 初始等待时间（秒）
# --------------------

def load_config():
    """从 .env 文件加载配置，若文件不存在则抛出友好错误"""
    if not ENV_FILE.exists():
        error_msg = (
            "\n===============================================\n"
            "Scnet OCR 配置文件不存在\n"
            "===============================================\n"
            "⚠️ 安全警告：切勿在聊天中直接粘贴 API Key！\n\n"
            "请按以下步骤安全配置：\n\n"
            "1. 申请 Scnet API Token：\n"
            "   访问 https://www.scnet.cn 注册并获取密钥\n\n"
            "2. 配置 Token（选择一种方式）：\n"
            "   a) 环境变量（推荐）：\n"
            "      export SCNET_API_KEY='你的密钥'\n"
            "   b) 配置文件：\n"
            f"      mkdir -p {SKILL_ROOT}/config\n"
            f"      echo 'SCNET_API_KEY=你的密钥' > {ENV_FILE}\n"
            f"      chmod 600 {ENV_FILE}\n"
            "\n配置完成后重新运行。"
        )
        sys.exit(error_msg)

    config = {}
    with open(ENV_FILE, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith('#'):
                continue
            if '=' in line:
                key, value = line.split('=', 1)
                key = key.strip()
                value = value.strip().strip('"').strip("'")
                config[key] = value

    # 检查必要配置
    [REDACTED_SECRET]'SCNET_API_KEY', '')
    if not api_key or api_key == 'your_scnet_api_key_here':
        error_msg = (
            "\n===============================================\n"
            "Scnet API Key 未配置\n"
            "===============================================\n"
            "请按以下步骤配置：\n\n"
            "1. 申请 Scnet API Token：\n"
            "   访问 https://www.scnet.cn 注册并获取密钥\n\n"
            "2. 配置 Token：\n"
            f"   编辑 {ENV_FILE}\n"
            "   设置 SCNET_API_KEY=你的密钥\n"
        )
        sys.exit(error_msg)

    config.setdefault('SCNET_API_BASE', 'https://api.scnet.cn/api/llm/v1')
    return config

def recognize_with_retry(ocr_type, file_path, config, retry_count=0):
    """
    带重试机制的 OCR 识别函数。
    当遇到 429 (Too Many Requests) 时，自动等待后重试。
    调用 Scnet OCR API 进行识别"""
    api_base = config['SCNET_API_BASE']
    [REDACTED_SECRET]'SCNET_API_KEY']
    url = f"{api_base}/ocr/recognize"

    # 检查文件是否存在
    if not os.path.isfile(file_path):
        sys.exit(f"错误: 文件不存在 - {file_path}")

    # 自动检测 MIME 类型
    mime_type, _ = mimetypes.guess_type(file_path)
    if mime_type is None:
        mime_type = 'application/octet-stream'

    headers = {
        'Authorization': f'Bearer {api_key}'
    }

    try:
        with open(file_path, 'rb') as f:
            files = {
                'file': (os.path.basename(file_path), f, mime_type)
            }
            data = {
                'ocrType': ocr_type,
                'channelTag': "scnetSkills"
            }
            response = requests.post(url, headers=headers, data=data, files=files, timeout=60)
    except Exception as e:
        sys.exit(f"网络请求失败: {str(e)}")
    # --- 新增：处理 429 速率限制 ---
    if response.status_code == 429:
        if retry_count >= MAX_RETRIES:
            sys.exit(f"错误: 请求被限流 (429)，已达到最大重试次数 {MAX_RETRIES}。请稍后再试。")

        # 尝试从响应中获取重试等待时间（如果有）
        retry_after = INITIAL_RETRY_DELAY * (RETRY_BACKOFF_FACTOR ** retry_count)
        try:
            error_data = response.json()
            # 某些 API 会在响应中返回 retry_after 字段
            if 'retry_after' in error_data:
                retry_after = int(error_data['retry_after'])
        except:
            pass

        # 输出友好提示到 stderr（不影响 JSON 输出）
        sys.stderr.write(
            f"⚠️ 请求过于频繁，等待 {retry_after} 秒后重试... (第 {retry_count + 1}/{MAX_RETRIES} 次重试)\n")
        time.sleep(retry_after)

        # 递归重试
        return recognize_with_retry(ocr_type, file_path, config, retry_count + 1)
    # ---------------------------------
    if response.status_code != 200:
        # 针对 401/403 给出明确提示
        if response.status_code in (401, 403):
            error_msg = (
                "\n===============================================\n"
                "Scnet API Token 无效或已过期\n"
                "===============================================\n"
                f"HTTP 状态码: {response.status_code}\n\n"
                "⚠️ 安全警告：请勿在聊天中直接粘贴 API Key！\n\n"
                "解决方法（二选一）：\n"
                "1. 环境变量方式：\n"
                "   export SCNET_API_KEY='你的新Token'\n"
                "2. 配置文件方式：\n"
                f"   echo 'SCNET_API_KEY=你的新Token' > {ENV_FILE}\n"
                f"   chmod 600 {ENV_FILE}\n"
                "\n更新后重新运行。"
            )
            sys.exit(error_msg)
        else:
            sys.exit(f"HTTP 错误 {response.status_code}: {response.text}")

    try:
        result = response.json()
    except Exception:
        sys.exit(f"响应不是有效的 JSON: {response.text}")

    # 检查业务状态码
    if result.get('code') != '0':
        sys.exit(f"API 错误 {result.get('code')}: {result.get('msg')}")

    # 输出 data 部分（识别结果）
    #print(json.dumps(result.get('data', []), ensure_ascii=False, indent=2))
    # 获取 data 部分
    data = result.get('data', [])

    # 移除每个识别项中的 confidence 字段（优化点）
    for file_result in data:
        if 'result' in file_result and isinstance(file_result['result'], list):
            for item in file_result['result']:
                item.pop('confidence', None)  # 删除 confidence，不存在则忽略

    # 输出处理后的数据
    print(json.dumps(data, ensure_ascii=False, indent=2))

def main():
    if len(sys.argv) != 3:
        print("用法: python main.py <ocrType> <filePath>")
        print("ocrType 可选值: VAT_INVOICE, VAT_ROLL_INVOICE, TAXI_INVOICE, TRAIN_TICKET, AIRPORT_TICKET, VEHICLE_SALE_INVOICE, QUOTA_INVOICE, TOLL_INVOICE, MEDICAL_INVOICE, TAX_CERTIFICATE, SHIP_TICKET, NON_TAX_BILL, GENERAL_MACHINE_INVOICE, BUS_TICKET")
        sys.exit(1)

    ocr_type = sys.argv[1]
    file_path = sys.argv[2]

    config = load_config()
    # 调用带重试的识别函数
    recognize_with_retry(ocr_type, file_path, config)

if __name__ == '__main__':
    main()


path: skill.yaml
content: name: expense_invoice_ocr
version: 1.0.4
description: 支持识别企业财务报销场景的常见票据，包括增值税发票、增值税卷票、出租车发票、火车票、航空电子客票行程单、机动车销售统一发票、定额发票、过路过桥费发票、医疗发票、税收完税证明、船票、非税票据、通用机打发票、汽车票识别。
author: SCNet
license: MIT
tags:
  - ocr
  - 文字提取

required_env_vars:
  - SCNET_API_KEY

optional_env_vars:
  - SCNET_API_BASE

primary_credential: SCNET_API_KEY

# 增加项目主页，解决 missing homepage 警告
homepage: https://github.com/SCNet-sugon/expense_invoice_ocr.git  # 请替换为实际仓库地址

dependencies:
  - python3
  - requests

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