2026-07-22-mcp-async-export-task-design.md 13 KB

MCP 异步导出任务设计

背景

export_pending_outbound_ordersexport_out_of_province_port_data 当前都在一次 MCP tools/call 内同步完成查询、文件生成、上传并返回下载链接。未排舱导出固定 is_sync=1;省外进港资料同步调用 Support 模板导出。数据量较大时整个请求可能 持续一分钟以上。

MCP Gateway 调用 fmsoperate 的默认 HTTP 超时为 10 秒,外层反向代理和 Workbuddy 也可能提前关闭长连接。单纯增加超时无法保证整条链路稳定,而且会长期占用 Gateway 并发名额和 PHP 请求进程。

本设计将两个导出工具改成短请求提交任务,并新增独立状态查询工具。文件生成继续复用 fmsoperate 现有导出算法、Redis 队列、st_download_list 和文件存储,不把业务逻辑 迁移到 Gateway。

本设计替代 2026-07-14-mcp-export-pending-outbound-orders-design.md 中关于同步等待、直接返回 文件 URL 和“不改造成任务轮询协议”的结论;原文件只作为当时设计记录保留。

目标

  • 两个导出提交请求在完成参数、权限和数据存在性校验后快速返回,不等待文件生成。
  • 新增 query_export_task,通过新的短 MCP 调用查询等待、执行、完成或失败状态。
  • 完成状态返回与当前合同相同的安全文件链接。
  • 复用现有导出实现,不复制 Excel 字段、模板、业务查询或 OSS 上传算法。
  • 保持工具注册、菜单权限、公司隔离和员工数据范围校验。
  • 防止客户端重试造成同一员工短时间内重复创建相同导出任务。
  • 不暴露 st_download_list.id、内部异常、队列载荷或原始失败备注。

非目标

  • 不通过一个 MCP 调用在 Gateway 内持续轮询。
  • 不使用流式响应、SSE 或 WebSocket 传输导出进度。
  • 不通过提高 Gateway、Nginx 或 Workbuddy 超时解决长任务。
  • 不修改后台页面现有导出交互。
  • 不改变两个导出的筛选条件、号码匹配、文件内容或模板。
  • 不新增业务导出任务表;继续使用 st_download_list

方案选择

采用:全异步提交和独立查询

两个导出工具一律创建任务并返回任务引用,调用方稍后单独调用 query_export_task。该方案使每次 HTTP 请求都保持短时、可重试,并且不依赖数据量 阈值或调用端连接时长。

不采用:按数据量混合同步与异步

导出耗时不仅取决于行数,还取决于关联查询、模板服务、文件写入和上传。预估阈值不稳定, 会让同一个工具出现两套时序和输出分支,增加调用方及测试复杂度。

不采用:只增加超时

Gateway、PHP、反向代理和 Workbuddy 任一层仍可能先超时。长请求还会占用每工具并发 配额,超时重试可能重复生成文件,因此只可作为临时排障手段。

对外工具合同

导出提交工具

保留两个现有工具名及输入 Schema:

  • export_pending_outbound_orders
  • export_out_of_province_port_data

成功提交后统一返回:

{
  "code": "MCP_0000",
  "msg": "导出任务已提交",
  "data": {
    "task_ref": "mexp_xxx",
    "status": "queued",
    "retry_after_seconds": 10
  },
  "meta": {
    "request_id": "rq_xxx"
  }
}

task_ref 是带完整性校验的不可枚举引用,不是数据库主键。retry_after_seconds 只是调用建议,不代表精确完成时间,也不允许 Gateway 在原请求内等待。

提交阶段仍完成以下同步检查:

  1. MCP Token、动态工具注册和 route_code。
  2. 对应后台菜单权限。
  3. 参数格式和组合关系。
  4. 当前公司和员工数据范围。
  5. 未排舱导出的单行数据存在性探测,或省外进港资料的全部号码解析和授权。

因此参数错误、无权限、目标不存在或无可导出数据仍立即返回现有安全业务错误,不创建 队列任务。

query_export_task

新增工具:

  • 工具名:query_export_task
  • 路由:POST /mcp/tools/queryExportTask
  • 必填参数:task_ref,字符串,最长 512 字符
  • 每次只查询一个任务,不接受任务 ID、员工 ID、公司 ID 或批量数组

返回状态:

内部状态 对外状态 对外数据
st_download_list.status=1 queued task_ref、建议重试秒数
status=4 running task_ref、建议重试秒数
status=2 completed task_reffiles[].label/url
status=3 failed task_ref、固定安全失败提示

