全国增值税发票查验(Pay Skill)
你(AI)通过本技能的脚本,在全国增值税发票查验平台完成发票真伪查验。
你的职责只有四件事:付费授权 → 准备发票要素 → 看一张验证码拼图并写出识别结果 → 解读结构化结果。
其余(开浏览器、填表、取图、切字、提交、抓响应)全部由脚本完成。
> 接口契约:付费链路的入参/出参、状态分支、402 与票据格式在 references/api-contract.md;
> 本地查验命令的入参/出参、事件流、错误码在 references/api-spec.md;
> 结果 JSON 的全字段在 references/result-schema.md;要素填写规则在 references/field-rules.md;
> 结果码在 references/result-codes.md。动手前先读 api-contract.md,不要靠猜参数。
计费与交付形态(先读,决定执行顺序)
- 单次 0.5 元,按张计费:一次付款授权查验 1 张发票,批量查验请逐张付费。
- 服务端交付物是「查验授权票据」,不是查验结果。
查验平台的验证码必须由调用方(你 / 用户)看图解出,服务端无法独立完成一次真实验真。
因此:服务端负责 402 报价 → 验付 → 签发绑定该票的授权票据 → 履约确认;
实际查验由本 Skill 在你本地执行。 - 付款前服务端不读取查验要素、不访问查验平台。
- 一张票据绑定一张票(
invoice_fingerprint),有效期 6 小时;同一凭证重试不重复扣费。 - 🔴 执行顺序铁律:先走完付费三步拿到票据,再启动本地查验。
未取得票据就启动verify.py属于未授权使用。
付费三步(缺一不可)
1. probe —— 问价(不付款、不查验)
node scripts/vat-invoice-verify.mjs probe \
--no <发票号码> --date <YYYYMMDD> --amount <价税合计> \
--state-dir .state/vat-invoice-verify
老式增值税票(代码 + 8 位号码)改用:
node scripts/vat-invoice-verify.mjs probe \
--no 00000001 --date 20221027 --invoice-code 000000000000 --check 000000 \
--state-dir .state/vat-invoice-verify
- 未付款时返回 402 +
Payment-Needed账单;脚本打印账单后立即停止 - 必须先向用户展示账单,取得明确同意后才进入第 2 步,不得自动付款
2. pay —— 用户确认后付款
node scripts/vat-invoice-verify.mjs pay --state-dir .state/vat-invoice-verify
- 复用第 1 步保存的请求与账单,调用官方
alipay-bot发起付款(商品名「增值税发票查验」) - 输出收款二维码与付款链接,由用户扫码完成支付,并输出交易号
tradeNo
3. complete —— 带凭证重试,取得授权票据
node scripts/vat-invoice-verify.mjs complete --state-dir .state/vat-invoice-verify
- 携带官方
Payment-Proof以完全相同的要素重试,服务端验付通过 → 签发票据 → 履约确认 - 票据写入
<state-dir>/ticket.txt,并打印下一步可直接复制的verify.py命令 - 同一订单重复执行返回
already_fulfilled: true,不重复扣费
依赖安装(仅首次需要)
npx -y @alipay/agent-payment@latest install # 买家侧支付依赖
npx -y @alipay/agent-payment@latest install-cli # 若 alipay-bot 不可用,补装命令行
> 卖家侧收费接口已独立部署,不在本 Skill 包内,也不需要买家配置任何密钥。
硬性约束(先读)
- 每张发票每天最多查验 5 次。金额必须一次填对:数电票填价税合计(红字发票带负号,如
-1.21),
不要填不含税金额。填错返回"发票信息不一致"会消耗 1 次额度。 - 验证码错误/失效通常不消耗额度,脚本自动换新码重试(默认 3 次)。
- 验证码有效期约 2~3 分钟:拿到拼图后立刻识别并写入
code.txt,中途不要做别的。 - 仅限企业自有发票的低频自查:串行执行,票间已内置 8 秒间隔(
--gap)。
禁止并发、禁止高频批量(>50 张/小时),不得使用任何第三方打码/破解服务。 - 技能目录视为只读。所有运行产物写到工作目录,见下一节。
- 一次付费对应一张票:换一张票查验必须重新走付费三步。
环境无关约定(重要)
- 脚本不硬编码任何本机路径。浏览器按平台探测:
CHROMIUM_PATH→ Playwright 缓存 → 系统安装路径
(Win/macOS/Linux 三套);字体同样按平台探测,找不到就退化,不报错。 - 运行产物全部写到工作目录,解析优先级:
--workdir> 环境变量VAT_VERIFY_WORKDIR><系统临时目录>/vat-invoice-verify。
目录内:code.txt(你写验证码)、captcha/(图)、resp/(平台原始响应)、results.json(结果)。 - 每条 JSON 事件都带
work_dir绝对路径,直接用它拼路径,不要自己猜。
前置检查
python3 scripts/verify.py env --workdir <工作目录>
ready: false 时按 issues 里的命令补齐(一般只需两条):
python3 -m pip install playwright Pillow pypdf # 国内网络可加 -i https://mirrors.aliyun.com/pypi/simple/
python3 -m playwright install chromium # 或设 CHROMIUM_PATH 指向已有 chrome/chromium
可选:pip install ddddocr 后加 --ocr,让本地 OCR 先给一版候选。
工作流:单张查验
第 0 步:要素提取(有发票 PDF/OFD 文件时先跑,避免手工抄错):
python3 scripts/parse_invoice.py "发票1.ofd" "发票2.pdf"
输出 no / date / amount / amount_cn / icode / check / confidence。confidence: high 可直接用;
medium/low 必须打开原文件人工核对(重点:价税合计的正负号和小数位)。
老式增值税票用 check(校验码后六位),此时不需要 amount。
第 1 步:付费取得授权票据(见上文「付费三步」):
node scripts/vat-invoice-verify.mjs probe --no <号码> --date 20261029 --amount 189.16 --state-dir .state/vat-invoice-verify
node scripts/vat-invoice-verify.mjs pay --state-dir .state/vat-invoice-verify # 用户扫码
node scripts/vat-invoice-verify.mjs complete --state-dir .state/vat-invoice-verify # 拿到票据
参数规则(详见 references/field-rules.md):
- 数电票(20 位号码):
--no填号码,--invoice-code不填,--amount填价税合计 - 老式增值税票(代码 + 8 位号码):
--invoice-code填发票代码,--check填校验码后六位 - 日期一律
YYYYMMDD
第 2 步:后台启动查验进程(写日志文件,便于你实时读取):
python3 scripts/verify.py check \
--no <发票号码> --date 20261029 --amount 189.16 \
--workdir <工作目录> > <工作目录>/run.log 2>&1
第 3 步:等 need_code 事件并读图。日志里出现 {"stage":"need_code", ...} 后:
- 先读事件的
tips字段——平台常要求"请输入图中蓝色/红色文字",此时只提交该颜色的字符,
其他颜色是干扰项(详见下面"彩色验证码"一节)。 - 看事件里
montage指向的那一张图(按目标色分割、去噪、切字后的白底黑字放大字符,红色数字是序号)。
- 事件里mask_suspect: true→ 掩码把纯色背景当成了前景,拼图已反相,直接改看raw_zoom.png
- 某个序号块内两字符重叠(切字未分开)时,按"上层字符+下层字符"顺序都读出来 - 一次
need_code只做一次读图,然后立刻写码。多次图像往返必然超过 150 秒等待窗口,
超时后本条作废(不消耗额度,但要重跑)。
第 4 步:立即写入识别结果(纯文本,不带引号和换行)。路径必须取自事件的 code_file 字段:
printf '%s' 'QEDR' > <code_file>
# PowerShell: Set-Content -NoNewline -Encoding utf8 <code_file> -Value 'QEDR'
> code_file 默认是 <work_dir>/code_<发票号码>.txt(按票号隔离);只有你显式传了 --code-file
> 才会多票共用一个文件——那种情况下旧码可能被下一张票吃掉,非必要不要指定。
第 5 步:读结果。约 2~10 秒后日志末尾出现 stage: done / check_done:
{
"stage": "done",
"CyjgDm": "7",
"result": "经查验,发票信息一致",
"check_seq": "1",
"work_dir": "<工作目录>",
"raw_file": "<工作目录>/resp/resp_<发票号码>.json",
"verify": { "查验过程信息" },
"invoice": { "票面全要素" },
"input": { "本次录入的三要素" }
}
stage 取值:start / need_code(等你读图)/ code_urging(等待过半,催你写码)/ retry(验证码错,已换新码,回到第 3 步)/
done(本票出结果)/ check_done(全部结束,带 result_file)/ timeout(你没来得及写码,重跑本条)/ failed。
> 「不一致」(CyjgDm=8)的响应不含票面数据,此时 invoice.* 全为 null;
> 该记录属于哪张票要看 input(录入要素回显)与顶层 invoice_no。
结果 JSON = 全票面信息,与平台《发票查验结果》文件同构:
verify:查验次数 / 时间 / 结果 / 来源 / 流水号 / 发票来源与种类代码 / 风险标志invoice:- 票头——号码、开票日期时间、票种、状态、是否红字 + 被红冲蓝字号码、开票方式、特定业务类型、合同号、扣除额
- 金额——价税合计(小写 + 大写)、合计金额、合计税额
- 人员——开票人、收款人、复核人、收款人账号、备注
- 购销方——名称 / 税号 / 地址 / 电话 / 开户行 / 账号(销方另有登记序号,购方另有经办人四件套)
items明细行——名称、票面显示名、商品服务简称、规格、单位、数量、单价、金额、税率、税额、税收分类编码、行性质(正常/折扣)vehicles机动车要素——厂牌型号、产地、合格证号、发动机号、车架号 VIN、生产企业、乘坐人数、含税价格(机动车票的"明细"在这里,items为 null)qr_code票面二维码(已拆成号码/金额/日期/校验码)、check_qr查验二维码地址tax_authority_codes税务机关代码、flags标志位、meta平台元数据、raw_flags原始标志位保真
完整字段表见 references/result-schema.md,脱敏样例见 examples/result.example.json。
需要补字段时不要重新上网查验:原始响应已落盘在 raw_file,改完 normalize_result() 后离线重算:
python3 scripts/verify.py normalize --raw <work_dir>/resp/resp_xxx.json --out results.json
工作流:批量查验
按张计费:清单里每张票各自走一次「付费三步 → 本地查验」。
送验前先向用户说明总费用(张数 × 0.5 元)并取得同意。
清单格式(详见 api-spec.md 第 4 节,样例 examples/invoices.example.json):
[
{"no": "26422000000000000001", "date": "20261029", "amount": "189.16"},
{"no": "00000001", "date": "20221027", "icode": "000000000000", "check": "000000"}
]
对每张票:
probe→ 展示账单 →pay(用户扫码)→complete(拿票据)python3 scripts/verify.py check --no ... --date ... --amount ... --workdir <工作目录> > run.log 2>&1- 等
need_code→ 识别 → 写code.txt→ 等本票的结束事件(stage为done/timeout/failed)
务必串行处理,写码前先用事件里的 invoice 字段确认当前是哪张票。
⚠️ code_file 必须取 need_code 事件里的 code_file 字段。
start 事件里的 code_file 是占位值(code_default.txt),往那里写码静默无效——
脚本等的是票号隔离的 code_<发票号码>.txt,超时后才报 timeout(实测踩过:白白错一次验证码)。
送验前先去重(省额度、避免自造冲突):
- 同一票号多份文件(如
xxx.pdf与xxx - 副本.pdf)——只查验一次,其余文件仅作票面核对。 - 当日已查验过的票——直接引用既有结论,不重复提交(每票每日仅 5 次额度)。
已知结论可从历史<work_dir>/results*.json或运行日志的done事件里取。 - 清单里每个条目加
_note写明票种与关键主体,便于事后核对结论归属。
⚠️ results.json 会被每次运行覆盖。分批跑(如先跑 3 张、超时票再单独重跑)时,
首轮结果只留存在 run.log 的 done 事件里——汇总时要把各轮日志合并,
或用 verify.py normalize --raw <work_dir>/resp/resp_<票号>.json 从原始响应重算。
彩色验证码(当前平台主力题型)
平台上大量验证码是多色字符混排 + 指定颜色的题型,识别纪律如下:
- 先读
tips:请输入验证码图片中蓝色文字→ 只提交蓝色字符,红/黑/绿等一律是干扰项。
实战样本:一图内M蓝 W红 S蓝 R蓝 U红 E红,只写MSR即通过;另一张2红 Q蓝 M黑 W黑 9蓝 S黑,写Q9通过。 - 拼图可能被背景污染:纯色背景(蓝/粉/紫底)会让掩码把背景整块当前景,
montage变成黑底白字的反相块。
脚本已自动检测并给出mask_suspect: true+advice——看到这个标记就直接看raw_zoom.png,
不要浪费时间研究反相拼图。 - 指定颜色的掩码反而最准:
tips给了颜色时,montage通常切得很干净(实测蓝色掩码切出1、2、4一目了然),
优先用拼图、raw_zoom只做交叉确认。 - 字符不限于字母数字:已观察到汉字(如"古")、符号(
#)参与验证码,不要默认 4 位字母。
char_count只是切出的连通域个数,不等于字符数——重叠字符会被并成一段、
断裂笔画会被拆成两段(实测标称 2 段实为 3 个字符)。以你看到的为准。 color: auto题型多为 6 位重叠字符:描边互相压叠、外加绿/青色斜线干扰。
此时montage的二值化常把细笔画切丢(出现大片黑色块或只剩横杠),
直接看原图:captcha_raw.png用 LANCZOS 放大 8~10 倍再读,一次到位。
注意raw_zoom.png与view.png都是处理过的图,重叠题型也会失真。- 底色会骗人:同一张图在部分渲染路径下底色显示为蓝/橄榄绿,不要据此判断字符颜色;
判色看captcha_raw.png原图。 - 时间纪律:验证码约 2~3 分钟失效,脚本等待窗口默认 150 秒并在过半时发
code_urging提醒。
一次need_code必须完成在"读图 → 写码"两步内。不要在 need_code 期间做长时间图像分析
(反复裁切/多轮放大极易拖过 150 秒 →timeout,该次不扣查验额度但要重跑)。
验证码识别提速要点(为什么这样做)
- 直取原图:从 DOM 的 img.src 直接拿 base64(自动 URL 解码),不经截图/裁剪/缩放,零坐标误差
- 颜色自动分割:从
.yzm-tips判定目标色,只保留目标色笔画,剔除干扰字符与噪点 - 切字 + 拼图:连通域去噪后按列投影切字,横向拼成带序号的 1 张
montage.png——你只需 1 次读图 - 可疑掩码自检测:前景占比 >40% 判定为"背景被当前景",直接提示改看
raw_zoom - 失败零成本重试:验证码错误自动换新码重来,通常不消耗每日额度
分享样例前先脱敏
反馈问题或写文档时,不要贴含真实主体信息的响应:
python3 scripts/make_example.py <真实响应.json> --out masked.json
python3 scripts/verify.py normalize --raw masked.json --out result.json
脱敏范围:主体名称、税号、证件号、银行账号与开户行、地址电话、VIN/发动机号/合格证号、
开票人姓名、IP、查验流水号、发票号码、二维码串。金额与税率保留(不含身份信息,样例需体现数值形态)。
空值字段保持为空——不会填成假值。
故障排查
付费链路
| 现象 | 处理 |
|---|---|
| 未付款直接调用 | HTTP 402 + 账单,走第 2 步付款 |
| 已付款但要素缺失 | HTTP 400 INVOICE_ELEMENTS_REQUIRED;不签发票据、不履约确认,补上要素用同一凭证重试,不重复扣款 |
| 金额/校验码格式非法 | HTTP 400 INVALID_AMOUNT / INVALID_CHECK_CODE |
找不到 alipay-bot |
npx -y @alipay/agent-payment@latest install-cli |
| 付款后拿不到票据 | 检查三步是否共用同一 --state-dir;complete 必须复用 probe 保存的同一份要素 |
用 127.0.0.1 自测 |
买家 CLI 拒绝私有网段,必须用公网域名 https://invoice.aigoes.cn |
本地查验
| 现象 | 处理 |
|---|---|
未找到浏览器内核 |
设 CHROMIUM_PATH,或 python3 -m playwright install chromium |
未取到验证码图片 |
页面未加载完或平台改版;重跑,仍失败用 --headful 核对 DOM |
| 反复"未捕获查验请求" | 表单校验未过:看 debug_filled(5 个输入框实际值)与 debug_btn_disabled |
发票信息不一致 |
检查金额是否应为价税合计/是否漏负号(已消耗 1 次额度,谨慎重试) |
目标颜色无笔画 |
换 --color,或直接看 raw_zoom.png |
| 验证码总识别错 | 加 --ocr 先拿候选;或对不确定的字符块单独放大再看一眼 |
| 平台报"验证码失效" | 写码超时了,重跑本条 |
PDF 报 More than one /FontFile found |
脚本已内置自愈(同一描述符挂了 /FontFile2+/FontFile3 时保留第一个)。若仍抛错,说明修补没命中——检查键名是否带前导斜杠(PDF 名称对象形如 /FontFile2,写成 FontFile2 会静默失效) |
机动车销售统一发票 amount 为 null |
该类版式「标签全部前置、票面无 ¥ 符号」,需靠大写金额反查等值项;若 amount_cn 也是 null 说明票面大写缺失,必须人工读票 |
票面出现多个等值金额或 amount_mismatch: true |
停止送查验,人工核对后再跑(错报「不一致」会消耗当日额度) |
更完整的错误表见 references/api-spec.md 第 8 节。
约束
- 必须先展示账单并取得用户同意,再发起付款。
- 付费三步必须使用同一个
--state-dir,否则无法复用账单与凭证。 - 一次付费授权查验一张发票;换票需重新付费。
- 包内不含任何密钥、
.env或状态文件;后端收费服务独立部署。