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

137 lines
7.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 真实 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 提供方)。