备案号更新流程
⛔ MCP 调用已封装为脚本(AI 禁止裸调 MCP 工具)
本专家所有 MCP 工具调用已封装为
skills/<skill>/references/scripts/下的 Python 脚本(共享客户端skills/_common/mcp_client.py)。AI 只通过execute_command运行脚本获取结果,禁止直接裸调 MCP 工具。单步脚本已内置:字段校验与归一化、项目/审批守卫、实时
old_no上下文过期校验、慈善中国链接校验、参数构造、缓存和失败上抛。普通no不一致提示由 Agent 在脚本前完成。 本地references/tools/*.md为 UI 调起入参 / 业务体输入的权威契约文档;提交在 UI 内直接完成(后端update_org_record_number接口校验),Agent 不执行提交动作。
⛔ 脚本调用与落盘约定(锚定)
本 Skill 的脚本调用与 JSON 落盘遵守编排层「全局落盘与脚本调用约定」(
agents/alert-expert.md,唯一完整定义)。要点速记:用 Bash +python3跑脚本(禁 PowerShell/cmd/bat),输出重定向到当前目录日志文件再读(禁/tmp);生成record_input.json用文件写入能力写到当前工作目录 + Read 验证;--json-file用相对路径、保持 cwd 一致(不 cd 到脚本目录)。
运行示例(Bash,相对路径,保持 cwd,不 cd 到脚本目录):
python3 "<skill脚本绝对路径>/run_record_ui.py" --source vision --json-file "record_input.json" > "run_record_ui.log" 2>&1
<skill脚本绝对路径>即skills/alert-record-forms/references/scripts目录的绝对路径(你加载本 SKILL 时已知)。运行后 Read 读run_record_ui.log(当前目录)解析状态。
概述
"备案号更新"能力,接管机构下项目的公开募捐活动备案号更新流程。
与证件更新的差异:
- OCR 走LLM 视觉(非云OCR)—— 备案表版式简单,用 prompt 约束即可
- 逐个处理(每个备案号对应一个项目)—— 不做批量收集
- 无排队机制 —— 备案号是项目级别独立更新,不受运营审核阻塞
- UI 内直接提交 —— 唯一
record_input.json经run_record_ui.py单步校验和缓存后调起 UI,用户核对/修改后在 UI 内直接完成提交;UI 回submit.next_step通知 Agent 进入提交后流程(命名步骤「提交备案号到远程」) - 显式链接查询 —— 只有用户已提供有效慈善中国详情页链接并明确选择查询时,统一入口
run_record_ui.py才会内部调用慈善中国查询模块;OCR 效果差本身不会进入查询步骤(见 Step 3.4/3.5) - 快速分流 —— 用户只提供慈善中国详情页链接未上传截图时,当轮立即弹窗让用户明确选择"查询慈善中国数据"还是"上传截图"(见 Step 2.5),不做多轮试探
触发场景
由专家在以下情境加载:
- 用户直接对话:"我要更新备案号"、"备案号到期了"、"更新公开募捐备案号"等
上下文(可能已知):
- 待更新备案号项目清单(按项目分组,含项目编号
project_no+ 项目名project_name+ 剩余天数 + 项目级fund_raising_program_id+fund_raising_program_no+fund_raising_program_audit_status(原始审核状态 1=已通过/2=审批中/3=已驳回,用于项目级守卫判定)+updatable(= audit_status != 2,审批中则 false,作为项目级守卫判据,优先用它判断能否提交)):- 通过
../alert-info-fetcher运行references/scripts/query_todo_detail.py --scope record获取,Agent 只消费返回的record.list,禁止裸调 MCP 工具、无需手动分页);下文record.list[k]即该数组的第 k 个元素 - ⚠️ 注意区分(4 个易混字段 + 守卫判据):
record.list[].project_no(项目自身编号,如224328,仅用于展示,不参与提交)vsrecord.list[].fund_raising_program_id(备案号业务主键)vsrecord.list[].fund_raising_program_no(真正的备案号编号,用于展示 +no一致性校验)vsrecord.list[].fund_raising_program_audit_status(原始审核状态 1=已通过/2=审批中/3=已驳回,用于项目级守卫判定);项目级守卫请直接用派生的record.list[].updatable(= audit_status != 2,审批中则 false)判断
- 通过
工作流程
Step 1: 展示待更新清单
核心动作:必须用 AskUserQuestion 弹出可点击列表(不得用纯文本编号列表代替),每个选项对应一个项目,label 固定格式为 [project_no][project_name] 备案号 [fund_raising_program_no](先项目编号,再项目名,再"备案号"字面量,再当前生效备案号的编号——⚠️ project_no ≠ fund_raising_program_no,二者都要展示且不能互相替代:project_no 是项目自身编号如 224328,fund_raising_program_no 才是真正的"备案号"如 52620600MJY949356HA26006),description=剩余天数/到期状态;根据 updatable(审批中则 false)做差异化标注:
updatable |
展示样式 | 可选状态 |
|---|---|---|
| true | 正常展示 | ✅ 可选 |
| false | description 标注"【审批中】备案号更新中, 请等待审批通过后再修改" |
⛔ 仍展示为选项,但点选后由 Step 2 的项目级守卫拦截 |
弹窗示例(project_no_A=项目编号如224328,fund_raising_program_no_A=备案号如52620600MJY949356HA26006):
AskUserQuestion(
questions: [{
question: "您有 [N] 项备案号即将到期, 请选择要处理的项目",
options: [
{ label: "[project_no_A][项目名A] 备案号 [fund_raising_program_no_A], 剩 5 天到期" },
{ label: "[project_no_B][项目名B] 备案号 [fund_raising_program_no_B], 【审批中】备案号更新中, 请等待审批通过后再修改" },
{ label: "[project_no_C][项目名C] 备案号 [fund_raising_program_no_C], 已过期" }
],
multiSelect: false
}]
)
⛔ 常见错误:把 project_no(项目编号)当成"备案号"直接拼进 备案号[project_no] 里展示——这是两个不同的字段,project_no 只是项目自身的编号,真正的备案号是 fund_raising_program_no,两者都要展示,不能用一个代替另一个。
若 N 超过一屏合理展示数量(如 >5),按剩余天数升序只弹前 N 项,其余可在选项末尾追加"查看更多"选项分批弹出。
说明:
- 审批中的项目照常计入到期数(N 保持原始语义),只在展示层标注差异
- 审批中的项目不消失、不隐藏,让用户看得到"确实存在但当前不能改"
Step 2: 用户选择要更新的项目
⛔ 项目级守卫:用户点选后Skill 必须先检查 record.list[k].updatable:
if record.list[k].updatable === false (AUDIT_PENDING 审批中):
# 审批中,拦截
话术: "该备案号正在更新审批中, 请等待审批通过后再修改。请选择其他项目, 或退出。"
→ 回到 Step 1 等待用户重选
→ **不进入 Step 3 上传**
else:
# updatable === true(audit_status ∈ {1, 3}),正常继续
Skill 从查询结果中定位并**在会话内保存**项目上下文:
- id = record.list[k].fund_raising_program_id
- selected_old_no = record.list[k].fund_raising_program_no
- org_no = org.org_no
- org_name = org.org_name
话术(正常分支):
好的,我们来更新「[项目名A]」(备案号 [fund_raising_program_no_A])。
请上传新的备案表截图:
- 【必传】备案表主体(含捐赠目的、募捐用途、受益人信息、募捐目标等)
- 【选传】合作方信息(若开展合作公开募捐,请一并上传第二张图)
⚠️ 续期不能改备案号编号,其他字段按实际情况填写。
Step 2.5: 用户没上传截图、只提供了慈善中国详情页链接(快速分流,避免多轮试探)
触发条件:用户在 Step 2 话术之后的下一条消息不含图片附件,内容是一条有效慈善中国详情页链接(cszg.mca.gov.cn 域名、csmhcdetail 详情页)。
⛔ 不要多轮试探、不要自己悄悄决定走哪条路:一旦识别到这个场景,立即(当轮内)调用 AskUserQuestion 让用户明确选择,而不是先反问"您是要查询还是上传"之类的纯文本追问,也不要不声不响就直接运行慈善中国查询脚本:
AskUserQuestion(
questions: [{
question: "检测到您输入的是慈善中国详情页链接而非截图, 请选择获取信息的方式",
options: [
{ label: "[查询慈善中国数据]尝试从慈善中国查询信息" },
{ label: "[上传备案表截图]手动上传截图, 由 AI 识别提取信息, 准确度更高(推荐)" }
],
multiSelect: false
}]
)
| 用户选择 | Skill 动作 |
|---|---|
| 查询慈善中国数据 | 记下并验证用户提供的链接,直接进入 Step 3.5(显式链接查询,跳过 Step 3 OCR) |
| 上传备案表截图 | 回到 Step 2 的上传话术,等待用户上传截图,走 Step 3 正常 OCR 流程 |
若用户消息同时带图片附件(哪怕也提到了链接)→ 不算命中本触发条件,直接走 Step 3 正常 OCR。
Step 3: 用户上传截图(主表必传 + 合作方选传)
3.1 图片载入(IMAGE_LOADED)
- 加载本包内的
alert-ocrSkill,选择识别策略strategy_id: llm_vision_record。 llm_vision_record不是工具、脚本、函数或 MCP 名称。禁止搜索llm_vision_record、LLM OCR、multimodal等工具,也禁止因为工具列表里没有视觉工具而宣称无法识别。- 从用户消息读取
image_local_path;对每个唯一图片路径调用一次图片读取能力。返回可视图片内容即记为image_loaded=true。 - 主表和合作方图分别载入,正常读取次数必须等于唯一图片路径数量。同一路径仅在明确读取失败且用户要求重试时允许再次读取。
- 图片载入只表示当前模型已获得像素,不等于 OCR 已完成;载入成功后直接进入 3.2,不再做能力发现或切换引擎。
- 只有图片读取明确返回文件不存在、格式不支持或无法呈现图片时,才能提示用户重传。
3.2 生成唯一输入文件(RECORD_INPUT_READY)
当前模型直接依据 ../alert-ocr/references/llm-vision-record.md 提取 20 个业务字段和精简质量信息,并将 Step 2 已保存的项目上下文合并为 schema 2.0 的 record_input.json:
context.id:所选项目fund_raising_program_idcontext.org_no/context.org_name:首次查询脚本返回的当前机构context.selected_old_no:用户选择项目时展示的fund_raising_program_noquality_warning_confirmed默认 false;confirmed_charity_org_name默认空。编号不一致由 Agent 询问,用户选择继续后不修改输入文件;慈善中国机构不一致确认仍写入当次机构名fields:主表 17 个字段 + 合作方 3 个字段quality:只含confidence、uncertain_fields、机器代码warnings
用文件写入能力把这一份 JSON 落盘到当前工作目录的相对路径 record_input.json(禁 shell 内联写文件,规则见编排层「全局落盘与脚本调用约定」)。禁止生成 raw_ocr_text、evidence、validated_record_ocr.json、project_context.json 或 UI 参数文件,不得写入 Skill 源码目录。
3.3 Agent 前置检查
在运行脚本前直接检查刚生成的结构化对象:
no和name同时为空 → 提示“未识别到备案信息,请确认上传的是备案表截图”,删除输入文件,不得调起 UI。- 仅
no或仅name缺失、confidence=low或uncertain_fields非空 → 进入 Step 3.4。 - JSON 结构化失败 → 基于已载入图片重新生成一次,不重复读取图片;再次失败则提示用户重传。
- 无质量问题 → 跳过 Step 3.4/3.5,直接进入 Step 3.6。
确定性类型、日期、合作方关系校验不再单独启动进程,由 Step 4 的 run_record_ui.py 内联执行。
Step 3.4: OCR 质量问题处理
触发条件:Step 3 校验后出现 quality.confidence=low、仅 no/name 任一缺失,或 quality.uncertain_fields 非空。
OCR 质量问题本身不是慈善中国查询的触发条件。只有用户实际提供了有效慈善中国详情页链接,并明确选择查询,才可进入 Step 3.5。
- 会话中已有有效慈善中国详情页链接:使用
AskUserQuestion提供“查询慈善中国补齐 / 重新上传截图 / 带警告继续 UI”。用户选择查询后进入 Step 3.5。 - 会话中没有有效慈善中国详情页链接:使用
AskUserQuestion提供“重新上传截图 / 提供慈善中国链接 / 带警告继续 UI”。 - 用户选择“提供慈善中国链接”时,仅提示并等待用户发送链接;收到并验证有效链接之前不得进入 Step 3.5。
- 用户选择“重新上传截图”时,先用
--cancel清理当前输入文件,再接收新图;新输入的全部确认字段必须恢复默认值。 - 用户选择“带警告继续 UI”后,将同一输入文件中的
context.quality_warning_confirmed更新为 true;不得创建第二份文件。 - 当
no与name同时为空时已在 Step 3.3 判定为非备案表,不进入本步骤,也不得提供“继续 UI”。 - 仅缺
no或仅缺name时允许继续,单步脚本在业务层转换为零值,由用户在 UI 内补录。
Step 3.5: 慈善中国详情页链接查询(显式链接路径)
⚠️ 定位:用户显式提供链接后选择的数据查询路径,不是由 OCR 失败自动触发的兜底路径。主路径仍是 Step 3(LLM 视觉 OCR)。
进入本步骤必须同时满足:
- 用户已经提供有效慈善中国详情页链接(
cszg.mca.gov.cn域名、csmhcdetail详情页); - 用户已在 Step 2.5 或 Step 3.4 明确选择“查询慈善中国数据/补齐”。
缺少任一条件都不得进入本步骤。低置信度、字段缺失、uncertain_fields 非空或 OCR envelope 中没有 org_name,均不能单独触发本步骤。机构一致性只在慈善中国实际返回非空 org_name 时执行。
单步调用方式:
本步骤不再单独运行 fetch_charity_record.py,也不生成 charity_result.json。只保存已经验证的链接,在 Step 4 调用统一脚本时增加 --charity-url <详情页链接>:
- 从 Step 2.5 进入(只有链接):先在当前工作目录创建一份最小
record_input.json,只需包含 schema 2.0、context、空fields和默认quality;Step 4 使用--source charity,慈善中国数据作为初始字段。 - 从 Step 3.4 进入(补齐视觉结果):继续使用视觉阶段的同一
record_input.json;Step 4 使用--source vision --charity-url <链接>,视觉非空字段保持,慈善中国只补齐空字段。
统一脚本在同一进程中完成链接校验、网页查询、字段映射和机构名比较。没有有效链接必须停留在 Step 2.5/3.4,禁止猜测链接或运行查询。
若脚本返回 USER_DECISION_REQUIRED:
reason=ocr_quality_warning:慈善中国补齐后仍存在低置信度或单个关键字段缺失;用户明确继续后将context.quality_warning_confirmed=true,再用同一输入文件重跑。- 慈善中国返回的
no不一致不在脚本内中断,识别值原样进入 UI,由 UI 标红并要求用户修改;用户选择查询慈善中国本身即表示继续查看查询结果。 reason=charity_org_mismatch:展示脚本返回的两个机构名,用户明确继续后将脚本返回的charity_org_name原样写入context.confirmed_charity_org_name,再用同一输入文件重跑。- 确认前脚本不会写公共缓存;用户取消时执行
python3 "<skill脚本绝对路径>/run_record_ui.py" --json-file "record_input.json" --cancel清理输入。
慈善中国输出字段与 proto 的映射:详见 references/scripts/README.md。
慈善中国查询失败处理:
| 失败类型 | Skill 行为 |
|---|---|
invalid_charity_url / charity_query_failed |
输入文件保持不变;提示检查链接、稍后重试或改传截图。用户改传截图时先用 --cancel 清理旧输入 |
RETRY_REQUIRED(网络/MCP 暂时失败) |
保留同一输入文件,只重跑脚本,不重新读图 |
慈善中国补齐后仍仅缺 no 或仅缺 name |
经用户明确选择后打开 UI 手工补齐;no 与 name 同时缺失则判定无有效备案表数据源,不调 UI |
⚠️ 慈善中国查询铁律:
- ❌ 用户未提供有效详情页链接时进入 Step 3.5 或向单步脚本传
--charity-url - ❌ 仅因 OCR 低置信度、字段缺失或不确定就自动进入 Step 3.5
- ❌ 跳过用户的明确查询选择,或跳过机构一致性比对直接进入 UI
- ❌ 用慈善中国数据覆盖 OCR 已识别的非空字段(只能补齐缺失字段)
- ❌ 查询失败时静默切换路径或不告知用户
⛔⛔ 总原则:调起 UI 前不能做"拒绝调用"的硬阻断,硬校验由 UI + 后端完成
在调起 UI(Step 4)之前,一般数据问题(非空 no 与 selected_old_no 不一致、慈善中国机构名不一致、单个字段缺失、识别可能有误等)不得演变成“拒绝调用 UI 且不给任何前进路径”的硬阻断。UI 页面用于用户核对、修正和补齐字段。
以下情况必须拒绝当前这次 UI 调起,但仍提供重选、重传或补充链接路径:项目不存在/审批中、id/org_no/selected_old_no/实时 old_no 缺失、no 与 name 同时为空、视觉输入未通过单步脚本校验、纯链接路径没有有效慈善中国详情页查询结果。
其余问题的正确处理方式只有两种:
- 用
AskUserQuestion提醒用户,但选项集里必须包含一条能推进到 UI 的路径(如"识别可能有误,继续下一步"),已示例见下方 Step 3.6;或 - 直接带着已识别的数据(哪怕不完整/有疑问)调起 UI,让用户在页面里亲自核对/修正/补齐(如未识别字段按零值传入,见下方「未识别字段的填充约定」)。
真正会"中止流程、拒绝继续"的强制校验(必填、日期格式、日期关系、长度、枚举、no 一致性、审批中守卫等)由前端 UI 页面 + 后端 update_org_record_number 接口在提交时完成(用户在 UI 内点提交时即校验)——Agent 不执行提交、也不做兜底校验。
Step 3.6:no 一致性预检(数据准备完成后、调起 UI 前)
3.6a 跨项目备案号匹配(优先检查,必须弹窗)
在比对 selected_old_no 之前,先把识别出的 fields.no 与 record.list 中每一个项目的 fund_raising_program_no 逐一比对:
- 若
no命中了另一个项目的fund_raising_program_no(即识别出的备案号属于清单里的其它项目,而非当前所选项目)→ 必须用AskUserQuestion弹出可点击选项,让用户在“切换到该项目 / 重新上传 / 取消”之间选择,严禁用纯文本表格、编号列表或让用户打字回复的方式代替弹窗:
AskUserQuestion(
questions: [{
question: "截图中的备案号([no])属于「[项目名X]」,与当前所选项目「[项目名A]」不匹配,请选择如何处理",
options: [
{ label: "[切换到该项目]使用本张截图, 处理「[项目名X]」的备案号更新" },
{ label: "[重新上传]继续处理「[项目名A]」, 重新上传正确的备案表截图" },
{ label: "[取消]结束本次备案号更新" }
],
multiSelect: false
}]
)
| 用户选择 | Skill 动作 |
|---|---|
| 切换到该项目 | 先用 --cancel 清理旧输入;会话上下文切换为命中项目(id/selected_old_no 取该项目的 fund_raising_program_id / fund_raising_program_no);基于已识别字段重新生成 record_input.json,进入 Step 4 |
| 重新上传 | 先用 --cancel 清理旧输入,回到 Step 3 重传 |
| 取消 | 用 --cancel 清理输入,结束本次更新 |
⛔ 识别出的
no命中了哪个项目,就以哪个项目为切换目标,[项目名X]必须取record.list中该项目的真实project_name,不得凭名称/序号猜测,也不得省略弹窗。
3.6b 普通编号不一致
no 未命中其它项目时,比对 record_input.json.fields.no 与 Step 2 会话中保存的 selected_old_no:
no为空但name非空 → 已在 Step 3.4 提示缺失并取得用户“带警告继续 UI”的明确选择,进入本 Step 后由 UI 要求用户补录。no === selected_old_no→ 直接进入 Step 4,无需打断用户。no非空且no !== selected_old_no→ 必须用AskUserQuestion弹出可点击选项让用户决定,选项集必须包含“继续下一步进入 UI”,不得只给“重新上传/重新选择”两个阻断性选项:
AskUserQuestion(
questions: [{
question: "截图中的备案号([no])与所选项目「[项目名]」的备案号([selected_old_no])不一致, 请选择如何继续",
options: [
{ label: "[重新选择要更新的项目]回到 Step 1清单重新选一个项目" },
{ label: "[重新上传正确的备案表截图]回到 Step 3 重传" },
{ label: "[识别可能有误,继续下一步]先按识别值进入 UI 页面, 可在页面里核对/手动修改后再提交" },
{ label: "[取消本次更新]结束本次备案号更新" }
],
multiSelect: false
}]
)
| 用户选择 | Skill 动作 |
|---|---|
| 重新选择要更新的项目 | 先用 --cancel 清理旧输入,再回到 Step 1 |
| 重新上传正确的备案表截图 | 先用 --cancel 清理旧输入,再回到 Step 3 |
| 识别可能有误,继续下一步 | 不修改 record_input.json,直接进入 Step 4;脚本保留识别 no 原值,由用户在 UI 中核对和修改 |
| 取消本次更新 | 调用单步脚本的 --cancel 清理输入文件,结束流程并清空会话内 id / selected_old_no |
Step 4: 单步构建并调起备案号更新 UI(open_fund_raising_program_update_ui)
⛔⛔ 调起 UI 前置红线:必须已选择项目并把当时展示的备案号写入
context.selected_old_no;视觉路径必须已有record_input.json,纯链接路径必须已有有效慈善中国详情页链接。no与name同时为空时不得调 UI。
4.1 运行唯一脚本
⛔ 严格按上方「脚本调用与落盘约定」执行:用 Bash +
python3、相对路径、保持 cwd(不 cd 到脚本目录)、输出重定向到当前目录日志再读。<record_input.json>用相对路径record_input.json,<run_record_ui.log>用相对路径;脚本用<skill脚本绝对路径>/run_record_ui.py。不要用 PowerShell/cmd/bat,日志严禁写到/tmp。
视觉快速路径:
python3 "<skill脚本绝对路径>/run_record_ui.py" --source vision --json-file "record_input.json" > "run_record_ui.log" 2>&1
视觉结果经用户明确选择使用慈善中国补齐:
python3 "<skill脚本绝对路径>/run_record_ui.py" --source vision --json-file "record_input.json" --charity-url "<详情页链接>" > "run_record_ui.log" 2>&1
只有链接、跳过视觉识别:
python3 "<skill脚本绝对路径>/run_record_ui.py" --source charity --json-file "record_input.json" --charity-url "<详情页链接>" > "run_record_ui.log" 2>&1
运行后用 Read 能力读取 run_record_ui.log(当前目录),解析最后一段 JSON 作为脚本状态。脚本在单一进程内执行:结构和类型校验 → 实时查询项目 → 审批守卫 → 校验实时 old_no === selected_old_no → 构建业务体 → 写公共缓存 → 通过日志返回 UI 两字段。不得再调用 validate_record_ocr.py、build_record_ui_params.py 或单独运行 fetch_charity_record.py。
4.2 处理脚本状态
PAYLOAD_BUILT:把日志文件解析出的caller_expert_id、data_cache_id保存到会话后直接调用 UI;不落 UI 参数文件、不查询工具 schema、不做二次转换。若 UI 调用失败或用户关闭后要求重开,直接复用这两个值,不重跑脚本。USER_DECISION_REQUIRED:脚本尚未写缓存。仅质量确认更新quality_warning_confirmed,机构确认写入confirmed_charity_org_name;普通编号不一致不会由脚本返回该状态。STALE_PROJECT_CONTEXT / old_no_changed:用户选择项目后后台备案号已变化;旧确认立即失效,刷新清单并重新选择,不调 UI。RETRY_REQUIRED:MCP 或网络暂时失败;保留同一输入文件,刷新凭证或稍后只重跑脚本,不重新读图。REJECTED:按reason提示用户重选、重传或修正数据,不调 UI。
脚本在 PAYLOAD_BUILT、REJECTED、STALE_PROJECT_CONTEXT、CANCELLED 时自动删除当前工作目录中的输入文件;USER_DECISION_REQUIRED、RETRY_REQUIRED 时保留同一文件。
⛔ no、selected_old_no 与实时 old_no 的红线
selected_old_no ← 用户选择项目时的 record.list[k].fund_raising_program_no
no ← 图片识别值或慈善中国结果
实时 old_no ← run_record_ui.py 按 id 重新查询的最新值
Agent 在脚本前比较视觉 no 与 selected_old_no 并完成用户提示;用户选择继续后不修改输入文件。脚本不校验普通编号确认,只实时查询 old_no 判断项目上下文是否过期:若实时值不同于 selected_old_no,返回 STALE_PROJECT_CONTEXT,不写缓存。
MUST NOT 用任何 old_no 覆盖 no。用户确认不一致后,UI 中仍保留原识别值,由 UI 的红标和提交校验处理。
UI 侧行为(由前端页面实现):
no为空 → 显示必填提示,用户补录后才能提交。no === old_no→ 正常可提交。no非空且no !== old_no→no输入框标红并阻止提交,用户改成一致后才放行。
项目上下文、来源工件与最终 fundraising_program 字段契约见 references/tools/fundraising_program_input.md;UI 调起入参契约仅含 caller_expert_id + data_cache_id 两字段,完整业务体已进入公共缓存。
调起后 Skill 的行为(对齐 alert-expert 编排层「命名步骤:提交备案号到远程」):
- ✅
open_fund_raising_program_update_ui调用返回成功后,本轮立即结束——只输出一句极简提示(如"备案号信息已提取完成,已为您打开确认页面,请在页面中确认并提交"),不再输出任何其它文字或发起任何工具调用 - ✅ 然后等待 Agent 依据
submit.next_step重新调度执行命名步骤提交备案号到远程 - ❌ MUST NOT 自行轮询 / 猜测用户是否已提交
- ❌ MUST NOT 收到
submit.next_step后再次调用任何提交接口(提交已在 UI 内完成,命名步骤「提交备案号到远程」只进入提交后流程)
命名步骤:提交备案号到远程(UI 内提交后由 Agent 依据 submit.next_step 重新调度触发,进入提交后流程)
⭐ 步骤名 MUST 逐字符为
提交备案号到远程——submit.next_step固定文案里点名这个名字,改一个字 Agent 就没法正确重新调度到本步骤。
触发语义:用户在 UI 页面点"提交"时,提交动作已由 UI 内直接完成(后端 update_org_record_number 接口在 UI 侧校验并提交)。UI 随后回一句 submit.next_step 文案给 Agent,仅作为"已提交完成"的通知信号,不携带需要 Agent 再处理的数据。
执行:收到 submit.next_step 后,MUST NOT 再调用任何提交接口(提交已在 UI 内完成),直接进入提交后流程:
- Step 5:向用户输出成功话术(备案号已提交、进入审批流程)
- Step 6:询问是否继续处理下一个备案号(或收尾)
Step 5: 提交结果反馈
成功话术:
✅ 已更新「[项目名 A]」的备案号,进入审批流程。
审批完成前该项目无法再次修改,其他项目不受影响。
⚠️ 提交失败由 UI 页面自行提示处理(Agent 不感知、不处理失败分支);Agent 收到
submit.next_step即视为提交成功,直接进入提交后流程。
fundraising_program 业务体字段规范见 references/tools/fundraising_program_input.md。参数构建前由脚本执行项目存在性、审批状态、实时 old_no、来源和机构一致性守卫;UI + 后端继续执行提交时的 no 一致性、必填、日期、长度和枚举校验。Agent 不得绕过或重复实现这些校验。
Step 6: 询问是否继续下一个
若清单里还有未处理:必须用 AskUserQuestion 弹出可点击列表(选项与 Step 1 同构:每个未处理项目一个选项 + 末尾追加"结束"选项),不得用"回复项目名/回复退出"这类纯文本问答代替:
AskUserQuestion(
questions: [{
question: "还有 [N-1] 项备案号待更新, 请选择下一个要更新的项目",
options: [
{ label: "[project_no_B][项目名B] 备案号 [fund_raising_program_no_B], 剩 12 天到期" },
{ label: "[project_no_C][项目名C] 备案号 [fund_raising_program_no_C], 【审批中】暂无法更新" },
{ label: "[结束]结束本次备案号更新" }
],
multiSelect: false
}]
)
若已全部处理完(所有备案号项目均已提交):先重新拉取一次最新数据(query_todo_summary.py),确认当前是否还有未处理的证件预警(kind === "cert" 或 kind === "both";⚠️ 该脚本只返回 kind / has_pending_review,不返回 cert_count,计数需另跑 query_todo_detail.py --scope cert):
- 仍有未处理证件预警 → 必须调用
AskUserQuestion弹出可点击二选一,让用户在"继续更新证件"与"结束"之间做选择(⛔ 严禁以"是否需要我帮你更新证件吗?"这类纯文本问句收尾,这正是此前漏弹窗的根因):AskUserQuestion( questions: [{ question: "备案号已处理完毕, 但您当前机构还有证件预警未处理, 是否现在更新证件?", options: [ { label: "[更新证件]继续协助更新到期证件" }, { label: "[结束]结束本次对话" } ], multiSelect: false }] )- 用户选"更新证件" → 交还预警专家(alert-expert)路由进入任务二:证件更新(从 Step 1 提示上传开始;⚠️ 若
has_pending_review===true则证件暂不可处理,按任务零对应分支提示"审批完成后才能提交证件更新") - 用户选"结束" → 输出总结后结束本次对话
- 用户选"更新证件" → 交还预警专家(alert-expert)路由进入任务二:证件更新(从 Step 1 提示上传开始;⚠️ 若
- 无证件预警 → 输出总结告知用户,结束
铁律
🟠 流程与数据
- ❌ 任何需要用户在多个处理方式间做选择的场景,用纯文本表格 / 编号列表 / “请告诉我你想怎么处理”之类的问句代替
AskUserQuestion弹窗(跨项目备案号匹配、no不一致、OCR 质量处理、项目选择、是否继续等所有分支都必须弹可点击选项) - ❌ 走云OCR(应用 LLM 视觉)
- ❌ 跳过 Step 2 用户选项目、直接让用户上传截图(会导致后续
no校验没有比对基准) - ❌ 跳过 UI 二次确认,直接用 OCR 原始JSON 提交(用户没机会核对/修改)
- ❌ 收到
submit.next_step后再次调用任何提交接口(提交已在 UI 内完成) - ❌ 跳过
no一致性校验(UI 前端红标主校验 + 后端接口兜底) - ❌ 在
no ≠ old_no时强行提交(续期不能改编号,由 UI 红标 + 后端接口拦截) - ❌ 一次要求用户上传多个备案号(应逐个处理)
- ❌ 用
warning_id作为update_org_record_number.id的入参(必须用record.list[].fund_raising_program_id) - ❌ 使用已废弃的
warns[].id字段路径(v2 已移除,改用项目级fund_raising_program_id) - ❌ 为
updatable === false(审批中)的项目发起更新流程或调起 UI(项目级守卫硬约束) - ❌ 展示清单时把审批中的项目隐藏不展示(应展示但标注不可选,让用户看得到状态)
- ❌ 展示清单时把审批中的项目从计数里扣减(
pending_stop_project_count保持"到期数"原始语义)
特殊情况处理
| 情况 | 处理方式 |
|---|---|
用户点选了 audit_status=2(审批中)的项目 |
Step 2 项目级守卫拦截,提示"该备案号正在更新审批中, 请等待审批通过后再修改。请选择其他项目, 或退出。" 回到 Step 1 |
| 用户上传的截图不是备案表 | no 与 name 同时为空 → 校验器返回 record_not_detected,提示“未识别到备案信息,请确认上传的是备案表截图”,不得调起 UI |
OCR 仅缺 no 或仅缺 name |
Step 3.4 提供重传/提供链接/带警告继续 UI;只有用户随后实际提供有效链接并选择查询才进入 Step 3.5,否则可选择继续后按类型填零值,由 UI 要求补录 |
OCR 识别的 no 命中了另一个项目的备案号 |
Step 3.6a 用 AskUserQuestion 弹窗三选一(切换到该项目/重新上传/取消);严禁用纯文本表格或让用户打字回复代替弹窗;用户选“切换到该项目”则切换会话上下文为该命中项目 |
OCR 识别的非空 no 与 old_no 不一致 |
Step 3.6b 用 AskUserQuestion 弹窗四选一(重选项目/重传截图/识别可能有误继续下一步/取消);选“继续下一步”则照原值传给 UI(不得覆盖)→ UI 把 no 标红并阻止提交 → 用户改成一致后放行 |
UI 回调落在新会话、会话内 selected_old_no 已丢失 |
无影响——参数已由单步脚本在调 UI 前完成实时守卫并缓存,提交在 UI 内直接完成 |
| OCR 其他字段无法识别 | 按类型填零值传给 UI(string→""/int32→0/float→0),由用户在页面里补 |
| 用户只上传了主表,没传合作方 | has_partner=0、partner_type=0、partner_name="" 传给 UI,由用户在页面确认 |
| 起止时间已过期 | 由 UI 前端 + 后端接口校验拦截,提示"该备案号已过期, 无法作为新备案号提交" |
用户在 UI 里改了no 之外的字段 |
允许,UI 内提交时透传(no仍须等于 old_no) |
| 提交成功后用户想立即再改同一项目 | 该项目已进入审批中(audit_status=2),下次列表刷新后 Step 2 守卫会拦截,符合预期 |
| 用户中途关闭 UI 页面不提交 | 不会收到命名步骤回调;会话保留本次 caller_expert_id / data_cache_id,用户说“重新打开”时直接用这两个值再次调用 UI,不重跑脚本、不重新 OCR |
| 用户中途选择"退出" | 保留已提交的更新(已生效),回归预警专家的意图询问 |
参考文件
references/tools/fundraising_program_input.md— ⭐ 唯一record_input.json与最终业务体契约references/tools/open_fund_raising_program_update_ui.md— UI 顶层两字段契约../alert-ocr/references/llm-vision-record.md— schema 2.0 精简视觉输入 Promptreferences/scripts/run_record_ui.py— ⭐ 唯一业务脚本入口,完成校验、实时守卫、可选慈善中国查询、缓存和 UI 两字段输出references/scripts/README.md— 单步脚本状态与慈善中国字段映射说明