任务不存在、引用签名无效、任务不属于当前员工或公司、来源工具权限失效时,统一返回 “导出任务不可用”,不区分具体原因,避免枚举和越权探测。不得返回 st_download_list.remark

query_export_task 自身先经过动态注册校验。Logic 再根据任务中记录的来源工具执行该 导出的菜单权限检查;只有任务所属员工、公司和来源权限均仍有效时才返回状态或文件链接。

Gateway 展示

两个提交工具的 Presenter 白名单改为:

  • task_ref
  • status=queued
  • retry_after_seconds

query_export_task 的 Presenter 只接受四个已知状态。完成时复用现有导出 URL 和主机 白名单校验;其他状态不得携带文件链接。未知字段、未知状态、畸形 URL 或未知错误继续 关闭失败。

工具说明明确:

  • 导出工具只负责提交任务。
  • 收到 queued 后应告知用户任务已提交。
  • 用户要求查看结果或经过建议等待时间后,才调用 query_export_task
  • 不得在一次工具调用内睡眠、循环轮询或并行创建相同任务。

CLI 保留 fmsoperate 原始 code/msg/data/meta 信封。

任务引用

task_ref 使用 mexp_ 前缀和 URL-safe Base64 编码,载荷至少包含:

  • task_id
  • create_user_id
  • company_id
  • source_tool
  • issued_at

载荷使用从 fmsoperate 专用 MCP_EXPORT.TASK_REF_SECRET 分别派生的 AES-256-CBC 加密密钥和 HMAC-SHA256 认证密钥,按 encrypt-then-MAC 生成不暴露内部 ID 的引用。密钥少于 32 字节、为空、解密失败或引用验签失败时 关闭访问。验签使用恒定时间比较;解析失败不得记录完整引用。引用有效期固定为 7 天, 超过有效期后要求用户重新提交导出。

签名只防止引用伪造,不替代数据库授权。查询时仍必须按以下条件读取:

  • id=task_id
  • create_user_id=当前员工
  • system_id=当前系统
  • export_params.company_id=当前公司
  • export_params.mcp_source_tool=source_tool

日志只记录任务数据库 ID 或引用短哈希中的一种内部关联值,不记录完整 task_ref

fmsoperate 数据流

未排舱订单

  1. McpPendingOutboundExportLogic 保留当前权限检查、参数归一化和单行探测。
  2. 服务端固定写入 export_type=EXPORT_PEND_OUTBOUND_ORDERis_sync=0、 当前员工和公司上下文及 mcp_source_tool
  3. 创建 st_download_list 等待任务。
  4. 通过现有 RedisAction::EXPORT_PEND_OUTBOUND_ORDERExportPendOutboundOrder Handler 入队。
  5. Worker 复用 ExportPendOutboundOrderLogic,按现有约定将状态从等待更新为执行中、 完成或失败。
  6. 提交接口返回由任务 ID 和可信会话生成的 task_ref

省外进港资料

  1. McpOutOfProvincePortExportLogic 保留四类号码严格四选一、全部匹配和全部授权。
  2. 把已授权排舱 ID、资料类型、可信员工/公司上下文及 mcp_source_tool 写入任务参数。
  3. 创建 st_download_list 等待任务并推送新的专用 Redis Action/Handler。
  4. Worker 把任务标记为执行中,调用从现有 ExportLogic::shangHaiExport() 抽出的可复用 模板导出方法。
  5. 新的内部方法接受既有 task_id 作为 down_record_id,不得再次创建第二条 st_download_list 记录;后台现有 shangHaiExport() 对外行为保持不变。
  6. Worker 根据模板响应更新同一任务的文件 URL、文件名、完成时间或安全失败状态。

省外进港 Worker 只接收提交阶段已经全部授权并固定下来的排舱 ID,不重新根据原始号码做 模糊匹配。状态查询和链接交付阶段再次验证当前员工、公司及来源菜单权限。

队列失败

创建任务与入队不能组成单个数据库事务,因此采用补偿:

  • 任务插入失败:不入队,返回系统繁忙。
  • 入队返回失败或抛出异常:把任务标记为失败并返回系统繁忙。
  • Worker 异常:先把任务标记为失败,再重新抛给队列框架记录队列 Action、任务 ID 和 内部异常;提交与查询的 request ID 继续由 st_mcp_access_log 保留,公共响应不返回异常。
  • Worker 重试必须以任务状态为门禁,完成任务不得重复生成文件。

