| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111 |
- """Ai-DOP 只读预检(任务书 P1-G)。
- 只做只读探测,不写任何 Ai-DOP 表:
- - 可达性:GET {base}/api/mdp/inbound/__probe_not_exists(404/401 均视为服务在线);
- - 实体预检:用真实签名调 GET schema,按 HTTP 状态与消息分层判定
- READY / CONFIG_NOT_REGISTERED / CONFIG_NOT_ENABLED / GRANT_MISSING / SOURCE_UNREACHABLE;
- - 管理 API 预检(可选):仅当 AIDOP_SIM_ADMIN_TOKEN 提供时查询来源/实体配置,全部只读。
- 目标 host 必须命中白名单,否则硬阻断(默认拒绝非白名单目标)。
- """
- from __future__ import annotations
- import json
- import urllib.error
- import urllib.request
- from urllib.parse import urlparse
- from clients.inbound_client import InboundClient
- GRANT_HINTS = ("grant", "授权", "accesskey", "access key", "开放身份", "identity")
- class HostNotAllowedError(RuntimeError):
- pass
- def assert_host_allowed(base_url: str, allowed_hosts: list[str]) -> None:
- host = (urlparse(base_url).hostname or "").lower()
- allowed = {h.lower() for h in allowed_hosts}
- if host not in allowed:
- raise HostNotAllowedError(
- f"target host '{host}' not in allowedTargetHosts {sorted(allowed)};生产目标默认阻断")
- def check_reachable(base_url: str, allowed_hosts: list[str], timeout: int = 5) -> dict:
- assert_host_allowed(base_url, allowed_hosts)
- url = base_url.rstrip("/") + "/api/mdp/inbound/__probe_not_exists"
- req = urllib.request.Request(url, method="GET")
- try:
- with urllib.request.urlopen(req, timeout=timeout) as resp:
- return {"reachable": True, "httpStatus": resp.status}
- except urllib.error.HTTPError as ex:
- # 401/404 说明服务在线(缺签名头或路径不存在)
- return {"reachable": True, "httpStatus": ex.code}
- except Exception as ex: # URLError / timeout
- return {"reachable": False, "error": f"{type(ex).__name__}: {ex}"}
- def classify_inbound_precheck(status: int, body) -> str:
- """schema 探测结果 → 配置层状态码。"""
- if status == 0:
- return "SOURCE_UNREACHABLE"
- if status == 200:
- return "READY"
- message = ""
- if isinstance(body, dict):
- message = str(body.get("message") or "")
- if status == 404:
- return "CONFIG_NOT_REGISTERED"
- if status in (401, 403):
- low = message.lower()
- if any(h in low for h in GRANT_HINTS):
- return "GRANT_MISSING"
- return "CONFIG_NOT_ENABLED"
- return "RUN_FAILED"
- def precheck_inbound_entity(client: InboundClient, entity_code: str,
- allowed_hosts: list[str], contract_version: str | None = None) -> dict:
- assert_host_allowed(client.base, allowed_hosts)
- status, body, headers = client.op_schema(entity_code, contract_version=contract_version)
- state = classify_inbound_precheck(status, body)
- result = {
- "entityCode": entity_code,
- "configState": state,
- "httpStatus": status,
- "requestHeaders": headers,
- }
- if state == "READY" and isinstance(body, dict):
- data = body.get("data") or {}
- fields = data.get("fields") or []
- result["requiredFields"] = [f.get("name") for f in fields if f.get("required")]
- result["bizKeyExpr"] = data.get("bizKeyExpr")
- elif isinstance(body, dict):
- result["message"] = body.get("message")
- return result
- def precheck_all_inbound(client: InboundClient, entity_codes: list[str],
- allowed_hosts: list[str]) -> list[dict]:
- return [precheck_inbound_entity(client, code, allowed_hosts) for code in entity_codes]
- def query_admin_api(base_url: str, admin_token: str, path: str,
- allowed_hosts: list[str], timeout: int = 10):
- """可选只读管理 API 查询;未提供 admin token 时由调用方跳过。"""
- assert_host_allowed(base_url, allowed_hosts)
- req = urllib.request.Request(
- base_url.rstrip("/") + path,
- headers={"Authorization": f"Bearer {admin_token}"},
- method="GET")
- try:
- with urllib.request.urlopen(req, timeout=timeout) as resp:
- return resp.status, json.loads(resp.read().decode("utf-8") or "{}")
- except urllib.error.HTTPError as ex:
- try:
- return ex.code, json.loads(ex.read().decode("utf-8") or "{}")
- except json.JSONDecodeError:
- return ex.code, {}
- except Exception as ex:
- return 0, {"message": f"{type(ex).__name__}: {ex}"}
|