> ## Documentation Index
> Fetch the complete documentation index at: https://docs.profy.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# 报关单智能提取

> profy-customs — 从贸易单据提取中国海关进口报关单 83 个字段，输出可校验的结构化 JSON

# 报关单智能提取（profy-customs）

把合同、发票、提单、装箱单、核注清单这类贸易单据，转成一份**字段级可追溯、可机器校验**的报关单 JSON。每个字段都带 `value` / `reason` / `confidence` / `page`——你能看到它从第几页的哪句话来的，而不是拿到一堆无从复核的结果。

<Info>
  这是垂直行业插件，不是通用 OCR。它内置了中国海关的字段定义、代码表、跨字段规则与歧义决策树；通用 OCR 只能给你文字，它给你的是**可申报的字段**。
</Info>

## 启用方式

`profy-customs` 是 `user_selectable: true` 的插件——**必须在对话的插件面板里手动勾选**，不会自动介入。

它的 `activation` 只有 `user_selectable` 一项，没有 `requires` 也没有 `conditions`：勾选后在任何沙箱模式下都可用，不依赖 Desktop、不依赖浏览器。

```json theme={null}
"activation": { "user_selectable": true }
```

## 快速开始

上传单据后直接说：

```
帮我提取这份报关单
```

或指定模式：

| 你想做的事  | 说法               | 走的路径                      |
| ------ | ---------------- | ------------------------- |
| 完整提取   | 「帮我提取这份报关单」      | Step 1 → Step 7 全流程       |
| 问字段规则  | 「成交方式怎么填」        | 只加载对应字段组说明                |
| 校验已有结果 | 「帮我校验这份 JSON」    | 直接调 `customs_validate`    |
| 代码互查   | 「一般贸易的监管方式代码是什么」 | 直接调 `customs_code_lookup` |

## 三个工具

### `customs_ocr` — 版面级文字识别

```python theme={null}
customs_ocr(
    file_url="https://.../bill-of-lading.pdf",
    pages="all",        # 或 "1-3"
)
```

调用自建 OCR 服务（RapidOCR），**不在 Agent 进程内跑 OCR**。返回每页：

| 字段                 | 含义                                           |
| ------------------ | -------------------------------------------- |
| `page`             | 页码（从 1 开始）                                   |
| `width` / `height` | 页面像素尺寸                                       |
| `texts[]`          | 每条含 `text` / `bbox([x1,y1,x2,y2])` / `score` |
| `blocks[]`         | 版面块，`type` 为 `table` / `text` / `title`      |
| `markdown`         | 整页的 markdown 表示，表格转成 md table                |

`bbox` 是这个工具存在的理由：报关单大量信息在表格里，光有文字流会丢掉「这个数字属于哪一列」。

**服务地址**由 `CUSTOMS_OCR_URL` 决定，默认 `http://profy-ocr:8001`，请求打到 `/ocr/parse`，**单次超时 120 秒**。

### `customs_code_lookup` — 海关代码表查询

```python theme={null}
customs_code_lookup(
    table="supervision-methods",
    query="一般贸易",
    direction="name_to_code",   # 或 code_to_name
    fuzzy=False,
)
```

五张标准代码表：

| `table` 取值            | 中文   |
| --------------------- | ---- |
| `supervision-methods` | 监管方式 |
| `transport-modes`     | 运输方式 |
| `trade-terms`         | 成交方式 |
| `packaging-types`     | 包装种类 |
| `insurance-modes`     | 保险方式 |

确定性查表，不让模型猜代码值。精确匹配会同时比对 `name` 与 `aliases`；`fuzzy=True` 走子串匹配，**最多返回 10 条**（防止工具输出撑爆上下文）。

### `customs_validate` — 格式与跨字段校验

```python theme={null}
customs_validate(fields_json="{...完整提取结果...}")
```

纯计算，无外部调用。返回：

```json theme={null}
{
  "passed": true,
  "errors": [],
  "cross_issues": [],
  "summary": {
    "total_fields": 83,
    "failed": 0,
    "cross_issues": 0,
    "by_severity": { "P0": 0, "P1": 0, "P2": 0, "P3": 0 }
  }
}
```

