3 次代码提交 5057203575 ... 76241b056f

作者 SHA1 备注 提交日期
  jackson 76241b056f docs: 制定未排舱订单导出实施计划 1 周之前
  jackson 139a87ddb9 docs: 补充未排舱筛选项权限设计 1 周之前
  jackson ac97bab7f2 docs: 设计 MCP 未排舱订单导出 1 周之前

+ 89 - 0
docs/superpowers/plans/2026-07-14-mcp-export-pending-outbound-orders.md

@@ -0,0 +1,89 @@
+# MCP Export Pending Outbound Orders Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Expose the fmsoperate add-page “导出未排舱订单” workflow through MCP, including a permission-scoped filter-options tool that resolves user-facing names to valid values.
+
+**Architecture:** Python Gateway owns tool metadata, argument shape, and HTTP forwarding only. fmsoperate owns validation, tool registry checks, session-derived identity, permission-scoped option queries, and the existing synchronous export task. Both new tools use the established MCP response and access-log boundary.
+
+**Tech Stack:** Python 3 unittest, ThinkPHP 6/PHP 7.1+, MySQL ORM, Redis-backed MCP registry, existing export queue/OSS logic.
+
+---
+
+### Task 1: Add Python tool contracts and red tests
+
+**Files:**
+- Create: `Y:/mcp/tools/export_pending_outbound_orders.py`
+- Create: `Y:/mcp/tools/list_pending_outbound_export_filter_options.py`
+- Create: `Y:/mcp/tests/test_export_pending_outbound_tools.py`
+- Modify: `Y:/mcp/app.py`
+- Modify: `Y:/mcp/public_gateway.py`
+
+- [ ] **Step 1: Write failing tests** for both tool metadata contracts, all add-page filter names, the three multi-select representations, `is_remove=0` preservation, product-type dependency forwarding, and local/public registration.
+- [ ] **Step 2: Run the focused tests** with `python -m unittest tests.test_export_pending_outbound_tools`; confirm failure because the new tool modules and registrations do not exist.
+- [ ] **Step 3: Implement minimal Python tools** with explicit schemas, whitelist-based argument forwarding, route paths, and bounded pagination.
+- [ ] **Step 4: Register both tools** in `GatewayApp` and `PublicGatewayApp`, preserving dynamic enabled-tool filtering.
+- [ ] **Step 5: Run focused tests** and commit the Python contract changes.
+
+### Task 2: Add fmsoperate tool routes, validation, and registry migrations
+
+**Files:**
+- Modify: `Y:/fmsoperate/route/mcp/mcp_route.php`
+- Modify: `Y:/fmsoperate/app/mcp/validate/McpToolValidate.php`
+- Modify: `Y:/fmsoperate/sql/mcp_add_exact_order_tools.sql`
+- Create: `Y:/fmsoperate/sql/mcp_add_pending_outbound_export_tools.sql`
+- Create: `Y:/settlement_tests/tests/Unit/FmsOperate/McpPendingOutboundExportRegisterTest.php`
+
+- [ ] **Step 1: Write failing tests** asserting both routes, validation scenes, and idempotent registry rows with the exact tool codes and route codes.
+- [ ] **Step 2: Run the focused PHP test** with `php Y:/settlement_tests/bin/phpunit.phar -c Y:/settlement_tests/phpunit.xml --filter McpPendingOutboundExportRegisterTest`; confirm the new routes/scenes are absent.
+- [ ] **Step 3: Add route closures** for `exportPendingOutboundOrders` and `listPendingOutboundExportFilterOptions`, both behind `checkMcpToken`.
+- [ ] **Step 4: Add strict validation scenes** for page-native parameters, array/scalar distinctions, `Y` property flags, date format, pagination, and reject identity/internal fields by scene allow-listing.
+- [ ] **Step 5: Add repeatable SQL registry entries** with risk level 1, POST, correct route codes, enabled status, and permission-oriented remarks.
+- [ ] **Step 6: Run the focused PHP test and PHP lint** on changed files.
+
+### Task 3: Implement permission-scoped pending-export filter options
+
+**Files:**
+- Create: `Y:/fmsoperate/app/mcp/logic/McpPendingOutboundExportFilterOptionsLogic.php`
+- Modify: `Y:/fmsoperate/app/mcp/controller/McpToolsController.php`
+- Create: `Y:/settlement_tests/tests/Unit/FmsOperate/McpPendingOutboundExportFilterOptionsLogicTest.php`
+- Create: `Y:/settlement_tests/tests/Unit/FmsOperate/McpPendingOutboundExportFilterOptionsDispatchTest.php`
+
+- [ ] **Step 1: Write failing Logic tests** for each filter type, A/B company isolation, platform-customer product restriction, `country_auth`, empty country authorization, product-type narrowing, keyword-after-authorization, and static option values.
+- [ ] **Step 2: Run focused Logic tests** and confirm expected failures before production implementation.
+- [ ] **Step 3: Implement the Logic** using session-derived identity only and the same page data sources: `OutboundTypeModel`, `ProductModel`, `OrderModel::getSortWarehouse`, `getTcAddCountryCode` plus account country auth, company-scoped importer rows, packing dictionary, operate statuses, and goods attributes.
+- [ ] **Step 4: Apply keyword and pagination only after the authorized result set is built**; never query unrestricted values before filtering.
+- [ ] **Step 5: Add the controller method through `executeNewQueryTool`** so dynamic registry checks, request IDs, response envelopes, and access logs match existing tools.
+- [ ] **Step 6: Run focused PHP tests and lint** for the new Logic/controller.
+
+### Task 4: Implement export dispatch with page-native parameters
+
+**Files:**
+- Create: `Y:/fmsoperate/app/mcp/logic/McpPendingOutboundExportLogic.php`
+- Modify: `Y:/fmsoperate/app/mcp/controller/McpToolsController.php`
+- Create: `Y:/settlement_tests/tests/Unit/FmsOperate/McpPendingOutboundExportLogicTest.php`
+- Create: `Y:/settlement_tests/tests/Unit/FmsOperate/McpPendingOutboundExportDispatchTest.php`
+
+- [ ] **Step 1: Write failing tests** for whitelist filtering, fixed `EXPORT_PEND_OUTBOUND_ORDER`, fixed `is_sync=1`, session-derived timezone/company/operator fields, URL normalization to `file_url`, missing URL failure, and permission/tool-disabled rejection.
+- [ ] **Step 2: Run focused tests** and confirm they fail before implementation.
+- [ ] **Step 3: Implement Logic** to accept only the add-page fields, preserve arrays/string formats exactly, invoke the existing async export controller/service path, and normalize the successful URL without duplicating SQL or Excel code.
+- [ ] **Step 4: Add the controller method through the shared MCP execution boundary** with the dedicated route code and validation scene.
+- [ ] **Step 5: Run focused PHP tests and lint**.
+
+### Task 5: Complete Python integration and regression coverage
+
+**Files:**
+- Modify: `Y:/mcp/tests/test_app_coverage.py`
+- Modify: `Y:/mcp/tests/test_mcp_protocol.py`
+- Modify: `Y:/mcp/tests/test_public_gateway.py`
+- Modify: `Y:/mcp/tests/test_public_gateway_unit.py`
+- Modify: `Y:/mcp/tests/test_public_server_integration.py`
+- Modify: `Y:/mcp/project-docs/requirements.md`
+- Modify: `Y:/mcp/project-docs/timeline.md`
+
+- [ ] **Step 1: Add integration assertions** that tools/list exposes both tools only when the backend registry enables them, tools/call forwards the exact route/tool headers, and business errors become `isError=true` without JSON-RPC termination.
+- [ ] **Step 2: Run all focused Python tests** with `python -m unittest discover -s tests -p 'test_*pending*outbound*.py'` and the touched integration modules.
+- [ ] **Step 3: Run the complete Python suite** with `python -m unittest discover -s tests -p 'test_*.py'`.
+- [ ] **Step 4: Run PHP MCP regression tests** including existing query/order-filter tests and all new pending-export tests.
+- [ ] **Step 5: Update project requirements/timeline** with the delivered tools, permission guarantees, and verification results.
+- [ ] **Step 6: Run `git diff --check` in both repositories and record final test evidence before completion.

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

