Bladeren bron

docs: 设计 MCP 未排舱订单导出

jackson 1 week geleden
bovenliggende
commit
ac97bab7f2
1 gewijzigde bestanden met toevoegingen van 144 en 0 verwijderingen
  1. 144 0
      docs/superpowers/specs/2026-07-14-mcp-export-pending-outbound-orders-design.md

+ 144 - 0
docs/superpowers/specs/2026-07-14-mcp-export-pending-outbound-orders-design.md

@@ -0,0 +1,144 @@
+# MCP 导出未排舱订单设计
+
+## 背景
+
+`fmsoperate/app/admin/view/outbound/add.html` 已提供“导出未排舱订单”功能。页面把当前筛选条件提交到
+`export/asyncExportData`,固定使用 `EXPORT_PEND_OUTBOUND_ORDER`,并通过 `is_sync=1` 同步等待导出
+任务完成后取得 OSS 下载链接。
+
+本次在 MCP 中开放相同能力。MCP 不重新实现订单筛选或 Excel 生成逻辑,只复用 fmsoperate 已有的
+员工会话、数据权限、未排舱订单查询和导出任务,最终向调用方返回下载链接。
+
+## 目标
+
+- 新增 MCP 工具 `export_pending_outbound_orders`。
+- 筛选参数名称、业务含义、类型和多选传参方式与 `outbound/add.html` 完全一致。
+- 复用 `ExportPendOutboundOrderLogic` 和 `OutboundModel::getPendOutboundOrder()`,确保导出内容与页面一致。
+- 使用当前 MCP 员工身份、公司和数据权限,不接受调用方传入身份或权限字段。
+- 成功时返回可下载的文件 URL;失败时返回明确业务错误且不终止 MCP 会话。
+
+## 参数契约
+
+所有筛选参数均为可选。不传筛选条件时,行为与页面清空筛选后点击“导出未排舱订单”一致。
+
+| 页面中文名称 | MCP 参数名 | 类型与传参方式 | 业务说明 |
+|---|---|---|---|
+| 单号 | `number` | 字符串 | 订单号、参考号。可录入多个单号,以空格、英文逗号或回车分割。保持页面原始字符串格式,由现有导出逻辑调用 `format_numbers()`。 |
+| 产品分类 | `product_type_id` | 整数 | 单选产品分类 ID。 |
+| 物流产品 | `product_id` | 整数数组 | 多选物流产品 ID。与页面 `product_id` 多选提交的数组一致。 |
+| 集货仓库 | `order_warehouse_id` | 整数 | 单选集货仓库 ID。 |
+| 目的国 | `receiver_country` | 字符串 | 单选目的国国家代码,如 `US`。 |
+| 是否装柜剔除 | `is_remove` | 整数 | `1` 表示是,`0` 表示否;不传表示全部。必须保留 `0` 的有效含义。 |
+| 派送地址 | `address` | 字符串 | 按页面现有规则模糊匹配派送地址。 |
+| 入库时间 | `inbound_date` | 字符串 | 日期范围,格式为 `YYYY-MM-DD - YYYY-MM-DD`。由现有导出逻辑按员工时区转换开始和结束时间。 |
+| 订单状态 | `status` | 整数数组 | 多选订单状态。页面可选值为 `0`、`10`、`20`、`30`、`40`,默认选中 `30`、`40`;MCP 不传时不擅自补默认值,只有用户明确要求按页面默认筛选时才传 `[30, 40]`。 |
+| 商品属性 | `is_battery` | 字符串枚举 | 页面选择“带电”后提交 `is_battery="Y"`。 |
+| 商品属性 | `is_magnetic` | 字符串枚举 | 页面选择“带磁”后提交 `is_magnetic="Y"`。 |
+| 商品属性 | `is_wood` | 字符串枚举 | 页面选择“带木”后提交 `is_wood="Y"`。 |
+| 商品属性 | `is_other` | 字符串枚举 | 页面选择“其它”后提交 `is_other="Y"`。 |
+| 商品属性 | `is_fda` | 字符串枚举 | 页面选择“FDA产品”后提交 `is_fda="Y"`。 |
+| 商品属性 | `is_toy` | 字符串枚举 | 页面选择“玩具”后提交 `is_toy="Y"`。 |
+| 商品属性 | `is_ultra_limit` | 字符串枚举 | 页面选择“单箱尺寸超长超重”后提交 `is_ultra_limit="Y"`。 |
+| 商品属性 | `is_sensitive` | 字符串枚举 | 页面选择“敏感货”后提交 `is_sensitive="Y"`。 |
+| 商品属性 | `is_food` | 字符串枚举 | 页面选择“食品”后提交 `is_food="Y"`。 |
+| 商品属性 | `no_property` | 字符串枚举 | 页面选择“无属性”后提交 `no_property="Y"`。 |
+| 合并报关单号 | `merge_declare_number` | 字符串 | 精确匹配合并报关单号。 |
+| 进口商 | `importer_id` | 整数 | 单选进口商 ID。 |
+| 货物类型 | `packing_type` | 字符串 | 页面多选后以英文逗号拼接,例如 `散货,整柜`;不能改为数组。 |
+
+### 多选规则
+
+多选参数必须严格保持页面的三种不同传法:
+
+1. `product_id`、`status` 使用数组。
+2. 商品属性不传 `property_2`;每个选中属性转换为对应字段并传字符串 `Y`。
+3. `packing_type` 使用英文逗号拼接的字符串。
+
+Gateway 只接受白名单中的参数。`property_2`、`export_type`、`is_sync`、`company_id`、`operator_id`、
+`is_super`、`timezone` 等页面内部字段或身份字段不放入 MCP Schema。
+
+## 架构与数据流
+
+### Python Gateway
+
+新增 `ExportPendingOutboundOrdersTool`:
+
+- 工具名为 `export_pending_outbound_orders`。
+- 路由为 `/mcp/tools/exportPendingOutboundOrders`。
+- Schema 使用上表原始参数名和中文业务备注。
+- 调用前仅做类型、枚举、长度、数组数量及日期范围格式校验。
+- 保留页面要求的原始传参结构,不在 Gateway 改写多选格式。
+- 通过现有 `ApiClient` / `ScopedApiClient` 转发,并携带工具代码和 request ID。
+
+本地 stdio 与公网 Gateway 都注册该工具;是否展示和允许调用继续由 fmsoperate 的动态工具注册表控制。
+
+### fmsoperate
+
+新增 MCP Controller 路由、Validate 规则和专用 Logic。Controller 仅接收请求、调用 Logic 并返回统一响应。
+
+Logic 执行以下步骤:
+
+1. 从当前 MCP token 恢复的员工会话读取身份、公司、时区和权限上下文。
+2. 只提取参数白名单,拒绝未知参数和调用方提供的身份字段。
+3. 服务端固定补充 `export_type=EXPORT_PEND_OUTBOUND_ORDER`、`is_sync=1`。
+4. 复用现有导出任务创建及 `ExportPendOutboundOrderLogic`,不复制 SQL、Excel 表头或 OSS 上传逻辑。
+5. 将现有导出结果中的 `url` 标准化为 MCP 返回字段 `file_url`。
+
+为了保证与后台页面权限一致,MCP 工具必须复用后台“新增排舱/导出未排舱订单”对应菜单权限,并继续经过
+动态工具白名单校验。不得仅依赖工具注册表绕过后台菜单权限。
+
+## 返回与错误处理
+
+成功响应示例:
+
+```json
+{
+  "code": "MCP_0000",
+  "msg": "导出成功",
+  "data": {
+    "file_url": "https://example.com/fms_export/order-20260714.xlsx"
+  },
+  "meta": {
+    "request_id": "rq_xxx"
+  }
+}
+```
+
+以下情况返回业务错误:
+
+- 参数类型、枚举、数量或日期格式不合法;
+- 当前员工没有对应后台菜单权限或工具已停用;
+- 没有可导出的未排舱订单;
+- 导出任务、Excel 生成或 OSS 上传失败;
+- fmsoperate 返回成功但缺少有效下载链接。
+
+业务错误继续由现有 MCP 协议适配器转换为 `isError=true`,保留 `code`、`msg` 和 `request_id`,不产生
+JSON-RPC 协议错误,不中断会话。
+
+## 测试
+
+### Python
+
+- 工具元数据逐项校验参数名、中文备注、类型和枚举。
+- 验证 `product_id`、`status` 保持数组。
+- 验证商品属性转换后的独立 `Y` 字段,不暴露 `property_2`。
+- 验证 `packing_type` 保持英文逗号字符串。
+- 验证 `is_remove=0` 不会被当作空值删除。
+- 验证 stdio、公网工具列表、动态启用和调用转发。
+- 验证成功 URL、业务错误及缺失 URL 的处理。
+
+### PHP
+
+- Validate 覆盖全部白名单参数及边界。
+- Controller/Logic 测试断言传给导出链路的参数与 `add.html` 实际提交结构一致。
+- 断言服务端固定导出类型、同步标记和会话身份字段,外部无法覆盖。
+- 断言员工菜单权限、动态工具状态和公司数据权限继续生效。
+- 断言成功响应返回 `file_url`,无数据和导出失败返回明确错误。
+
+## 非目标
+
+- 不修改 `outbound/add.html` 或其现有导出行为。
+- 不在 Python 中生成、保存或代理传输 Excel 文件。
+- 不新增导出模板、Excel 字段或筛选项。
+- 不改变 `OutboundModel::getPendOutboundOrder()` 的查询口径。
+- 不把同步导出改造成新的任务轮询协议。