# 真实 MES HTTP 适配器(config-driven) > round-45 方向 II · 验收矩阵 73/75 推进(round-46 方向 LL:对接就绪检查 readiness) > 模块:`server/integrations/mes_http.py`(`integ-mes-http`,可复用 ✔) ## 目标 - **矩阵 73**(「对接至少一个真实 MES 测试环境;鉴权、幂等、超时、重试、回执和补偿通过」): 真实工厂需外部环境,本轮以 **config-driven HTTP MES 适配器框架** 落地——`HttpMesClient` 可指向任意 base URL 的真实 MES,用本地 HTTP stub 黄金测试覆盖鉴权/幂等/超时/重试/回执。 - **矩阵 75**(「真实 MES/WMS 适配器(现场对接)」):适配器已注册进 **MCP 总线** (plugin `mes.http`,工具 `mes.http_dispatch` 等 + power 门禁),现场对接只需配置。 ## 与 Mock stub 的切换 - **默认 = 进程内 Mock**(`server/integrations/mes_stub.py`,`MockMesClient`),不依赖任何网络。 - **显式配置 `MES_HTTP_BASE_URL` 后 = HTTP 适配器**:`server/aps_domain/mes.py` 的 `_get_active_client()` 按环境变量选择客户端,接口完全一致(`status / create_work_order / post_report / cancel_work_order`),`mes.dispatch` P3 门禁与 `mes.cancel_dispatch` saga 补偿路径不受影响。 ```bash # 启用 HTTP 适配器(现场/联调环境) export MES_HTTP_BASE_URL=https://mes.example.com/api # 切回本地 Mock(默认):不设置 MES_HTTP_BASE_URL 即可 ``` ## 环境变量 | 变量 | 默认 | 说明 | |------|------|------| | `MES_HTTP_BASE_URL` | (空 = fail-closed) | 真实 MES / 本地 stub 的根地址,如 `http://127.0.0.1:8765` | | `MES_HTTP_TOKEN` | 空 | 鉴权 token;配合 `MES_HTTP_TOKEN_HEADER` 使用 | | `MES_HTTP_TOKEN_HEADER` | `Authorization` | token 所在头;`Authorization` 自动补 `Bearer ` 前缀 | | `MES_HTTP_TIMEOUT_SECONDS` | `10` | 单次请求超时(秒) | | `MES_HTTP_MAX_RETRIES` | `2` | 指数退避重试次数(连接失败/超时/5xx/429;4xx 不重试) | | `MES_HTTP_IDEM_HEADER` | `X-Idem-Key` | 幂等键所在头(也可放 body.idemKey) | > token 仅由 `HttpMesClient` 从环境/构造参数读取,**不写入** `plugins.json`(MCP manifest > 的 `auth` 字段保持为空)。 ## 端点约定(适配器 → MES) | 操作 | 方法/路径 | 说明 | |------|-----------|------| | 健康/连接 | `GET /health` | 返回 `{ok, system, plant, woCount, ...}` | | 下发(幂等) | `POST /work-orders` | body 携带工单数据 + `idemKey`;重复 idemKey 返回 `{duplicate:true, externalWo}` | | 状态查询 | `GET /work-orders/{externalWoId}` | 外部工单状态轮询 | | 报工回执 | `POST /work-orders/{externalWoId}/reports` | 回流 `progressPct/qtyDone/status` | | 撤单(幂等) | `POST /work-orders/{externalWoId}/cancel` | saga 补偿;重复撤单返回 `duplicate:true` | ## 失败语义(供 saga 补偿) `MesHttpError{code, status_code}` 显式抛出,`mes.cancel_dispatch` 会把 HTTP 失败记为 `failed`(`":"`)而不是静默跳过: | code | 触发 | |------|------| | `MES_HTTP_NOT_CONFIGURED` | 未配置 `MES_HTTP_BASE_URL`(fail-closed) | | `MES_HTTP_CONNECT_FAILED` / `MES_HTTP_TIMEOUT` | 连接失败 / 超时(重试耗尽后) | | `MES_HTTP_AUTH_FAILED` | 401/403 鉴权失败 | | `MES_HTTP_UPSTREAM_ERROR` | 5xx/429(重试耗尽后) | | `MES_HTTP_NOT_FOUND` / `MES_HTTP_REJECTED` / `MES_HTTP_INVALID_RESPONSE` | 404 / 其他拒绝 / 响应不合法 | ## MCP 总线注册(矩阵 75) - 总线启动即注册 plugin `mes.http`(manifest + 工具契约 + power 门禁,fail-closed): | 工具 | 权力 | 幂等 | 说明 | |------|------|------|------| | `mes.http_dispatch` | P3 | ✔ | 真实 MES 下发(需显式放行权限) | | `mes.http_status` | P0 | ✔ | 连接/外部工单状态(只读) | | `mes.http_report` | P2 | ✘ | 报工回执 | | `mes.http_cancel` | P2 | ✔ | 撤单(saga 补偿) | - P2/P3 写工具默认 deny,调用前需 `set_permission(plugin="mes.http", tool=..., allow=True)`; 未配置 `MES_HTTP_BASE_URL` 时任何工具调用抛 `MesHttpError MES_HTTP_NOT_CONFIGURED`(fail-closed)。 ## 对接就绪检查(readiness,矩阵 73 现场对接准备) `HttpMesClient.readiness(probe=False)` 返回**稳定 JSON 字段**(前端可直接展示,不抛错): | 字段 | 类型 | 说明 | |------|------|------| | `configured` | bool | 是否已配置 `MES_HTTP_BASE_URL`(fail-closed) | | `baseUrl` | string/null | 当前 base URL;未配置为 null | | `tokenPresent` | bool | 是否已配置 `MES_HTTP_TOKEN` | | `timeoutSec` | number | 单次请求超时(秒) | | `maxRetries` | int | 重试次数 | | `connectivity` | `"unknown"`/`"ok"`/`"failed"`/null | 未配置 null;配置未探测 unknown;探测后 ok/failed | | `lastError` | `{code,message,statusCode}`/null | 探测失败时记录最近一次错误,成功/未探测为 null | | `message` | string | 中文提示(如「未配置 MES_HTTP_BASE_URL」「连通正常」) | - `readiness()`(不探测):只返回配置视图,`connectivity="unknown"`,不发网络请求。 - `readiness(probe=True)`:对 `base_url` 发轻量 `GET /health`,短超时(`min(timeout, 3s)`); 失败**不抛错**,记录 `connectivity="failed"` + `lastError`。 - 未配置时 `configured=false`,`connectivity=null`,message 明确提示(网关 200 不 500)。 ### 网关端点 | 方法/路径 | 权力 | 说明 | |----------|------|------| | `GET /api/integrations/mes/readiness` | P0 只读 | 返回 `readiness()` 配置视图 | | `POST /api/integrations/mes/readiness/probe` | P1 | 触发一次连通性探测(GET /health 短超时) | ```bash curl -s http://127.0.0.1:8003/api/integrations/mes/readiness # {"configured":false,"baseUrl":null,"tokenPresent":false,"timeoutSec":10.0, # "maxRetries":2,"connectivity":null,"lastError":null,"message":"未配置 MES_HTTP_BASE_URL:..."} ``` ## 现场对接步骤 1. 获取真实 MES 的 base URL 与 token,配置 `MES_HTTP_BASE_URL` / `MES_HTTP_TOKEN`。 2. 对齐端点:若真实 MES 路径不同,改写 `HttpMesClient` 各方法中的 path 常量即可(框架已隔离)。 3. 校验:`GET /api/integrations/mes/readiness` 返回 `configured=true`, `POST /api/integrations/mes/readiness/probe` 返回 `connectivity="ok"`; `mes_connection_status()` 返回 `mode=http, connected=true`。 4. 权限放行:管理台/API 对 `mes.http_dispatch` 等工具 `allow=True`。 5. 联调:小批量下发 → 验证 MES 侧工单创建;重复 idemKey 验证幂等;断网/5xx 验证超时重试与补偿。 ## 测试 ```bash .venv\Scripts\python.exe -m pytest tests/golden/test_mes_readiness.py tests/golden/test_mes_http.py tests/golden/test_mes.py -q ``` - 本地 stub:FastAPI + uvicorn,**随机端口**(不占用 8003/5173)。 - 黄金覆盖:readiness(未配置 fail-closed / 字段齐 / 探测 ok/failed / 网关接线)+ 正常下发+回执 / 幂等 / 超时 / 5xx 重试恢复 / 重试耗尽 / 鉴权失败 / fail-closed / mes.py 域切换(stub 默认,HTTP 显式)/ MCP 总线端到端。 ## 边界与剩余风险 - **矩阵 73 的真实工厂对接仍需外部环境**:本轮的 HTTP 框架 + 本地 stub 黄金测试覆盖了 鉴权/幂等/超时/重试/回执/补偿的适配器侧语义;真实 MES 端点路径/报文差异需现场配置联调。 - 适配器假设 MES 侧以 `idemKey` 头/字段去重;真实 MES 若用不同幂等协议需在 `create_work_order` 内适配。 - token 经环境变量注入,未做密钥管理(生产可接入 KMS/Secrets 提供方)。