aidop_probe.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. """Ai-DOP 只读预检(任务书 P1-G)。
  2. 只做只读探测,不写任何 Ai-DOP 表:
  3. - 可达性:GET {base}/api/mdp/inbound/__probe_not_exists(404/401 均视为服务在线);
  4. - 实体预检:用真实签名调 GET schema,按 HTTP 状态与消息分层判定
  5. READY / CONFIG_NOT_REGISTERED / CONFIG_NOT_ENABLED / GRANT_MISSING / SOURCE_UNREACHABLE;
  6. - 管理 API 预检(可选):仅当 AIDOP_SIM_ADMIN_TOKEN 提供时查询来源/实体配置,全部只读。
  7. 目标 host 必须命中白名单,否则硬阻断(默认拒绝非白名单目标)。
  8. """
  9. from __future__ import annotations
  10. import json
  11. import urllib.error
  12. import urllib.request
  13. from urllib.parse import urlparse
  14. from clients.inbound_client import InboundClient
  15. GRANT_HINTS = ("grant", "授权", "accesskey", "access key", "开放身份", "identity")
  16. class HostNotAllowedError(RuntimeError):
  17. pass
  18. def assert_host_allowed(base_url: str, allowed_hosts: list[str]) -> None:
  19. host = (urlparse(base_url).hostname or "").lower()
  20. allowed = {h.lower() for h in allowed_hosts}
  21. if host not in allowed:
  22. raise HostNotAllowedError(
  23. f"target host '{host}' not in allowedTargetHosts {sorted(allowed)};生产目标默认阻断")
  24. def check_reachable(base_url: str, allowed_hosts: list[str], timeout: int = 5) -> dict:
  25. assert_host_allowed(base_url, allowed_hosts)
  26. url = base_url.rstrip("/") + "/api/mdp/inbound/__probe_not_exists"
  27. req = urllib.request.Request(url, method="GET")
  28. try:
  29. with urllib.request.urlopen(req, timeout=timeout) as resp:
  30. return {"reachable": True, "httpStatus": resp.status}
  31. except urllib.error.HTTPError as ex:
  32. # 401/404 说明服务在线(缺签名头或路径不存在)
  33. return {"reachable": True, "httpStatus": ex.code}
  34. except Exception as ex: # URLError / timeout
  35. return {"reachable": False, "error": f"{type(ex).__name__}: {ex}"}
  36. def classify_inbound_precheck(status: int, body) -> str:
  37. """schema 探测结果 → 配置层状态码。"""
  38. if status == 0:
  39. return "SOURCE_UNREACHABLE"
  40. if status == 200:
  41. return "READY"
  42. message = ""
  43. if isinstance(body, dict):
  44. message = str(body.get("message") or "")
  45. if status == 404:
  46. return "CONFIG_NOT_REGISTERED"
  47. if status in (401, 403):
  48. low = message.lower()
  49. if any(h in low for h in GRANT_HINTS):
  50. return "GRANT_MISSING"
  51. return "CONFIG_NOT_ENABLED"
  52. return "RUN_FAILED"
  53. def precheck_inbound_entity(client: InboundClient, entity_code: str,
  54. allowed_hosts: list[str], contract_version: str | None = None) -> dict:
  55. assert_host_allowed(client.base, allowed_hosts)
  56. status, body, headers = client.op_schema(entity_code, contract_version=contract_version)
  57. state = classify_inbound_precheck(status, body)
  58. result = {
  59. "entityCode": entity_code,
  60. "configState": state,
  61. "httpStatus": status,
  62. "requestHeaders": headers,
  63. }
  64. if state == "READY" and isinstance(body, dict):
  65. data = body.get("data") or {}
  66. fields = data.get("fields") or []
  67. result["requiredFields"] = [f.get("name") for f in fields if f.get("required")]
  68. result["bizKeyExpr"] = data.get("bizKeyExpr")
  69. elif isinstance(body, dict):
  70. result["message"] = body.get("message")
  71. return result
  72. def precheck_all_inbound(client: InboundClient, entity_codes: list[str],
  73. allowed_hosts: list[str]) -> list[dict]:
  74. return [precheck_inbound_entity(client, code, allowed_hosts) for code in entity_codes]
  75. def query_admin_api(base_url: str, admin_token: str, path: str,
  76. allowed_hosts: list[str], timeout: int = 10):
  77. """可选只读管理 API 查询;未提供 admin token 时由调用方跳过。"""
  78. assert_host_allowed(base_url, allowed_hosts)
  79. req = urllib.request.Request(
  80. base_url.rstrip("/") + path,
  81. headers={"Authorization": f"Bearer {admin_token}"},
  82. method="GET")
  83. try:
  84. with urllib.request.urlopen(req, timeout=timeout) as resp:
  85. return resp.status, json.loads(resp.read().decode("utf-8") or "{}")
  86. except urllib.error.HTTPError as ex:
  87. try:
  88. return ex.code, json.loads(ex.read().decode("utf-8") or "{}")
  89. except json.JSONDecodeError:
  90. return ex.code, {}
  91. except Exception as ex:
  92. return 0, {"message": f"{type(ex).__name__}: {ex}"}