AI 开票员技能——通过 MCP 工具调用后端开票服务,支持异步轮询实时获取进度。当前仅支持蓝票(正常开票/改票),暂不支持红票(红冲/作废)与批量开票。触发词:开票、开发票、专票、普票、改发票。
Scanned 9/8/2026
Install to Claude Code
npx -y skills add infometa/workbuddyskills --skill skills --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of yzf-invoice-mcp-server-skill?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/infometa-yzf-invoice-mcp-server-skill)More formats (shields.io, HTML) on the badges page.
---
name: yzf-invoice-mcp-server-skill
description: "AI 开票员技能——通过 MCP 工具调用后端开票服务,支持异步轮询实时获取进度。当前仅支持蓝票(正常开票/改票),暂不支持红票(红冲/作废)与批量开票。触发词:开票、开发票、专票、普票、改发票。"
version: "1.0.0"
author: "YunzhangFang"
---
# AI 开票员 (yzf-invoice-mcp-server-skill)
本 Skill 提供智能开票的完整能力,通过 MCP 工具与后端税局系统交互,支持异步轮询实时获取开票进度。
> ⚠️ **当前支持范围**:仅支持**蓝票**(正常开票 / 修改发票信息)。
> **红票**(红冲、作废、负数发票)与**批量开票**(一次开多张、批量导入开票)**暂不支持**,命中即拦截并向客户说明,不调用后端工具。详见 [暂不支持场景](#暂不支持场景)。
---
## 环境配置
使用本技能前需完成以下配置:
1. **激活 MCP Connector**:在 WorkBuddy 的「Connector 管理」页面找到 `yzf-invoice-mcp-server`,点击「信任」并激活。激活后所有 MCP 工具(`company_management` / `invoice_intent_process` / `poll_invoice` / `apply_storage_pre_signature_url`)才能正常调用。
2. **云帐房账号登录**:首次使用时,后端会引导完成云帐房账号授权登录(如尚未登录);登录状态由平台维护,后续无需重复操作。
3. **CODEBUDDY_SESSION_ID**:由平台在每次对话中自动注入为环境变量,技能内默认优先读取该环境变量;若环境中不存在,技能会通过标准库 UUID v4 自动生成兜底值,**无需手动配置**。
> ⚠️ 如果 MCP Connector 未激活,所有开票工具调用都会失败,技能无法正常使用。请务必先完成第 1 步。
---
## 可用工具
### company_management — 企业开票信息校验(**每次激活必调**)
> ⚠️ **铁律**:本技能被激活后,**必须最先调用此工具**校验当前用户的开票信息是否已维护完成,**未通过校验不得进入开票主流程**。
**参数说明**:
| 参数 | 类型 | 必填 | 说明 |
|------|------:|------|------|
| `codebuddySessionId` | string | ✅ | 当前会话 ID。按下方规则自动获取,调用时必须传入 |
| `returnUrl` | boolean | 否 | 是否返回企业信息维护页面 URL。当用户表达**修改企业信息**意图时传 `true`;正常开票校验场景不传 |
> `codebuddySessionId` 获取规则与 `invoice_intent_process` / `poll_invoice` 一致:优先读取 `CODEBUDDY_SESSION_ID`,不存在则生成 UUID v4 并持久化到系统临时目录下的 `.wb_invoice_session_id`(Python `tempfile.gettempdir()` / Node `os.tmpdir()`)。
调用后返回当前用户默认企业的开票信息维护状态。
**返回值**:
```json
// 1) 开票信息已维护完成(可直接进入开票主流程)
{
"code": "0",
"message": "success",
"cause": null,
"result": {
"invoice_info_filled": true,
"company_info_maintenance_url": null,
"company_info": {
"company_name": "云账房测试公司",
"taxlayer_no": "91110000123456789X"
}
}
}
// 2) 开票信息未维护完成(必须先引导用户去补全)
{
"code": "0",
"message": "success",
"cause": null,
"result": {
"invoice_info_filled": false,
"company_info_maintenance_url": "https://yunzhangfang.com/xxx/maintain",
"company_info": null
}
}
// 3) 修改企业信息意图(传入 returnUrl=true 时返回,引导用户修改后重新发起开票任务,而非接续之前的开票流程)
{
"code": "0",
"message": "success",
"cause": null,
"result": {
"invoice_info_filled": false,
"company_info_maintenance_url": "https://yunzhangfang.com/xxx/maintain",
"company_info": null
}
}
```
**字段说明**:
| 字段路径 | 类型 | 说明 |
|---------|------|------|
| `code` | String | 返回码,`"0"` 表示成功 |
| `message` | String | 返回信息 |
| `cause` | null / String | 错误原因(失败时非空) |
| `result.invoice_info_filled` | Boolean | **开票信息是否已维护完成**(核心判断字段) |
| `result.company_info_maintenance_url` | String | 维护页面 URL(未完成时返回,已完成时为 null 或不返回) |
| `result.company_info.company_name` | String | 企业名称 |
| `result.company_info.taxlayer_no` | String | 纳税人识别号 |
**逻辑说明**:
- `invoice_info_filled = false` → 必须返回 `company_info_maintenance_url`,引导用户去页面补全开票信息,**停止**(不进入开票主流程)
- `invoice_info_filled = true` → 不返回维护 URL,仅返回 `company_info` 和企业填写状态,**继续**进入开票主流程
- **修改企业信息意图**(传入 `returnUrl: true`)→ 返回结构与 `invoice_info_filled = false` 相同(`company_info_maintenance_url` 有值),但话术不同:引导用户去维护页面修改信息后**重新发起开票任务(而非接续之前的开票流程)**,**结束本次技能流程**
### 开票意图主流程
向后台提交开票/改票请求。对应 MCP 工具:`invoice_intent_process`。
**参数说明**:
| 参数 | 类型 | 必填 | 说明 |
|------|------:|------|------|
| `userInput` | string | ✅ | 用户原始**文字**输入。**只放客户说的话**(如「按这个文件开票」),**绝对不要把文件 URL、文件路径、Base64 等任何文件相关信息拼进去** |
| `files` | array | 否 | 客户携带的文件列表。每个元素为 `{"file_url": "", "name": ""}`。`file_url` 来自 `apply_storage_pre_signature_url` 返回的 `publicUrl`。无文件时不传此参数 |
| `createTime` | integer | 否 | 请求创建时间,毫秒时间戳。自动生成,无需手动传 |
| `invoiceType` | string | 否 | 发票类型:`"1"`=蓝票(默认,**当前唯一支持类型**);`"2"`=红票(**暂不支持,命中即拦截,不要传入**) |
| `codebuddySessionId` | string | 否 | 当前会话 ID。默认按下方规则自动获取,无需手动传 |
**codebuddySessionId 获取规则**:
1. 优先读取 `CODEBUDDY_SESSION_ID`
2. 如果不存在,则使用 Python/Node 标准库生成 **UUID v4**(随机 UUID),并持久化到系统临时目录(Python `tempfile.gettempdir()` / Node `os.tmpdir()`)下的 `.wb_invoice_session_id` 作为兜底,保证同一调用链内会话 ID 一致。UUID v4 基于随机数生成,每个会话独立生成,不同用户、不同会话之间**绝不重复**
**返回值**:
```json
// 异步场景(需要轮询)
{"phase": "submitted", "taskId": "1521216305259118849", "message": "您的开票请求已提交...", "finished": false}
// 同步场景(直接完成)
{"phase": "completed", "finished": true, "ok": true, "data": {...}}
```
**使用示例**:
- 客户说「帮云账房开一张专票」:调用 `invoice_intent_process`,传入 `userInput="帮云账房开一张专票"`
- 客户说「金额改成2元」:调用 `invoice_intent_process`,传入 `userInput="金额改成2元"`
- 客户说「不开了」:调用 `invoice_intent_process`,传入 `userInput="不开了"`
- 客户发了一张采购清单图片并说「按这个开票」:先调 `apply_storage_pre_signature_url`(传 fileName)拿到 uploadUrl + publicUrl,再跑 `python3 scripts/upload_file.py "文件路径" "uploadUrl" "publicUrl"` 上传,最后调 `invoice_intent_process`,传入 `userInput="按这个开票"`, `files=[{"file_url": publicUrl, "name": "采购清单.png"}]`
### poll_invoice — 轮询开票进度
对已提交的任务进行单次轮询查询。每次调用只做一次 HTTP 请求并立即返回本轮状态。
**参数说明**:
| 参数 | 类型 | 必填 | 说明 |
|------|------:|------|------|
| `taskId` | string | ✅ | `开票意图主流程`(invoice_intent_process)返回的 `taskId` |
| `codebuddySessionId` | string | 否 | 当前会话 ID。与提交阶段保持一致;默认按上方规则自动获取 |
| `createTime` | integer | 否 | 本次轮询请求创建时间,毫秒时间戳。自动生成,无需手动传 |
| `deliveredMessages` | string[] | 否 | 上一轮已展示给客户的消息列表(用于消息交付确认) |
**返回值**:每次调用输出 NDJSON(多行 JSON),最后一行为状态摘要行:
```
# 中间消息(0 到多条,逐条展示给客户,不能丢)
{"phase":"progress", "taskId":"xxx", "message":"正在核对发票信息...", "finished":false}
# 最后一条 — 状态摘要(同样可能携带 message_list,必须展示)
# ⚠️ 若 message/message_list 中某条为图片 URL → 用  内联展示,不要折叠、不要只给链接
# ⚠️ 所有文本消息必须以完整原文直接展示在对话正文中,禁止用 <details> 标签、代码块包裹、折叠面板等任何折叠手段
```
> ⚠️ 严格遵守 [展示铁律](#展示铁律消息不丢--不折叠),消息不丢、不折叠。
**最后一条可能的值**:
| phase | 含义 | 动作 |
|-------|------|------|
| `"summary"`, `current_round_finish_state: false`, `must_continue: true` | 轮次未结束 | **展示 message_list(如有)→ 继续轮询** |
| `"summary"`, `current_round_finish_state: true`, `confirm_invoice_flag: "async_login"` / `"submit_invoice"` | 需要切换任务 | **展示 message_list(如有)→** 有 nextTaskId 则切换并重置计时;**无则用旧 taskId 继续轮询**,等下轮吐出新 taskId |
| `"summary"`, `current_round_finish_state: true`, 无 `confirm_invoice_flag` | 本轮结束 | **展示 message_list(如有)→** 展示结果,**停止** ✅ |
| `"completed"`, `finished: true` | 完全结束 | **展示 message_list(如有)→** 展示最终结果,**停止** ✅ |
| `"completed"`, `taskTerminated: true` | 任务终止(E200/E400/E500/E800/CANCELED) | **先展示 message_list 给客户**,再根据不同 taskRec 告知结果,**停止** ✅ |
| `"error"` | 网络错误 | **展示 message_list(如有)→** 输出错误,继续轮询 ❌ |
**使用示例**:
- 拿到 `taskId` 后循环调用:`poll_invoice(taskId="1521216305259118849", deliveredMessages=[])`
- 每次间隔 **5 秒**
### apply_storage_pre_signature_url — 获取文件上传预签名 URL
在调用 `invoice_intent_process` **之前**,如果客户发送了文件(图片、PDF、Excel 等),需要先将文件上传到 OBS 云存储获取可访问的 URL。上传分两步:**此工具是第一步**,只传文件名不传文件内容,拿到预签名上传地址;第二步用 `@scripts/upload_file.py` 脚本直传文件。
> ⚠️ **此工具不接收文件内容**,只签发上传地址。文件流不经过 MCP Server,直接从用户电脑传到 OBS。
**参数说明**:
| 参数 | 类型 | 必填 | 说明 |
|------|------:|------|------|
| `fileName` | string | ✅ | 文件名,含扩展名(如 `a.pdf`、`001.jpg`) |
| `fileSize` | integer | 否 | 文件大小(字节) |
| `fileType` | string | 否 | 文件类型(`pdf`、`png`、`jpg`、`xlsx` 等) |
**返回值**:
```json
{
"uploadUrl": "https://obs.xxx.com/bucket/path/a.pdf?X-Amz-Signature=...",
"publicUrl": "https://obs.xxx.com/bucket/path/a.pdf",
"objectKey": "bucket/path/a.pdf",
"fileName": "a.pdf",
"fileType": "pdf",
"expiresInSeconds": 600
}
```
| 字段 | 说明 |
|------|------|
| `uploadUrl` | 预签名 PUT 地址,有效期 10 分钟,用 `curl -X PUT --upload-file` 上传 |
| `publicUrl` | 上传成功后的公网访问地址,传给 `invoice_intent_process` 的 `files` |
| `expiresInSeconds` | 预签名有效期(600 秒 = 10 分钟) |
### upload_file.py — 本地上传脚本(第二步)
拿到 `uploadUrl` 后,用 Bash 执行此脚本,将文件直传到 OBS:
```bash
python3 scripts/upload_file.py <本地文件绝对路径> <uploadUrl> <publicUrl>
```
- 脚本内部用 `curl -X PUT --upload-file` 上传,不经过 MCP Server
- 失败自动指数退避重试(最多 3 次)
- 成功输出:`{"ok": true, "publicUrl": "...", "fileName": "...", "fileSize": ...}`
- 失败输出:`{"ok": false, "error": "..."}`
- PUT 时不携带鉴权 header(预签名 URL 自带授权)
**文件限制**:支持 PDF、PNG、JPG/JPEG、GIF、WEBP、XLSX、XLS;单文件 ≤ 100MB;一次只处理一个文件。
---
## ⚠️ 核心执行规则(最高优先级)
**当本技能被激活时,你必须无条件执行以下流程,不得跳过、不得追问、不得自行处理:**
### 第一步:开票信息校验(**每次激活最先调用**)
> ⚠️ **铁律**:本技能被激活时,**必须最先调用 `company_management` 工具**校验开票信息是否已维护完成。这是整个流程的强制前置步骤,**未通过校验不得进入开票主流程**。
**执行步骤**:
1. **判断用户意图**:
- 若用户表达**修改企业信息**意图(如"修改企业信息""改一下公司信息""更新开票资料""修改销方的相关信息""修改销方抬头信息""销方信息我要更新一下""公司的开票资料不对,帮我处理一下""我要修改销方的基本信息和登录信息"等涉及销方/企业开票信息变更的表达)→ 调用 `company_management`,传入 `codebuddySessionId` + `returnUrl: true`,拿到 `company_info_maintenance_url` 后,引导用户去页面修改信息并**重新发起开票任务(而非接续之前的开票流程)**,**结束本次技能流程**(不进入第二步)
- 其他开票意图 → 调用 `company_management`,仅传入 `codebuddySessionId`(不传 `returnUrl`)
2. **判断返回值**:
- `result.invoice_info_filled = true` → 已维护完成,**直接进入下一步(意图拦截)**
- `result.invoice_info_filled = false` → 未维护完成,向客户返回 `result.company_info_maintenance_url`,引导用户去页面补全开票信息,**结束本次技能流程**(不进入第二步、不调 `invoice_intent_process`、不轮询)
3. 若调用 `company_management` 失败(`code != "0"`),向客户说明「开票信息校验失败,请稍后重试」,**结束本次技能流程**
**向客户展示维护页面的示例话术**:
> 「您当前的开票信息还未完善,请先点击链接补全开票信息:[维护地址]。补全后再次发起开票即可。」
**修改企业信息意图的示例话术**:
> 「请点击链接修改您的企业开票信息:[维护地址]。修改完成后请重新发起开票任务,不要继续之前的开票流程。」
### 第二步:意图拦截(提交前必做)
在调用任何 MCP 工具之前,先对用户输入做意图预判,命中以下两类意图即**直接拦截、不调用工具、不进入第三步**:
1. **红票意图**(红冲 / 作废原发票 / 负数发票 / 把这张票冲一下 / 对上一张开红字 / 退货冲红 …)→ 直接回复客户:
> 「抱歉,目前暂不支持红票(红冲/作废)相关操作,后续版本会支持,敬请谅解。」
2. **批量开票意图**(一次开多张 / 批量开票 / 把这个名单/Excel 都开了 / 帮这几家公司分别开票 / 一次开 N 张 …)→ 直接回复客户:
> 「抱歉,目前暂不支持批量开票,请逐张提供开票信息,后续版本会支持批量开票,敬请谅解。」
> 拦截后**结束本次技能流程**,不要进入第三步,也不要轮询。意图判定细节(红票关键词、批量开票特征)查阅 `@references/intent_prompt.md`。
### 第三步:提交
**立刻调用 `开票意图主流程`(invoice_intent_process)工具**,将用户原始输入传入。
**客户携带文件时的处理流程**:
当客户发送了文件(图片、PDF、Excel、文档等),**不要解析或提取文件内容,不要转 Base64**,按以下步骤处理:
1. **获取文件路径**:从对话上下文中获取客户发送的文件本地路径(WorkBuddy 平台自动提供,无需手动拼接)
2. **调 `apply_storage_pre_signature_url` 拿预签名地址**:传入 `fileName`(从文件路径提取文件名),获取 `uploadUrl`(预签名 PUT 地址)和 `publicUrl`(公网访问地址)
3. **跑 `upload_file.py` 直传文件**:用 Bash 执行脚本,将文件 PUT 到 `uploadUrl`,文件从用户电脑直达 OBS,不经过 MCP Server:
```bash
python3 scripts/upload_file.py "文件路径" "uploadUrl" "publicUrl"
```
4. **解析脚本输出**:成功时输出 `{"ok": true, "publicUrl": "...", ...}`,取 `publicUrl`
5. **调用 `invoice_intent_process`**:把 `publicUrl` 传入 `files` 参数
```
客户发文件 → 取文件路径 → apply_storage_pre_signature_url(拿预签名URL)→ upload_file.py(curl直传OBS)→ 拿到publicUrl → invoice_intent_process
```
> ⚠️ **`userInput` 和 `files` 严格分离,绝对不要混在一起**:
> - `userInput` = 客户说的**文字**(如「按这个文件开票」),原样传入,不添加任何文件信息
> - `files` = 文件 URL 列表,`file_url` 来自 `publicUrl`,**不要把 URL 拼到 `userInput` 里**
>
> ❌ 错误:`userInput = "按这个文件开票,文件链接:https://obs.xxx.com/..."`
> ✅ 正确:`userInput = "按这个文件开票"`, `files = [{"file_url": "https://obs.xxx.com/...", "name": "采购清单.pdf"}]`
>
> ⚠️ 多个文件时,逐个执行步骤 2-4(每个文件单独拿预签名地址 + 上传),合并所有 `publicUrl` 到一个 `files` 数组,再传给 `invoice_intent_process`。
>
> ⚠️ 预签名 URL 有效期 10 分钟。如果脚本报告 URL 过期,重新调 `apply_storage_pre_signature_url` 拿新地址再上传,最多重来 1 次。
>
> ⚠️ 上传完成前(脚本未返回 `"ok": true` 前),**不要调用 `invoice_intent_process`**,必须先拿到 `publicUrl`。
解析返回值:
- `phase:"submitted"` → 立即展示 `message` 给客户,**记录 `taskId`**
- `phase:"completed"` → 同步结果,直接展示,**结束**
- 无 taskId → 同步结果,**结束**
### 第四步:轮询获取消息(有 taskId 时)
**⚠️ 铁律:只有以下 4 种情况可以停止轮询,除此之外绝对不能停止!**
1. 收到 `phase:"completed"`
2. 收到 `current_round_finish_state` 为真值(`true` 或 `"1"`)且 **无 confirm_invoice_flag**(`confirm_invoice_flag` 为 `"0"` 或空)
3. 总轮询时间超过 **1 小时**
4. `taskRec` 返回终止状态值(E200/E400/E500/E800/CANCELED)—— **注意:此时 `message_list` 仍需正常发送给客户**
> **⚠️ 关键澄清(防误判)**:
> - `task_rec: E100` 表示"任务进行中",**但不等于"必须继续轮询"**!E100 时仍需检查 `current_round_finish_state`:若为真值且无 confirm_invoice_flag → **停止轮询**。
> - `current_round_finish_state` 后端可能返回字符串 `"1"`/`"0"` 而非布尔 `true`/`false`。`"1"` = true(轮次结束),`"0"` = false(继续)。
> - **判断优先级**:先看 `task_rec` 是否终止 → 若终止则停 → 若 E100 则看 `current_round_finish_state` → 若为真值且无 confirm_invoice_flag 则停,否则继续。
> - 详见 [Decision Flow](#decision-flow) 和 [taskRec 处理规则](#taskrec-处理规则)。
### ⛔ 绝对禁止的行为(触犯即 Bug)
在轮询循环中(从拿到 taskId 到满足上述 3 个停止条件之前),**绝对禁止**以下行为:
- ❌ **禁止说「请告诉我」「等我通知」「验证完成后告诉我」之类的话** —— 轮询是自动的,不需要客户触发!
- ❌ **禁止等待客户回复后再继续轮询** —— 展示完本轮消息后,立刻等待 5s 再调下一次 `poll_invoice`!
- ❌ **禁止因为返回了链接/验证码/登录页面就认为需要暂停** —— 链接是给客户点的,你的轮询不能停!
- ❌ **禁止输出任何暗示「我停下来等」的文字** —— 比如「我会继续轮询」「稍后查询」,这些话会让客户以为你停了!
- ❌ **禁止向客户展示 taskId** —— taskId 是内部技术标识,客户不需要知道。展示消息时只输出 `message` / `message_list` 的内容,绝不输出 taskId、task_id 等技术字段!
- ❌ **禁止折叠任何需要展示给客户的信息** —— `message` / `message_list` 中的每一条消息,必须以完整明文直接展示在对话中。禁止使用 `<details>` 标签、代码块包裹、折叠面板、"展开查看"等任何折叠/收起手段。图片 URL 用 `` 内联展示,文本消息直接输出原文,让客户一眼就能看到全部内容,不需要任何额外点击或展开操作。
**正确做法:展示消息 → 等 5s → 自动调用下一轮 `poll_invoice` → 展示消息 → 等 5s → ... 循环直到终态**
### 展示铁律(消息不丢 + 不折叠)
> **最高优先级规则,覆盖所有轮询阶段。**
1. **消息不丢**:每一轮 `poll_invoice` 返回的所有 `message` / `message_list`,无论在哪条 NDJSON 行、无论 `phase` 是什么,**必须逐条展示给客户,一条都不能丢、不能省略、不能合并**。即使需要继续轮询,也**必须先把本轮全部消息展示完**再进入 5s 等待。`deliveredMessages` 也要同步追加,确保下一轮不重复发送。
2. **信息不折叠**:所有需要展示给客户的信息,**必须以完整内容直接展示在对话正文中,禁止任何形式的折叠**:
- 文本消息:直接将原文输出到对话中,不截断、不省略
- 图片 URL(以 `http` 开头,结尾为 `.png` / `.jpg` / `.jpeg` / `.gif` / `.webp` / `.bmp`):必须用 Markdown 图片语法 `` 内联展示
- 链接类消息:直接输出链接文本
- 禁止使用 `<details>` / `<summary>` 标签、代码块包裹消息、"点击展开"等任何需要客户额外操作的手段
- 每条消息独立展示,不合并多条为一条
**伪代码流程**:
```
# 第一步:开票信息校验(必须最先做)
# 判断是否为修改企业信息意图
if 用户表达修改企业信息意图:
companyCheck = company_management(codebuddySessionId=会话ID, returnUrl=true)
if companyCheck.code != "0":
告知客户"开票信息校验失败,请稍后重试",结束
# 返回 company_info_maintenance_url,引导用户修改后重新发起开票任务(非接续之前流程)
告知客户"请点击链接修改企业开票信息:[维护地址]。修改完成后请重新发起开票任务,不要继续之前的开票流程",结束
else:
companyCheck = company_management(codebuddySessionId=会话ID)
if companyCheck.code != "0":
告知客户"开票信息校验失败,请稍后重试",结束
if companyCheck.result.invoice_info_filled == false:
告知客户"开票信息未完善,请先点击 [维护地址] 补全后再次发起开票",结束
# 第二步:意图拦截(已由 references/intent_prompt.md 完成判断)
# 如果命中红票/批量开票 → 直接拦截,不进入主流程
# 如果客户发了文件,预签名URL直传OBS(不走MCP传文件内容,不转Base64)
if 客户发送了文件:
fileList = []
for file_path in 客户发送的文件列表:
# 第一步:调 MCP 工具拿预签名地址(只传文件名,不传文件内容)
presign = apply_storage_pre_signature_url(fileName=文件名)
uploadUrl = presign.uploadUrl
publicUrl = presign.publicUrl
# 第二步:Bash 执行脚本,curl 直传文件到 OBS
# python3 scripts/upload_file.py "文件路径" "uploadUrl" "publicUrl"
# 输出:{"ok": true, "publicUrl": "...", ...}
uploadResult = 脚本输出
fileList.append({"file_url": publicUrl, "name": 文件名})
else:
fileList = None
result = invoice_intent_process(userInput="用户原话", files=fileList) # 开票意图主流程
if result.phase == "submitted":
taskId = result.taskId
startTime = 当前时间
deliveredMessages = []
while True:
if 当前时间 - startTime > 1小时:
告知客户"处理超时,请稍后重试",结束
result = poll_invoice(
taskId=taskId,
codebuddySessionId=提交阶段相同会话ID,
createTime=当前毫秒时间戳,
deliveredMessages=deliveredMessages
)
# 逐行处理 NDJSON 输出
for line in result.lines:
# ⚠️ 严格遵守展示铁律,所有消息逐条完整展示
#
# NDJSON 每行可能是以下几类之一(不要依赖 phase 字段做分支,直接读实际字段):
# 1) 进度行:{"message":"...", "phase":"progress", ...}
# 2) 状态摘要行:{"current_round_finish_state":"1/0", "confirm_invoice_flag":"0/async_login/submit_invoice",
# "task_rec":"E100/E200/...", "message_list":[...]}
# 3) 终态行:{"finished":true, "ok":true, "message_list":[...], "task_rec":"E200/E400/...", ...}
# 收集本行所有待展示消息
pendingMessages = []
if line.get("message"): # 单条消息(进度行)
pendingMessages.append(line["message"])
if line.get("message_list") and len(line["message_list"]) > 0: # 消息列表(状态摘要/终态行)
pendingMessages.extend(line["message_list"])
for msg in pendingMessages:
# ⚠️ 所有消息必须完整展示给客户,禁止折叠、禁止用代码块包裹
# ⚠️ 图片 URL 直接用 Markdown 内联展示,不要折叠、不要只给链接
if msg 匹配图片URL正则 (以 http 开头,结尾为 .png/.jpg/.jpeg/.gif/.webp/.bmp):
用 Markdown 图片语法展示:
else:
# 文本消息直接输出原文到对话正文,不截断、不省略、不用 <details> 或代码块折叠
展示 msg 文本给客户(完整原文,禁止折叠)
deliveredMessages.append(msg)
# ── 状态判断:直接读后端字段,不依赖 phase ──
#
# ⚠️ 字段名以下划线为准(后端实际字段),值可能是字符串 "1"/"0"
roundFinishedRaw = line.get("current_round_finish_state") # "1"=true, "0"=false
confirmFlagRaw = line.get("confirm_invoice_flag") # "0"/""=无, "async_login"/"submit_invoice"=有
taskRec = line.get("task_rec", "") # E100/E200/E400/E500/E800/CANCELED
isRoundFinished = roundFinishedRaw in (True, "1", 1)
hasConfirmFlag = confirmFlagRaw in ("async_login", "submit_invoice")
hasNextTask = bool(line.get("nextTaskId"))
# 按 [状态判断表](#第五步状态判断表唯一退出继续依据) 优先级 ①→②→③ 判断:
# ① taskRec 终止 → 按编码告知结果后停止
# ② confirm_invoice_flag + nextTaskId → 切换 taskId/重置计时/继续
# ③ roundFinished 真且无 confirm_invoice_flag → 停止(即使 taskRec=E100);否则继续
sleep(5秒) # 等待 5 秒再轮下一次
```
### 第五步:状态判断表(唯一退出/继续依据)
> **⚠️ 按优先级①→②→③从上往下判断,命中即执行。不要自己加判断!**
| 优先级 | 条件 | 动作 |
|:---:|------|------|
| ① | `task_rec` = E200/E400/E500/E800/CANCELED(终止) | 展示 message_list → 按 [taskRec 处理规则](#taskrec-处理规则) 的客户提示语告知结果 → **停止** ✅ |
| ② | `confirm_invoice_flag` = async_login/submit_invoice,**有** `nextTaskId` | 展示 message_list → 切换 taskId + 重置计时 → **继续轮询** |
| ② | `confirm_invoice_flag` = async_login/submit_invoice,**无** `nextTaskId` | 展示 message_list → 用旧 taskId → **继续轮询** |
| ③ | `current_round_finish_state` = false/"0" | 展示 message_list → **继续轮询** |
| ③ | `current_round_finish_state` = true/"1",`confirm_invoice_flag` = "0"/空 | 展示 message_list → 展示结果 → **停止** ✅ |
> **⚠️ E100 特别注意**:`task_rec=E100` 只是"进行中",非终止。必须继续向下判断 ②③。常见误判:看到 E100 就以为"继续",但若 `current_round_finish_state` 已为真值且无 confirm_invoice_flag → 按优先级③停止!
**一句话记住:技能激活 = 开票意图主流程 → 死循环 poll_invoice 展示 → 直到终态才停。中间不说话、不等客户、不停顿。**
---
## 触发条件
**当客户表达与发票开具相关的意图时激活本技能。** 意图识别细节(意图分类、红蓝票判定与拦截、批量开票拦截、反例拦截、few-shot 示例)查阅 `@references/intent_prompt.md`。
### 强触发(激活技能)
- "开一张发票""开票""按这个开"
- "开 xxx 元给 xx 公司"
- "开 xxx 商品,普票/专票"
- "改成专票""金额改一下""增加一个发票项目"(修改发票信息,属蓝票流程)
- "不开了""先不开"(取消尚未开具的发票,属蓝票流程,需记录拒绝原因)
- "修改企业信息""改一下公司信息""更新开票资料""修改销方的相关信息""修改销方抬头信息""销方信息我要更新一下""公司的开票资料不对,帮我处理一下""我要修改销方的基本信息和登录信息"(修改企业/销方开票信息,调 `company_management` 传 `returnUrl: true`)
### 暂不支持场景(激活后拦截,不调工具)
> 以下场景**会激活本技能**,但在第二步被拦截,**不调用 `invoice_intent_process`**,直接向客户说明暂不支持:
- **红票 / 红冲 / 作废**:红票、红字发票、红冲、冲红、发票作废、把这个票冲一下、退货冲红、负数金额开票、对上一张发票开红字 等
- **批量开票**:一次开多张、批量开票、把这个名单/Excel 都开了、帮这几家公司分别开票、一次开 N 张 等
### 不触发
- "开账户""开户头"(开户 ≠ 开票)
- "开发""开门""开车"
- 单纯提供开户行信息无开票意图
> ⚠️ **意图判断职责**:本技能为 MCP 工具型,意图判断由后端 `invoice_intent_process` 接口完成。`intent_prompt.md` 作为 LLM 侧的意图识别参考,用于判断是否激活本技能、是否命中暂不支持场景,以及在不激活时如何回复客户。激活后 LLM 不再做字段提取,直接把用户原话传入 `userInput` 参数。
---
## 异步轮询协议详解
### 后端响应结构
`poll_invoice` 对应的后端接口返回:
```json
{
"code": "0",
"result": {
"current_round_finish_state": true/false,
"confirm_invoice_flag": "0" / "async_login" / "submit_invoice",
"message_list": ["消息1", "消息2"],
"task_rec": "E200" / "E101" / ...
}
}
```
| 字段 | 含义 |
|------|------|
| `current_round_finish_state` | `true`/`"1"`=当前轮次结束,`false`/`"0"`=继续。⚠️ 后端可能返回字符串 `"1"`/`"0"` |
| `confirm_invoice_flag` | `"async_login"`=异步登录;`"submit_invoice"`=提交开票;`"0"`/空=无 confirm_invoice_flag |
| `message_list` | 后端消息列表(字符串数组),逐条展示给客户。所有消息以完整原文直接展示在对话正文中,禁止折叠。若某条消息为图片 URL,用 `` 内联展示 |
| `task_rec` | 任务终止标识(见下方)。E100=进行中(非终止),其余为终止 |
### Decision Flow
> ⚠️ 严格遵守 [展示铁律](#展示铁律消息不丢--不折叠),每轮先展示全部消息再判断状态。
```
【每轮通用】展示消息 → 按 [状态判断表](#第五步状态判断表唯一退出继续依据) 优先级 ①→②→③ 判断 → 停 / 继续轮询
```
> **⚠️ 字段值类型说明**:后端返回的 `current_round_finish_state` 可能是字符串 `"1"`/`"0"` 而非布尔 `true`/`false`。`"1"` 等价于 true(轮次结束),`"0"` 等价于 false(继续)。`confirm_invoice_flag` 为 `"0"` 表示无 confirm_invoice_flag。
**易错示例**:
```
返回:{"current_round_finish_state": "1", "confirm_invoice_flag": "0", "task_rec": "E100", "message_list": ["当前轮次已处理完成"]}
分析:task_rec=E100 → 非终止,继续判断 → current_round_finish_state="1"(真值) + confirm_invoice_flag="0"(无) → ✅ 终止轮询
⚠️ 常见误判:看到 E100 就认为"进行中→继续轮询",忽略了 current_round_finish_state 已经为真值!
```
### taskRec 处理规则
`taskRec` 是 **后端开票任务的终止标识**。E100 为唯一非终止状态;其余值(E200/E400/E500/E800/CANCELED)均为终止状态,触发轮询停止。**但即使为终止状态,`message_list` 仍需正常发送给客户。**
> **⚠️ E100 ≠ 继续轮询**:E100 只表示"任务未终止",是否继续轮询还需看 `current_round_finish_state`。若 `current_round_finish_state` 为真值且无 confirm_invoice_flag → 停止轮询。
| taskRec 值 | 含义 | 是否终止 | 客户提示语 | 轮询行为 |
|-----------|------|---------|-----------|---------|
| `"E100"` | 进行中 | ❌ 非终止 | — | **需结合 `current_round_finish_state` 判断**:真值+无 confirm_invoice_flag→停;假值→继续 |
| `"E200"` | 成功 | ✅ 终止 | 「开票成功,请查收发票」 | 正常发送 message_list 给客户,**停止轮询** |
| `"E400"` | 失败 | ✅ 终止 | 「开票失败,请检查信息后重试」 | 正常发送 message_list 给客户,**停止轮询** |
| `"E500"` | 超时 | ✅ 终止 | 「处理超时,请稍后重试」 | 正常发送 message_list 给客户,**停止轮询** |
| `"E800"` | 取消 | ✅ 终止 | 「开票已取消」 | 正常发送 message_list 给客户,**停止轮询** |
| `"CANCELED"` | 超时 | ✅ 终止 | 「处理超时,请稍后重试」 | 正常发送 message_list 给客户,**停止轮询** |
### 消息交付确认(deliveredMessages)
每轮 `poll_invoice` 调用需携带上一轮已发送的消息列表:
- 第一轮:不传或传空数组
- 后续每轮:携带**上一轮**所有 `phase:"progress"` 的消息
- 后端据此判断消息是否已成功交付
---
## 注意事项
- **轮询是自动的**:拿到 `taskId` 后 AI 必须自动循环调 `poll_invoice`,不需要客户任何操作
- **链接不影响轮询**:如果返回了验证链接,链接是给客户点的,AI 的轮询不能因此暂停
- **超时兜底**:总轮询超过 1 小时仍未结束时,告知客户「处理超时,请稍后重试」
- **多阶段跳转**:遇到 `async_login` 或 `submit_invoice` 标志时,自动切换新 `taskId` 继续轮询(最多跳转 5 次)
- **消息展示**:严格遵守 [展示铁律](#展示铁律消息不丢--不折叠),消息不丢、不折叠、图片用 `` 内联
- **taskId 不展示**:taskId 是内部技术标识,**绝不向客户展示**。展示消息时只输出 `message` / `message_list` 的内容,不输出 taskId 等技术字段
- **跨平台兼容**:macOS 和 Windows 均可正常工作
---
## 子资源
- `@references/intent_prompt.md` — 意图识别 Prompt(LLM 用,意图分类 + 红蓝票判定与拦截 + 批量开票拦截 + 反例拦截 + few-shot 示例,不含字段提取)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!