去重与并发

提交前基于以下内容生成稳定 SHA-256 指纹:

  • 当前公司 ID
  • 当前员工 ID
  • 来源工具
  • 归一化后的业务参数

使用 Redis 原子 SET NX EX 建立 60 秒提交锁,并把已创建的任务 ID 保存为值。同一指纹 在锁有效期内再次提交时返回同一个 task_ref,不重复插入或入队。入队失败时释放锁。

去重只覆盖客户端超时或快速重试,不缓存导出结果,不跨员工复用文件,也不改变用户在 60 秒后主动再次导出的能力。

安全边界

  • 身份、公司、员工、超级管理员、时区和数据范围只来自 MCP Token 恢复的 Session。
  • 外部请求不能提交 task_idcompany_idoperator_idis_super 或队列参数。
  • 导出提交时做完整业务授权;状态查询和链接交付时再次检查任务归属和来源菜单权限。
  • 任务引用验签、Redis、数据库或菜单查询失败时关闭访问。
  • Presenter 只输出固定状态、建议等待时间和安全文件链接。
  • 不返回队列名、内部 Action、数据库主键、导出参数、失败备注、SQL 或异常栈。
  • st_mcp_access_log 继续记录每次提交和查询的独立 request ID 与安全响应。

注册与部署

新增 query_export_task 后,本地和公网 Gateway 候选工具由 12 个变为 13 个。需要同步:

  • Python 工具 metadata、双注册表、CLI 转发和 Presenter。
  • fmsoperate 路由、Validate、Controller、Logic、任务查询 Model。
  • fmsoperate 动态工具注册 SQL,首次插入默认 status=0,重复执行保留管理员现有状态。
  • 新省外进港导出 Redis Action、配置和 Handler。
  • README 与集中仓当前需求、技术规格、用户流程和里程碑。

部署顺序:

  1. 部署 fmsoperate 代码及持续运行的 fmsoperate:inside Worker。
  2. 执行并核验新工具注册 SQL,由管理员决定是否启用。
  3. 部署并重启 MCP Gateway。
  4. 让 Workbuddy 重新连接;tools.listChanged=false 不会热更新 Schema。
  5. 分别用小数据和超过一分钟的数据验证提交、状态查询及下载。

不执行新的业务表 DDL。队列 Worker、Redis 和文件存储不可用时不得启用新合同。

测试与验收

fmsoperate

  • 两个提交 Logic 验证参数、菜单、公司和数据范围后快速返回任务引用。
  • 未排舱无数据、省外号码缺失/越权时不创建任务。
  • 外部身份字段、任务 ID 和队列字段不能覆盖服务端值。
  • 入队失败会把任务置为失败且不留下可再次执行的等待任务。
  • Worker 状态完整覆盖 queued -> running -> completed/failed
  • 省外进港异步导出只创建一条下载任务并复用原模板结果。
  • 状态查询覆盖有效引用、篡改、过期、跨员工、跨公司、来源工具权限失效和任务不存在。
  • 失败响应不包含 remark、异常或内部 ID。
  • 相同员工和参数 60 秒内重试返回同一任务;不同员工或公司不共享。

MCP Gateway

  • stdio 和公网均注册 13 个同序工具,并受动态启用列表控制。
  • 两个导出工具只接受规范的 queued 响应。
  • 状态查询 Presenter 覆盖四种状态及完成文件链接。
  • 非完成状态携带 URL、未知状态、未知字段和非法 URL 时关闭失败。
  • CLI 原样保留新的后端信封。
  • 全量 unittest、语句和分支覆盖率继续为 100%。

时序验收

  • 提交接口不等待实际文件生成,在正常数据库和 Redis 条件下应在短请求时间内返回。
  • 模拟导出耗时超过 60 秒时,原提交 MCP 请求仍已正常结束。
  • 导出过程中多次状态查询不会重复入队或生成文件。
  • 完成后新的状态查询返回可下载文件,失败后返回固定安全提示。

回滚

  • 先在动态注册表中关闭 query_export_task 和两个导出工具,阻止产生新合同调用。
  • 已入队任务允许 Worker 完成;不要删除等待或执行中的 st_download_list 记录。
  • 回滚 Gateway 和 fmsoperate 代码后重启 Gateway,并让客户端重新连接。
  • 不依赖回滚 SQL;注册表状态由管理员保留,代码回滚期间关闭相关工具即可。