纳税合规报告(nshg-baogao)
> 面向纳税合规报告平台的调用 Skill。全程只向用户收 3 样东西:开放平台 API Key / Secret(仅首次需要)、
> 要查哪家企业、以及企业主体确认(二选一时点头)。其余自动完成,不向用户输出接口路径、
> 参数、脚本命令等技术细节。
环境
SKILL_DIR="$HOME/.workbuddy/skills/nshg-baogao__skillhub" # 本机已安装路径(开发源:D:\skill\nshg-baogao__skillhub)
OPENAPI="$SKILL_DIR/scripts/openapi.py" # 网关调用层(凭证 + HMAC-SHA256 签名)
COLLECT="$SKILL_DIR/scripts/collect.py" # 业务编排层(凭证/企业名检查 → 查记录 / 搜企业 / 取授权链接 / 再次采集 / 报告 PDF)
ENDPOINTS="$SKILL_DIR/scripts/endpoints.json" # 业务接口登记表
- 运行需 Python 3.9+(纯标准库,无第三方依赖)。
- 默认测试环境网关
https://openapi.shuzutech.com/test/baseplat;切生产设BAOGAO_ENV=prod。 - 命令输出为一行 JSON(含
success/error/next_step),依据next_step推进下一步。
先分流:用户到底想要什么(入口路由,重要)
用户开口通常是两种诉求,入口不同,不要混着走:
| 用户说 | 意图 | 走哪条链路 | 入口 |
|---|---|---|---|
| 查看报告 / 看报告 / 出报告 / 报告呢 | report | 查授权记录 → 由记录状态决定出报告 / 提示等待 / 引导采集 | records |
| 做采集 / 采集一下 / 帮我采集 / 再次采集 | collect | 不查记录,直接生成采集链接给用户 | recollect |
任务开始时运行一次 check,同时完成意图分流、凭证检查和企业名称检查;
如果用户补充了意图、企业名或凭证,补齐后再运行 check:
python "$COLLECT" check --intent report --company "某某科技有限公司" # 看报告 → records
python "$COLLECT" check --intent collect --company "某某科技有限公司" # 做采集 → recollect/search
python "$COLLECT" check # 诉求不明 → ask-intent
check不发网络请求,并会记住传入的企业名;切换企业时会清除上一家的信用代码。- 用户没说明白就不要猜:返回
ask=intent时,先让用户在【查看报告】和【做采集】中选择,再带对应--intent重跑。 route保留作兼容入口;新流程不需要额外运行它。- 采集链路必须先有统一社会信用代码:本地没有该企业的码时(含「上一家的码不能给这家用」的情况),
next_step会是search-enterprise—— 先搜企业让用户确认主体,确认后直接recollect即可。
> ⚠️ 主体绝不串用:本地缓存的信用代码只在归属企业 == 当前企业时才复用(local_credit_code() 做校验),
> 换企业时会清掉上一家的码。宁可多搜一次企业让用户确认,也不要拿错主体的码去生成采集链接。
唯一调用链路(重要)
用户指令 → collect.py(业务编排)→ openapi.py(凭证 + 签名)→ 开放平台网关
- 不存在第二条链路:旧的
app-code+shu-auth: Bearer会话链路已下线,collect.py不再依赖登录态,
调业务接口无需先登录。 openapi.py完全自包含:凭证落在独立文件
~/.xiaomei-workspace/nshg-baogao/openapi_creds.json(600 权限,可用BAOGAO_CREDS_FILE覆盖),
不读写任何登录模块的数据。历史版本曾把凭证寄存在auth_tokens.json,首次读取会自动迁移过来。- dev / prod 两套凭证按环境隔离,绝不互串。当前环境没绑凭证时,
check会返回
ask=credentials要求补齐该环境的 Key,不会退回去用另一个环境的凭证。
切换环境时顺序必须是「先切BAOGAO_ENV,再bind」,否则新 Key 会被记到旧环境下。
unbind也只删当前环境那一条,另一环境的同名凭证(通常都叫default)保留。
流程(严格按序,不可跳步)
【任务开始时运行 check --intent】
先确认意图、凭证和企业名称;补齐缺项后重跑 check,再按 next_step 继续:
├─「想看报告」→ 链 A:records ← 查授权记录,按记录状态决定下一步
└─「想做采集」→ 链 B:recollect ← 不查记录,直接生成采集链接
│
┌─────────────────┴──────────────────┐
▼ 链 A(report) ▼ 链 B(collect)
records 取最新一条记录 recollect 生成采集链接
│ (本地无该企业信用代码 → 先 search 确认主体)
├─ 无记录 ──> search ─> 用户确认主体 ─> auth-url ─> 用户授权 ─> 回到 records
├─ 1 待采 / 2 认证中 / 3 采集中 ─> 问用户「继续等待」还是「再次采集」
│ └─ 选「再次采集」─> recollect ─> 给用户新链接
├─ 4 采集失败 ─> 告知失败 + 直接 recollect 重新生成采集链接
└─ 5 成功 ────> 直接生成报告 PDF ─> 打开给用户
> 「再次采集」不是独立接口:它与第 5 步首次授权共用同一条链路(get-auth-url),
> 区别只是重新生成一张采集链接、让用户再走一次采集。所以 recollect 与 auth-url
> 参数完全一致,实现也是同一份代码。
依据 collect.py check --intent report|collect 返回的 ask / next_step 推进,缺什么补什么,不要自己拼流程。
诉求不明时运行 collect.py check,先询问用户选择,再带意图重跑。
第 1 步:凭证(先看缓存,别上来就问用户)
凭证缓存在 ~/.xiaomei-workspace/nshg-baogao/openapi_creds.json,
脚本已经自动查过了,只有当 check 返回 cred_bound=false 时才轮到你开口:
ask="credentials"/next_step="bind-openapi"→ 用 AskUserQuestion 一次性索取
【API Key】【Secret】两项,不要分两次打扰。- 拿到后执行
bind(Secret 走 stdin 喂入,不进命令行历史、不回显)。 ask=""/ready=true→ 缓存里已有凭证,直接进第 2 步,不要再问用户要密钥。
> dev / prod 凭证按环境隔离;Secret 只在绑定时经 stdin 传入,creds 输出一律脱敏。
第 2 步:企业名称(用户没说就必须问)
ask="company" / next_step="ask-company" 说明用户还没指明查哪家,向用户询问企业名称。
用户已在对话里给出企业名时,用 --company "名称" 传入;名称确认后脚本会记住,
之后同一家企业不用再问(company_source="state" 时建议向用户复述一句确认)。
check 输出的 recent_companies 是可复用候选。
> 用户给的是简称也没关系 —— 不用追问全称,交给第 4 步的企业搜索去匹配;
> 但不要自作主张替用户补全名称。
第 3 步:查授权记录
ready=true 后执行 records,脚本取该企业最新一条记录(pageSize=1,不带状态过滤),
并按状态直接给出下一步:
python "$COLLECT" records --company "某某科技有限公司"
授权/采集状态映射(服务端 status 字段):
| status | 含义 | 脚本给出的分支 |
|---|---|---|
| 1 | 待采 | ask=wait-or-recollect → 问用户「继续等待」还是「再次采集」 |
| 2 | 认证中 | 同上 |
| 3 | 采集中 | 同上 |
| 4 | 采集失败 | 直接重新生成采集链接(recollect 同链路),返回 auth_url 展示给用户 |
| 5 | 成功 | 自动生成报告 PDF,返回 file_path,直接打开给用户 |
| 无记录 | 尚未授权 | next_step=search-enterprise → 走第 4、5 步 |
各分支的处置要求:
- 1 / 2 / 3 → 用 AskUserQuestion 让用户在【继续等待】【再次采集】里二选一,
不要替用户决定,也不要说"应该快好了"这种话。
用户选【再次采集】时执行recollect,把返回的auth_url完整展示给用户重新采集。 - 4 → 先明确说采集失败、需要再次采集(不要粉饰成"稍后就好"),
然后把输出里的auth_url完整展示给用户点击重新采集。 - 5 → 直接打开
file_path,不要在报告之外再追加解读或结论(报告里有什么就展示什么)。 - 无记录 → 见第 4、5 步。
> 需要临时加回状态过滤(例如只看成功记录)可用 --param status=5;
> 需要多看几条用 --page-size N。
> 状态 4 时会自动调 get-auth-url 出链接;若记录里和本地都没有统一社会信用代码,
> 会返回 recollect_error=MISSING_CREDIT_CODE / next_step=search-enterprise,
> 回到第 4 步让用户确认主体即可(确认后本地就有信用代码了)。
第 4 步:搜索企业,让用户确认主体(仅无记录时)
python "$COLLECT" search --keyword "数族" --page-size 5
把用户给的企业名称(简称也行)原样作为 keyword,返回 candidates 列表。
必须做的一步:把候选的【企业名称】+【统一社会信用代码】列给用户,让他确认到底是哪一家。
即使只有 1 条候选也要确认——主体错了后面全错。脚本已把候选存进本地状态。
第 5 步:生成授权链接给用户(确认主体后)
python "$COLLECT" auth-url --index 2 # 用户说"第 2 家"
# 或直接指定:
python "$COLLECT" auth-url --company "数族科技(南京)股份有限公司" --credit-code "91320191MA1ML4CL25"
返回的 auth_url 是 H5 链接,必须完整展示给用户点击(不要只说"已生成链接")。
用户打开完成授权后,回到第 3 步再跑一次 records —— 只要状态到了「成功」,records 会直接把
报告 PDF 出好,不需要再单独跑 report。
> --index 对应上次 search 的候选序号(1 起),省得手工拼信用代码;越界会明确报错。
> 主体信息也会落盘,下次同一家企业可直接 auth-url 复用。
> 「再次采集」用 recollect —— 与 auth-url 是同一条链路、同样的参数
> (--company / --credit-code / --index),只是语义上是重新采集,一般无需带参数直接执行:
>
> bash
> python "$COLLECT" recollect
>
第 6 步:报告 PDF
状态为「成功」时,records 已经顺手把 PDF 生成好了,不必再单独跑一条命令。
仅在需要单独重出(换 reportCode、指定 requestId)时才用:
python "$COLLECT" report # requestId 自动复用上次 records 的结果
用 reportCode + requestId(授权记录返回的那个)换取报告临时下载直链,脚本同时把
PDF 下载到本地。拿到 file_path 后必须用 present_files 打开(不要只把路径贴成文本);
若落盘失败,输出里仍带 temp_url,直接打开该链接即可。
文件按企业命名:<企业名>-纳税合规报告.pdf(例:数融智创(南京)信息科技有限公司-纳税合规报告.pdf),
存放在 ~/.xiaomei-workspace/nshg-baogao/;同一企业重复生成会覆盖上一份。
企业名中的 \ / : * ? " < > | 会自动替换成 -,不会因为非法字符写盘失败。
> reportCode 是全局变量,定义在 collect.py 顶部 REPORT_CODE(当前 eEu8pMTVJKLFUtU),
> 查询记录与生成报告共用这一个值。需要换模板时改这一行,或临时用 --report-code /
> 环境变量 BAOGAO_REPORT_CODE 覆盖。
> requestId 无需手工准备:records 会自动提取并落盘,report 直接复用;也可用
> --request-id 显式指定。
命令速查
| 命令 | 作用 |
|---|---|
route |
兼容用的独立意图路由;新流程用 check --intent 一次完成路由和前置检查 |
check |
一次完成意图分流与前置检查(不发任何请求);缺少意图时返回 ask-intent |
records |
取最新一条授权记录(pageSize=1)并按状态分支;成功时直接出报告 PDF。--company 必填或复用上次 |
search |
搜索企业,返回 candidates(companyName + socialCreditCode)供用户确认主体 |
auth-url |
生成 H5 授权链接:--index N 选候选,或 --company + --credit-code 直接指定 |
recollect |
再次采集:同 auth-url 链路重新生成采集链接(状态 1/2/3 用户选重新采集、或状态 4 时用) |
flow |
records 的兼容别名,行为完全一致(早期版本由它做串联,现已合并) |
report |
单独生成报告 PDF:reportCode + requestId 换临时直链并落盘,用 present_files 打开 file_path |
state |
查看网关、凭证脱敏值、当前企业名、候选列表与已生成的授权链接 |
接口对应:collect-record-list(查记录)、search-enterprise(搜企业)、get-auth-url(取授权链接)、
report-download(生成报告 PDF)。
> 新增业务接口统一登记到 endpoints.json(一个接口一条记录),不要把路径散落在命令里。
> companyName 这类运行时必填项在登记表里留空,由用户在对话中提供,禁止臆造默认值。
错误处置
| error / next_step | 含义 | 处理 |
|---|---|---|
cred_bound=false / bind-openapi |
本地缓存里没有密钥 | 向用户索取 Key + Secret 后 bind |
ask-intent / ask=intent |
用户没说清是想看报告还是想采集 | 用 AskUserQuestion 让用户在【查看报告】【做采集】里选一个,再带 --intent 重跑 check |
ASK_COMPANY / ask-company |
凭证就绪但没有企业名称 | 向用户询问企业名称,再带企业名和原意图重跑 check |
NO_CREDENTIALS |
调用时才发现缺凭证 | 同上,走第 1 步 |
MISSING_CREDIT_CODE |
要生成授权链接但缺统一社会信用代码 | 先 search 让用户确认主体,或用 --credit-code 直接给 |
CANDIDATE_INDEX_INVALID |
--index 超出上次 search 的候选范围 |
重新 search 后再选,或改用 --company + --credit-code |
next_step=ask-wait-or-recollect / ask=wait-or-recollect |
状态 1/2/3,采集未完成 | 用 AskUserQuestion 让用户选【继续等待】或【再次采集】;选后者就执行 recollect |
next_step=recollect |
需要再次采集(状态 4,或用户选了再次采集) | 执行 recollect(同 auth-url 链路),把返回的 auth_url 完整展示给用户 |
recollect_error=MISSING_CREDIT_CODE |
要再次采集但记录与本地都没有统一社会信用代码 | 走第 4 步 search 让用户确认主体,之后直接 recollect |
recollect_error=API_BIZ_ERROR 等 |
再次采集时网关业务报错 | 按服务端 msg 如实转述,别自己重试式编造参数 |
MISSING_REQUEST_ID |
状态已成功但记录里没有 requestId |
用 --request-id 指定,或让后台核对记录字段 |
API_BIZ_ERROR + 未找到成功的企业采集记录 |
传的 requestId 在服务端无对应成功记录 | 回到第 3 步用企业名重查最新记录,拿它返回的 requestId 再出报告 |
REPORT_PARSE_FAILED |
报告接口有响应但没解析出 tempUrl |
用 openapi.py call 看原始响应核对字段名 |
SIGN_REJECTED(401/403)/ check-credential |
凭证已绑但被网关拒:后台未启用、环境串了,或签名不符 | 先让用户确认 Key 在后台已启用、creds 核对环境,再用 sign 比对签名串;重新绑定解决不了 |
PATH_NOT_READY |
该步骤的网关路径还没登记 | 补 endpoints.json |
NETWORK_ERROR |
网络/超时 | 直接向用户转述,重试一次即可 |
API_HTTP_ERROR |
其他 HTTP 错误 | 转述服务端 msg |
约束与注意事项
- 禁止回显密钥:Secret 只在绑定时经 stdin 传入,
creds输出一律脱敏;
排查签名问题用sign(本地自测,不发请求),不要为验证而向第三方重发携带真实密钥的请求。 - 主体必须用户确认:
search出来的候选,无论几条都要让用户点头确认是哪一家,
再拿它的companyName+socialCreditCode去生成授权链接;不要自己挑一条。 - 授权链接要完整给出:
auth_url是 H5 链接,必须原样展示给用户点击,
不能只写"已生成授权链接"。 - 新业务接口一律先登记到
endpoints.json再调用,不要把路径散落在对话或临时命令里。 - 禁止原样回吐响应:搜索接口单条数据有 100+ 字段(标签、坐标、商标…),脚本已裁到
最小集(企业名 / 信用代码 / 法人 / 状态 / 地址 / 成立日期),只回带response_summary。
不要把request_body/ 原始响应整段贴给用户,需要看原始报文时用openapi.py的call。 - 报告等落盘结果必须用 present_files 打开
file_path,不要只给路径或链接;
落盘失败时退化为打开temp_url。
6b.requestId/ 社会信用代码这类长数字串保持字符串,不要转成数字(会丢精度且不符契约)。
类型还原只在登记表里该字段本来就是数字时才生效(见page/pageSize/status)。 - 不要跳过
check:凭证和企业名是否齐全由脚本判定,Agent 不要凭对话印象假设;
更不要在cred_bound=true时还去问用户要密钥。 - 企业名称以用户提供的为准,不要为了凑参数编造公司名;登记表里必填项留空是刻意为之。
- 状态分支照实说:1/2/3 让用户二选一、4 直说失败、5 只出报告不加戏;
不要把「采集失败」说成「稍后就好」,也不要在报告之外自行追加解读。 - 两条链不要混走:用户说「查看报告」就走
records(先查记录,由状态决定下一步);
用户说「做采集」就直接recollect生成采集链接,不要先去查授权记录。
诉求不明先问,不要替用户选。 - 信用代码禁止跨企业复用:本地缓存的码只在归属企业 == 当前企业时可用;
拿不到就search让用户确认主体。宁可多一步,也不要生成出别家企业的采集链接。 - 本文件与 meta(
_meta.json/_skillhub_meta.json)中的 version 需保持一致(当前 0.1.1)。
环境变量
| 变量 | 用途 |
|---|---|
BAOGAO_ENV=prod |
切生产网关与生产凭证(默认 dev) |
BAOGAO_CREDS_FILE |
覆盖凭证文件路径(测试隔离用) |
BAOGAO_WORK_DIR |
覆盖状态文件目录(测试隔离用) |
BAOGAO_REPORT_CODE |
覆盖报告模板编码 reportCode(默认 eEu8pMTVJKLFUtU) |
BAOGAO_BODY_STYLE=compact |
签名仍被拒时的备选序列化风格(已实测无需启用) |
> Windows + Git Bash 下给这两个路径变量赋值时脚本会自动做 POSIX→盘符转换
> (/d/x → D:\x),不必手工改写。