@@ -0,0 +1,205 @@
+# 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`。
+- 新增独立工具 `list_pending_outbound_export_filter_options`,让 AI 先把用户提供的筛选名称解析成当前员工有权使用的值。
+- 筛选参数名称、业务含义、类型和多选传参方式与 `outbound/add.html` 完全一致。
+- 复用 `ExportPendOutboundOrderLogic` 和 `OutboundModel::getPendOutboundOrder()`,确保导出内容与页面一致。
+- 使用当前 MCP 员工身份、公司和数据权限,不接受调用方传入身份或权限字段。
+- 筛选项列表沿用 add 页面当前员工权限,禁止返回其他公司、其他客户或当前员工无权使用的筛选值。
+- 成功时返回可下载的文件 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。
+
+### 名称解析规则
+
+用户提供产品、仓库、国家、进口商等业务名称时,AI 必须先调用
+`list_pending_outbound_export_filter_options` 查询当前员工有权使用的选项,再把返回的 `value` 原样传给
+导出工具。不得根据名称、历史调用或示例猜测 ID。
+
+- `product_id`、`product_type_id`、`order_warehouse_id`、`importer_id` 使用筛选项返回的 ID。
+- `receiver_country` 使用国家选项返回的国家代码。
+- `packing_type` 使用货物类型选项返回的名称值;多选时以英文逗号拼接。
+- 商品属性使用选项返回的字段名,并把对应字段设置为 `Y`。
+- `status` 使用订单状态选项返回的整数值。
+
+## 架构与数据流
+
+### Python Gateway
+
+新增 `ExportPendingOutboundOrdersTool`:
+
+- 工具名为 `export_pending_outbound_orders`。
+- 路由为 `/mcp/tools/exportPendingOutboundOrders`。
+- Schema 使用上表原始参数名和中文业务备注。
+- 调用前仅做类型、枚举、长度、数组数量及日期范围格式校验。
+- 保留页面要求的原始传参结构,不在 Gateway 改写多选格式。
+- 通过现有 `ApiClient` / `ScopedApiClient` 转发,并携带工具代码和 request ID。
+
+本地 stdio 与公网 Gateway 都注册该工具;是否展示和允许调用继续由 fmsoperate 的动态工具注册表控制。
+
+### 筛选项工具
+
+新增 `ListPendingOutboundExportFilterOptionsTool`:
+
+- 工具名为 `list_pending_outbound_export_filter_options`。
+- 路由为 `/mcp/tools/listPendingOutboundExportFilterOptions`。
+- 必填 `filter_type`,可选 `keyword`、`page`、`limit`;查询物流产品时可选传
+  `product_type_id`,实现与 add 页面相同的产品分类联动。
+- `filter_type` 支持 `product_type`、`product`、`warehouse`、`country`、`importer`、
+  `packing_type`、`order_status`、`goods_attribute`、`is_remove`。
+- 返回统一的 `records`,每项包含可直接用于导出参数的 `value`、显示名称 `label` 和辅助识别字段
+  `code`;分页信息放在 `meta`。
+- 工具说明明确要求返回值只能用于对应导出字段,不得跨字段使用或猜测未返回的 ID。
+
+### 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 工具必须复用后台“新增排舱/导出未排舱订单”对应菜单权限,并继续经过
+动态工具白名单校验。不得仅依赖工具注册表绕过后台菜单权限。
+
+### 筛选项权限边界
+
+筛选项工具必须先通过动态工具白名单和后台“新增排舱/导出未排舱订单”菜单权限,再查询选项。所有查询只
+使用 MCP token 恢复出的服务端会话,禁止接收 `company_id`、`admin_id`、`is_super`、`account_type`、
+`country_auth`、`platform_company_id`、`platform_customer_id` 等外部身份或权限参数。
+
+各类筛选项必须复用 add 页面的数据来源和权限口径:
+
+| `filter_type` | add 页面数据来源与权限要求 | 返回 `value` |
+|---|---|---|
+| `product_type` | `OutboundTypeModel::getAllOutboundType()`,按当前公司和模型现有条件返回可用排舱分类 | 分类 ID |
+| `product` | `ProductModel::getProductList()`,保留当前公司或平台客户产品配置限制,以及页面对产品状态的展示条件;传 `product_type_id` 时只能在已授权产品内继续过滤 | 产品 ID |
+| `warehouse` | `OrderModel::getSortWarehouse()`,保留当前公司、账号和仓库权限排序/过滤规则 | 仓库 ID |
+| `country` | `getTcAddCountryCode()` 可用国家范围;非超级管理员且账号类型包含 `3` 时,继续按当前员工 `country_auth` 取交集;无授权返回空数组 | 国家代码 |
+| `importer` | `fms_importer.company_id=session('company_id')` 且 `status=1`,并保留页面的“由EAC提供”选项 | 进口商 ID,或 `-1` |
+| `packing_type` | `dictionary('fms_packing_type')` 当前有效字典值 | 页面提交的货物类型名称 |
+| `order_status` | `OrderModel::getOperateOrderStatus()` 中 add 页面实际展示的 `0/10/20/30/40` | 整数状态值 |
+| `goods_attribute` | `GoodsModel::getGoodsAttr()` 及页面附加的“无属性” | 对应属性字段名,如 `is_battery` 或 `no_property` |
+| `is_remove` | 页面固定选项“是/否” | 整数 `1/0` |
+
+不得为了分页或关键字搜索改成绕过上述业务方法的无权限全表查询。关键字过滤必须在授权结果集内执行;先按
+关键字查全表再补权限过滤也不允许。对于静态字典项,仍需先通过工具和菜单权限后才返回。
+
+筛选项查询只负责减少错误输入,不授予数据权限。导出接口收到筛选值后,仍由
+`OutboundModel::getPendOutboundOrder()` 和现有导出链路再次按当前员工上下文限制实际订单数据。
+
+## 返回与错误处理
+
+成功响应示例:
+
+```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 的处理。
+- 验证筛选项工具的全部 `filter_type`、产品分类联动、分页结构和名称解析引导。
+- 验证筛选项工具不接受任何身份或权限覆盖参数。
+
+### PHP
+
+- Validate 覆盖全部白名单参数及边界。
+- Controller/Logic 测试断言传给导出链路的参数与 `add.html` 实际提交结构一致。
+- 断言服务端固定导出类型、同步标记和会话身份字段,外部无法覆盖。
+- 断言员工菜单权限、动态工具状态和公司数据权限继续生效。
+- 断言成功响应返回 `file_url`,无数据和导出失败返回明确错误。
+- 使用 A/B 两个公司或客户上下文验证选项隔离,A 员工不得看到 B 公司的产品、仓库、进口商和排舱分类。
+- 验证平台客户只能看到其产品配置范围内的物流产品。
+- 验证受限头程账号只看到 `country_auth` 允许的目的国,无国家授权时返回空列表。
+- 验证 `product_type_id` 只能缩小已授权产品范围,不能通过分类联动扩大权限。
+- 验证无菜单权限、工具被停用和伪造身份字段均被拒绝并记录访问日志。
+
+## 非目标
+
+- 不修改 `outbound/add.html` 或其现有导出行为。
+- 不在 Python 中生成、保存或代理传输 Excel 文件。
+- 不新增导出模板、Excel 字段或筛选项。
+- 不改变 `OutboundModel::getPendOutboundOrder()` 的查询口径。
+- 不把同步导出改造成新的任务轮询协议。