Skip to content
Back to skills

Alert Record Forms

ASecurity

备案号更新流程能力。为机构下项目的公开募捐活动备案号提供更新流程:用户先选项目 → 上传备案表截图 → LLM 视觉识别(备案表主体 + 合作方选传)→ 识别结果与所选项目不匹配时引导切换项目或重传 → 调 open_fund_raising_program_update_ui 调起前端表单页 → 用户在 UI 内直接提交 → UI 回 submit.next_step 通知 Agent 进入提交后流程(命名步骤「提交备案号到远程」)。

  • 28 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 12, 2026
toolspythongoshellbash

Works with

  • cli
  • mcp

Security analysis

A100/100

Pro scans all 9 files and shows the line behind each finding

Scanned September 12, 2026

npx -y skills add ahang1598/doubao-workbuddy-qwenwork-skills --skill alert-record-forms --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Alert Record Forms?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Alert Record Forms
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ahang1598-alert-record-forms/badge)](https://www.skillsdirectory.com/skills/ahang1598-alert-record-forms)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: alert-record-forms
description: 备案号更新流程能力。为机构下项目的公开募捐活动备案号提供更新流程:用户先选项目 → 上传备案表截图 → LLM 视觉识别(备案表主体 + 合作方选传)→ 识别结果与所选项目不匹配时引导切换项目或重传 → 调 open_fund_raising_program_update_ui 调起前端表单页 → 用户在 UI 内直接提交 → UI 回 submit.next_step 通知 Agent 进入提交后流程(命名步骤「提交备案号到远程」)。
---

# 备案号更新流程

## ⛔ 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 到脚本目录):

```bash
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`,仅用于展示,不参与提交)vs `record.list[].fund_raising_program_id`(备案号业务主键)vs `record.list[].fund_raising_program_no`(**真正的备案号编号**,用于展示 + `no` 一致性校验)vs `record.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`)

1. 加载本包内的 `alert-ocr` Skill,选择识别策略 `strategy_id: llm_vision_record`。
2. `llm_vision_record` **不是工具、脚本、函数或 MCP 名称**。禁止搜索 `llm_vision_record`、`LLM OCR`、`multimodal` 等工具,也禁止因为工具列表里没有视觉工具而宣称无法识别。
3. 从用户消息读取 `image_local_path`;对每个唯一图片路径调用一次图片读取能力。返回可视图片内容即记为 `image_loaded=true`。
4. 主表和合作方图分别载入,正常读取次数必须等于唯一图片路径数量。同一路径仅在明确读取失败且用户要求重试时允许再次读取。
5. 图片载入只表示当前模型已获得像素,**不等于 OCR 已完成**;载入成功后直接进入 3.2,不再做能力发现或切换引擎。
6. 只有图片读取明确返回文件不存在、格式不支持或无法呈现图片时,才能提示用户重传。

#### 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_id`
- `context.org_no` / `context.org_name`:首次查询脚本返回的当前机构
- `context.selected_old_no`:用户选择项目时展示的 `fund_raising_program_no`
- `quality_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)。

**进入本步骤必须同时满足**:

1. 用户已经提供有效慈善中国详情页链接(`cszg.mca.gov.cn` 域名、`csmhcdetail` 详情页);
2. 用户已在 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`](./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` 同时为空、视觉输入未通过单步脚本校验、纯链接路径没有有效慈善中国详情页查询结果。

其余问题的正确处理方式只有两种:
1. **用 `AskUserQuestion` 提醒用户,但选项集里必须包含一条能推进到 UI 的路径**(如"识别可能有误,继续下一步"),已示例见下方 Step 3.6;或
2. **直接带着已识别的数据(哪怕不完整/有疑问)调起 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`。

视觉快速路径:

```bash
python3 "<skill脚本绝对路径>/run_record_ui.py" --source vision --json-file "record_input.json" > "run_record_ui.log" 2>&1
```

视觉结果经用户明确选择使用慈善中国补齐:

```bash
python3 "<skill脚本绝对路径>/run_record_ui.py" --source vision --json-file "record_input.json" --charity-url "<详情页链接>" > "run_record_ui.log" 2>&1
```

只有链接、跳过视觉识别:

```bash
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`](./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 内完成),直接进入提交后流程:
1. **Step 5**:向用户输出成功话术(备案号已提交、进入审批流程)
2. **Step 6**:询问是否继续处理下一个备案号(或收尾)

### Step 5: 提交结果反馈

**成功话术**:
```
✅ 已更新「[项目名 A]」的备案号,进入审批流程。
审批完成前该项目无法再次修改,其他项目不受影响。
```

> ⚠️ 提交失败由 UI 页面自行提示处理(Agent 不感知、不处理失败分支);Agent 收到 `submit.next_step` 即视为提交成功,直接进入提交后流程。

`fundraising_program` 业务体字段规范见 [`references/tools/fundraising_program_input.md`](./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` 则证件暂不可处理,按任务零对应分支提示"审批完成后才能提交证件更新")
  - 用户选"结束" → 输出总结后结束本次对话
- **无证件预警** → 输出总结告知用户,结束

## 铁律

### 🟠 流程与数据

- ❌ **任何需要用户在多个处理方式间做选择的场景,用纯文本表格 / 编号列表 / “请告诉我你想怎么处理”之类的问句代替 `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`](./references/tools/fundraising_program_input.md) — ⭐ 唯一 `record_input.json` 与最终业务体契约
- [`references/tools/open_fund_raising_program_update_ui.md`](./references/tools/open_fund_raising_program_update_ui.md) — UI 顶层两字段契约
- `../alert-ocr/references/llm-vision-record.md` — schema 2.0 精简视觉输入 Prompt
- [`references/scripts/run_record_ui.py`](./references/scripts/run_record_ui.py) — ⭐ 唯一业务脚本入口,完成校验、实时守卫、可选慈善中国查询、缓存和 UI 两字段输出
- [`references/scripts/README.md`](./references/scripts/README.md) — 单步脚本状态与慈善中国字段映射说明

Files in this skill

  • SKILL.md36.8 KB
  • references/scripts/README.md5.9 KB
  • references/scripts/build_record_ui_params.py18.5 KB
  • references/scripts/fetch_charity_record.py10.4 KB
  • references/scripts/requirements.txt18 B
  • references/scripts/run_record_ui.py15.1 KB
  • references/scripts/validate_record_ocr.py11.1 KB
  • references/tools/fundraising_program_input.md5.8 KB
  • references/tools/open_fund_raising_program_update_ui.md699 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…