| name | yzf-invoice-mcp-server-skill |
| description | AI 开票员技能——通过 MCP 工具调用后端开票服务,支持异步轮询实时获取进度。当前仅支持蓝票(正常开票/改票),暂不支持红票(红冲/作废)与批量开票。触发词:开票、开发票、专票、普票、改发票。 |
| version | 1.0.0 |
| author | YunzhangFang |
AI 开票员 (yzf-invoice-mcp-server-skill)
本 Skill 提供智能开票的完整能力,通过 MCP 工具与后端税局系统交互,支持异步轮询实时获取开票进度。
⚠️ 当前支持范围:仅支持蓝票(正常开票 / 修改发票信息)。
红票(红冲、作废、负数发票)与批量开票(一次开多张、批量导入开票)暂不支持,命中即拦截并向客户说明,不调用后端工具。详见 暂不支持场景。
可用工具
company_management — 企业开票信息校验(每次激活必调)
⚠️ 铁律:本技能被激活后,必须最先调用此工具校验当前用户的开票信息是否已维护完成,未通过校验不得进入开票主流程。
此工具无入参,调用后返回当前用户默认企业的开票信息维护状态。
返回值:
{
"code": "0",
"message": "success",
"cause": null,
"result": {
"invoice_info_filled": true,
"company_info_maintenance_url": null,
"company_info": {
"company_name": "云账房测试公司",
"taxlayer_no": "91110000123456789X"
}
}
}
{
"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 和企业填写状态,继续进入开票主流程
开票意图主流程
向后台提交开票/改票请求。对应 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 获取规则:
- 优先读取
CODEBUDDY_SESSION_ID
- 如果不存在,则使用 Python/Node 标准库生成 UUID v4(随机 UUID),并持久化到临时文件
/tmp/.wb_invoice_session_id 作为兜底,保证同一调用链内会话 ID 一致。UUID v4 基于随机数生成,每个会话独立生成,不同用户、不同会话之间绝不重复
返回值:
{"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> 标签、代码块包裹、折叠面板等任何折叠手段
⚠️ 消息不丢铁律:每一轮返回的所有 message / message_list,无论在哪条 NDJSON 行、无论 phase 是什么,必须逐条展示给客户。即使后续要继续轮询(mustContinue: true)或切换任务,也必须先展示完本轮全部消息再进入等待。
最后一条可能的值:
| phase | 含义 | 动作 |
|---|
"summary", currentRoundFinished: false, mustContinue: true | 轮次未结束 | 展示 message_list(如有)→ 继续轮询 |
"summary", currentRoundFinished: true, confirmFlag: "async_login" / "submit_invoice" | 需要切换任务 | 展示 message_list(如有)→ 有 nextTaskId 则切换并重置计时;无则用旧 taskId 继续轮询,等下轮吐出新 taskId |
"summary", currentRoundFinished: true, 无 confirmFlag | 本轮结束 | 展示 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 等) |
返回值:
{
"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:
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 工具校验开票信息是否已维护完成。这是整个流程的强制前置步骤,未通过校验不得进入开票主流程。
执行步骤:
- 立刻调用
company_management(无入参)
- 判断返回值:
result.invoice_info_filled = true → 已维护完成,直接进入下一步(意图拦截)
result.invoice_info_filled = false → 未维护完成,向客户返回 result.company_info_maintenance_url,引导用户去页面补全开票信息,结束本次技能流程(不进入第二步、不调 invoice_intent_process、不轮询)
- 若调用
company_management 失败(code != "0"),向客户说明「开票信息校验失败,请稍后重试」,结束本次技能流程
向客户展示维护页面的示例话术:
「您当前的开票信息还未完善,请先点击链接补全开票信息:[维护地址]。补全后再次发起开票即可。」
第二步:意图拦截(提交前必做)
在调用任何 MCP 工具之前,先对用户输入做意图预判,命中以下两类意图即直接拦截、不调用工具、不进入第三步:
- 红票意图(红冲 / 作废原发票 / 负数发票 / 把这张票冲一下 / 对上一张开红字 / 退货冲红 …)→ 直接回复客户:
「抱歉,目前暂不支持红票(红冲/作废)相关操作,后续版本会支持,敬请谅解。」
- 批量开票意图(一次开多张 / 批量开票 / 把这个名单/Excel 都开了 / 帮这几家公司分别开票 / 一次开 N 张 …)→ 直接回复客户:
「抱歉,目前暂不支持批量开票,请逐张提供开票信息,后续版本会支持批量开票,敬请谅解。」
拦截后结束本次技能流程,不要进入第三步,也不要轮询。意图判定细节(红票关键词、批量开票特征)查阅 @references/intent_prompt.md。
第三步:提交
立刻调用 开票意图主流程(invoice_intent_process)工具,将用户原始输入传入。
客户携带文件时的处理流程:
当客户发送了文件(图片、PDF、Excel、文档等),不要解析或提取文件内容,不要转 Base64,按以下步骤处理:
-
获取文件路径:从对话上下文中获取客户发送的文件本地路径(如 /Users/xxx/.workbuddy/blobs/xxx.jpg)
-
调 apply_storage_pre_signature_url 拿预签名地址:传入 fileName(从文件路径提取文件名),获取 uploadUrl(预签名 PUT 地址)和 publicUrl(公网访问地址)
-
跑 upload_file.py 直传文件:用 Bash 执行脚本,将文件 PUT 到 uploadUrl,文件从用户电脑直达 OBS,不经过 MCP Server:
python3 scripts/upload_file.py "文件路径" "uploadUrl" "publicUrl"
-
解析脚本输出:成功时输出 {"ok": true, "publicUrl": "...", ...},取 publicUrl
-
调用 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 种情况可以停止轮询,除此之外绝对不能停止!
- 收到
phase:"completed"
- 收到
current_round_finish_state 为真值(true 或 "1")且 无 confirmFlag(confirm_invoice_flag 为 "0" 或空)
- 总轮询时间超过 1 小时
taskRec 返回终止状态值(E200/E400/E500/E800/CANCELED)—— 注意:此时 message_list 仍需正常发送给客户
⚠️ 关键澄清(防误判):
task_rec: E100 表示"任务进行中",但不等于"必须继续轮询"!E100 时仍需检查 current_round_finish_state:若为真值且无 confirmFlag → 停止轮询。
current_round_finish_state 后端可能返回字符串 "1"/"0" 而非布尔 true/false。"1" = true(轮次结束),"0" = false(继续)。
- 判断优先级:先看
task_rec 是否终止 → 若终止则停 → 若 E100 则看 current_round_finish_state → 若为真值且无 confirmFlag 则停,否则继续。
- 详见 Decision Flow 和 taskRec 处理规则。
⛔ 绝对禁止的行为(触犯即 Bug)
在轮询循环中(从拿到 taskId 到满足上述 3 个停止条件之前),绝对禁止以下行为:
- ❌ 禁止说「请告诉我」「等我通知」「验证完成后告诉我」之类的话 —— 轮询是自动的,不需要客户触发!
- ❌ 禁止等待客户回复后再继续轮询 —— 展示完本轮消息后,立刻等待 5s 再调下一次
poll_invoice!
- ❌ 禁止因为返回了链接/验证码/登录页面就认为需要暂停 —— 链接是给客户点的,你的轮询不能停!
- ❌ 禁止输出任何暗示「我停下来等」的文字 —— 比如「我会继续轮询」「稍后查询」,这些话会让客户以为你停了!
- ❌ 禁止向客户展示 taskId —— taskId 是内部技术标识,客户不需要知道。展示消息时只输出
message / message_list 的内容,绝不输出 taskId、task_id 等技术字段!
- ❌ 禁止折叠任何需要展示给客户的信息 ——
message / message_list 中的每一条消息,必须以完整明文直接展示在对话中。禁止使用 <details> 标签、代码块包裹、折叠面板、"展开查看"等任何折叠/收起手段。图片 URL 用  内联展示,文本消息直接输出原文,让客户一眼就能看到全部内容,不需要任何额外点击或展开操作。
正确做法:展示消息 → 等 5s → 自动调用下一轮 poll_invoice → 展示消息 → 等 5s → ... 循环直到终态
⚠️ 消息不丢铁律:每一轮 poll_invoice 返回的所有 message / message_list,无论在哪条 NDJSON 行、无论 phase 是什么,必须逐条展示给客户,一条都不能丢、不能省略、不能合并。即使 mustContinue == true 需要继续轮询,也必须先把本轮全部消息展示完,再进入 5s 等待。deliveredMessages 也要同步追加,确保下一轮不重复发送。
⚠️ 客户消息展示不折叠铁律:所有需要展示给客户的信息(message / message_list 中的每一条),必须以完整内容直接展示在对话正文里,禁止任何形式的折叠。具体要求:
- 文本消息:直接将原文输出到对话中,不截断、不省略、不用
... 代替
- 图片 URL 消息(以
http 开头,结尾为 .png / .jpg / .jpeg / .gif / .webp / .bmp):必须用 Markdown 图片语法  内联展示
- 链接类消息:直接输出链接文本,不折叠、不隐藏在折叠面板里
- 禁止使用
<details> / <summary> 标签、代码块(` 或 ```)包裹消息内容、折叠面板、"点击展开"等任何需要客户额外操作才能看到内容的展示方式
- 每条消息独立展示,不合并多条为一条
伪代码流程:
# 第一步:开票信息校验(必须最先做)
companyCheck = company_management()
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 = submit_invoice(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"))
# 1) taskRec 终止状态(E200/E400/E500/E800/CANCELED)→ 停止
if taskRec in ("E200", "E400", "E500", "E800", "CANCELED"):
if taskRec == "E200":
告知客户「开票成功,请查收发票」
elif taskRec == "E400":
告知客户「开票失败,请检查信息后重试」
elif taskRec in ("E500", "CANCELED"):
告知客户「处理超时,请稍后重试」
elif taskRec == "E800":
告知客户「开票已取消」
结束 ✅
# 2) 有 confirmFlag 且带 nextTaskId → 切换任务继续轮询
if hasConfirmFlag and hasNextTask:
taskId = line["nextTaskId"]
startTime = 当前时间
deliveredMessages = []
break # 跳出 for 循环,进入 sleep(5秒) 后继续 while
# 3) 本轮结束 + 无 confirmFlag → 停止(即使 taskRec=E100 也停!)
if isRoundFinished and not hasConfirmFlag:
展示结果,结束 ✅
# 4) 本轮未结束(roundFinished=false/"0")→ 继续轮询
# 注意:E100 + roundFinished=false → 下轮继续
# 5) 无状态字段的行(纯进度行等)→ 无操作,继续处理下一行
sleep(5秒) # 等待 5 秒再轮下一次
第五步:状态判断表
⚠️ 以下表格是唯一的退出/继续判断依据,不要自己加判断!
| summary 返回 | mustContinue | 动作 |
|---|
currentRoundFinished: false | true | 必须继续轮询,等待 5s |
currentRoundFinished: true, confirmFlag 有值 | true | 有 nextTaskId 则切换并重置计时;无则用旧 taskId 继续轮询,等待 5s |
currentRoundFinished: true, 无 confirmFlag | 不存在 | 本轮结束,展示结果 ✅ 停止 |
一句话记住:技能激活 = 开票意图主流程 → 死循环 poll_invoice 展示 → 直到终态才停。中间不说话、不等客户、不停顿。
触发条件
当客户表达与发票开具相关的意图时激活本技能。 意图识别细节(意图分类、红蓝票判定与拦截、批量开票拦截、反例拦截、few-shot 示例)查阅 @references/intent_prompt.md。
强触发(激活技能)
- "开一张发票""开票""按这个开"
- "开 xxx 元给 xx 公司"
- "开 xxx 商品,普票/专票"
- "改成专票""金额改一下""增加一个发票项目"(修改发票信息,属蓝票流程)
- "不开了""先不开"(取消尚未开具的发票,属蓝票流程,需记录拒绝原因)
暂不支持场景(激活后拦截,不调工具)
以下场景会激活本技能,但在第二步被拦截,不调用 invoice_intent_process,直接向客户说明暂不支持:
- 红票 / 红冲 / 作废:红票、红字发票、红冲、冲红、发票作废、把这个票冲一下、退货冲红、负数金额开票、对上一张发票开红字 等
- 批量开票:一次开多张、批量开票、把这个名单/Excel 都开了、帮这几家公司分别开票、一次开 N 张 等
不触发
- "开账户""开户头"(开户 ≠ 开票)
- "开发""开门""开车"
- 单纯提供开户行信息无开票意图
⚠️ 意图判断职责:本技能为 MCP 工具型,意图判断由后端 invoice_intent_process 接口完成。intent_prompt.md 作为 LLM 侧的意图识别参考,用于判断是否激活本技能、是否命中暂不支持场景,以及在不激活时如何回复客户。激活后 LLM 不再做字段提取,直接把用户原话传入 userInput 参数。
异步轮询协议详解
后端响应结构
poll_invoice 对应的后端接口返回:
{
"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"/空=无 confirmFlag |
message_list | 后端消息列表(字符串数组),逐条展示给客户。所有消息以完整原文直接展示在对话正文中,禁止折叠。若某条消息为图片 URL,用  内联展示 |
task_rec | 任务终止标识(见下方)。E100=进行中(非终止),其余为终止 |
Decision Flow
⚠️ 核心原则:每一轮 poll_invoice 返回的 message / message_list 必须逐条展示给客户,无论后续是继续轮询还是终止。消息不能丢、不能省、不能合并。所有消息以完整内容直接展示在对话正文中,禁止折叠(禁止 <details> 标签、代码块包裹、"点击展开"等)。图片 URL 消息用  内联展示。
【每轮通用】先展示 message/message_list 给客户(逐条,不丢不省不合并,完整原文直接展示在对话正文中,禁止折叠;图片用  内联,不折叠),再判断状态 ↓
task_rec 为终止状态(E200/E400/E500/E800/CANCELED) → 展示 message_list,根据 taskRec 告知结果,停止轮询
task_rec 为 E100(进行中) → ⚠️ 不一定继续!必须按下方 current_round_finish_state 判断:
current_round_finish_state 为假值(false / "0") → message_list 已展示,继续轮询
current_round_finish_state 为真值(true / "1") + confirm_invoice_flag 为 "async_login" → message_list 已展示,有 nextTaskId 则切换并重置计时,无则用旧 taskId 继续轮询
current_round_finish_state 为真值(true / "1") + confirm_invoice_flag 为 "submit_invoice" → message_list 已展示,有 nextTaskId 则切换并重置计时,无则用旧 taskId 继续轮询
current_round_finish_state 为真值(true / "1") + confirm_invoice_flag 为 "0" / 空 / 其他 → message_list 已展示,⚠️ 终止轮询!
⚠️ 字段值类型说明:后端返回的 current_round_finish_state 可能是字符串 "1"/"0" 而非布尔 true/false。"1" 等价于 true(轮次结束),"0" 等价于 false(继续)。confirm_invoice_flag 为 "0" 表示无 confirmFlag。
易错示例:
返回:{"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 为真值且无 confirmFlag → 停止轮询。
| taskRec 值 | 含义 | 是否终止 | 轮询行为 |
|---|
"E100" | 进行中 | ❌ 非终止 | 需结合 current_round_finish_state 判断:真值+无confirmFlag→停;假值→继续 |
"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 次)
- 消息不丢:每一轮
poll_invoice 返回的 message / message_list,无论 phase 是什么、无论后续继续轮询还是终止,必须逐条展示给客户,不能丢、不能省、不能合并。展示完消息后再做状态判断和 5s 等待
- 客户信息不折叠:所有需要展示给客户的信息(
message / message_list),必须以完整内容直接展示在对话正文中,禁止任何形式的折叠。禁止使用 <details>/<summary> 标签、代码块(```)包裹消息、"点击展开"等需要客户额外操作才能看到内容的展示方式。文本消息直接输出原文,图片 URL 用  内联展示,让客户一眼就能看到全部内容
- 图片内联展示:如果消息内容是图片 URL(以
http 开头,结尾为 .png / .jpg / .jpeg / .gif / .webp / .bmp),必须用 Markdown 图片语法  直接展示图片,禁止折叠、禁止只输出链接让客户自己点
- taskId 不展示:taskId 是内部技术标识,绝不向客户展示。展示消息时只输出
message / message_list 的内容,不输出 taskId 等技术字段
- 跨平台兼容:macOS 和 Windows 均可正常工作
子资源
@references/intent_prompt.md — 意图识别 Prompt(LLM 用,意图分类 + 红蓝票判定与拦截 + 批量开票拦截 + 反例拦截 + few-shot 示例,不含字段提取)