`passed` 的判定是 **P0 与 P1 均为 0**——P2/P3 不影响通过。

## 字段全集（83 个）

表头 56 个 + 表体 27 个。按组路由，处理哪组才加载哪组的规则说明：

| 组        | 字段数 | 主题             |
| -------- | --- | -------------- |
| 1+2 身份信息 | 14  | 备案、企业主体、检验检疫编码 |
| 5 运输     | 9   | 运输方式、提单号、启运港   |
| 7 成交条件   | 8   | 成交方式、运保费、杂费    |
| 8 货物规格   | 5   | 件数、包装、毛/净重     |
| 9 集装箱    | 3   | 箱号、规格、项号关系     |
| 10 目的地   | 5   | 贸易国别、目的地       |
| 11 监管    | 6   | 监管方式、征免、随附单证   |
| 12 确认事项  | 6   | 价格确认、标记唛码      |
| 表体基础     | 14  | 商品明细（每行）       |
| 表体·食品    | 7   | 食品进口特殊字段       |
| 表体·危险品   | 6   | 危险品特殊字段        |

食品组与危险品组是**条件激活**的：HS 编码监管条件含 A/B 才加载食品组，出现 MSDS 且第十四部分有危险品分类才加载危险品组。

## 校验规则全表

### 格式校验

| 字段               | 规则                            | 报错原文                           | 级别 |
| ---------------- | ----------------------------- | ------------------------------ | -- |
| 商品编号(HSCODE)     | `^\d{10}$`                    | HS编码必须为10位纯数字                  | P0 |
| 境内收发货人-18位社会信用代码 | `^[0-9A-Z]{18}$`              | 统一社会信用代码必须为18位字母数字             | P0 |
| 境内收发货人-10位海关编码   | `^\d{10}$`                    | 海关编码必须为10位纯数字                  | P0 |
| 消费使用单位-10位海关编码   | `^(\d{10}\|NO)$`              | 海关编码必须为10位纯数字或'NO'             | P1 |
| 运输方式             | `^[25]$`                      | 运输方式代码必须为2(海运)或5(空运)           | P0 |
| 净重 / 毛重          | `^\d+(\.\d+)?$`               | 净重必须为纯数字                       | P0 |
| 件数               | `^\d+$`                       | 件数必须为正整数                       | P0 |
| 进口日期 / 启运时间      | `^\d{8}$` + 真实日历校验            | 日期格式必须为YYYYMMDD 8位数字           | P1 |
| 成交方式             | `^(CIF\|FOB\|C&F\|EXW\|C&I)$` | 成交方式必须为CIF/FOB/C\&F/EXW/C\&I之一 | P1 |
| 运费方式 / 保费方式      | `^[123]$`                     | 运费方式必须为1(率)/2(单价)/3(总价)        | P1 |
| 征免性质             | `^\d{3}$`                     | 征免性质必须为3位数字代码                  | P1 |
| 备案号              | `^[A-Z]\d{11}$`               | 备案号首位大写字母+11位数字(共12位)          | P1 |
| 包装种类             | `^\d{2}$`                     | 包装种类必须为2位数字代码                  | P1 |
| 集装箱规格            | 8 种标准选项枚举                     | 集装箱规格必须是8种标准选项之一               | P2 |

日期不只看位数：年份限 1900–2099，月份 1–12，日按 `calendar.monthrange` 取该年该月真实天数，所以 `20250230` 会报「日期30超出2025年2月最大天数28」。

### 跨字段一致性

| 检查项           | 触发条件                   | 报错原文                              | 级别 |
| ------------- | ---------------------- | --------------------------------- | -- |
| 净重≤毛重         | 两者都有值且净重 > 毛重          | 净重(N)大于毛重(M)                      | P0 |
| 海运FCL必填集装箱号   | 运输方式=2 且提单号含 CY 且无箱号   | 海运整箱(CY)场景下集装箱号为必填                | P0 |
| CIF/C\&F运费留空  | 成交方式为 CIF/C\&F 但运费方式有值 | 成交方式为CIF时运费方式应留空，当前值=X            | P1 |
| CIF保费留空       | 成交方式为 CIF 但保费方式有值      | 成交方式为CIF时保费方式应留空，当前值=X            | P1 |
| 备案号首字母与监管方式匹配 | B/C 应对应加工贸易，Z 应对应一般贸易  | 备案号首字母B通常对应\['来料加工',...]，当前监管方式=X | P2 |
| 空运无集装箱        | 运输方式=5 但集装箱号有值         | 运输方式为空运(5)时集装箱号应留空                | P2 |

