aps-agent/docs/integrations/mes-http.md

137 lines
7.5 KiB
Markdown
Raw Normal View History

# 真实 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>:<externalWoId>"`)而不是静默跳过:
| 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 提供方)。