## 边界与失败态

<Warning>
  **`customs_validate` 不检查必填**。空值直接跳过校验并按 P3 通过——它回答的是「填了的对不对」，不是「该填的填了没」。缺字段要靠提取阶段的清点环节发现，别指望校验兜底。
</Warning>

其余边界：

* **跨字段检查只跑表头**。表体逐行只做格式校验，行间关系不检查。
* **`customs_code_lookup` 模糊搜索最多 10 条**，超出静默截断，返回的 `count` 是截断后的数量。
* **未知代码表名**返回 `{"error": "未知代码表 'X'", "available_tables": [...]}`，不会抛异常。
* **大票建议拆会话**。提取按 Phase 拆分：Phase A 做表头 56 字段，Phase B 做表体；表体 ≥8 行时建议开新会话，否则上下文会被单据原文吃满。

### 排错

| 症状                                   | 原因                               | 处理                      |
| ------------------------------------ | -------------------------------- | ----------------------- |
| `OCR 服务超时（120s），请尝试减少页数或检查服务状态`      | 多页 PDF 在 CPU 上跑满 120 秒           | 用 `pages="1-3"` 分批识别    |
| `无法连接 OCR 服务 http://profy-ocr:8001`  | OCR 服务未就绪或 `CUSTOMS_OCR_URL` 配错  | 联系管理员确认服务状态             |
| `OCR 服务返回 HTTP 4xx/5xx: ...`         | 文件 URL 不可达或格式不支持                 | 确认文件已上传成功、是 PDF/PNG/JPG |
| `代码表 'supervision-methods' 为空或文件不存在` | 代码表文件未随镜像分发到工具期望的路径              | 见下方说明                   |
| `输入 JSON 解析失败: ...`                  | 传给 `customs_validate` 的不是合法 JSON | 检查是否误传了 markdown 代码块围栏  |

<Warning>
  **已知缺陷（核对于 2026-08-11）**：`customs_code_lookup` 读取代码表的路径是插件目录下的 `data/code-tables/`，而实际代码表文件位于 `skills/customs-extraction/assets/code-tables/`。路径不一致时该工具对五张表都会返回 `代码表 'X' 为空或文件不存在`。规避方式是直接问模型（技能文档里带了常用代码），或在提取流程中依赖技能自带的映射表而非该工具。
</Warning>

## 结果去哪了

最终 JSON 会由 Core 在对话 `complete` 时写入 `message.metadata.declaration`，映射为预览契约 `{basic, fields, goods, ...}`。这是插件与产品之间的**持久化契约**——前端报关单预览面读的就是它。

## 验证你的配置

1. 勾选插件后，先做一次不带文件的代码查询：「一般贸易的监管方式代码是什么」。若返回 `代码表 ... 为空或文件不存在`，说明命中了上面的已知缺陷。
2. 传一份单页发票，说「只提取表头身份信息组」。应该看到 OCR 卡片展开、字段带 `page` 与 `confidence`。
3. 拿提取结果说「帮我校验」。应看到校验卡片给出 `by_severity` 统计——**这一步返回 `passed: true` 才算链路通**。

## 相关页面

<CardGroup cols={2}>
  <Card title="插件总览" href="/zh/documentation/plugins/overview">
    28 个内置插件的定位与激活方式
  </Card>

  <Card title="技能" href="/zh/documentation/concepts/skills">
    技能如何按需加载，为什么不一次性塞满上下文
  </Card>
</CardGroup>

<Note>
  核对日期 2026-08-11。来源：`services/agent-runtime/src/plugins/builtin/customs/plugin.json`、`tools/customs_ocr.py`、`tools/customs_code_lookup.py`、`tools/customs_validate.py`、`skills/customs-extraction/SKILL.md`。
</Note>
