targets.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. """服务端目标目录:将业务名称解析为真实身份,技术参数不交给模型猜测。"""
  2. import json
  3. from pathlib import Path
  4. import re
  5. import unicodedata
  6. from pydantic import BaseModel, ConfigDict, Field
  7. import config
  8. from algorithms.main_fan.analysis import fan_numbers
  9. class TargetResolutionError(ValueError):
  10. """需要用户明确业务对象,不能继续取数。"""
  11. class FanTarget(BaseModel):
  12. model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
  13. mine_code: str = Field(min_length=1)
  14. mine_name: str = Field(min_length=1)
  15. fan_id: str = Field(min_length=1)
  16. fan_name: str = Field(min_length=1)
  17. mine_aliases: list[str] = Field(default_factory=list)
  18. aliases: list[str] = Field(default_factory=list)
  19. fan_pair: list[str] = Field(default_factory=list, max_length=2)
  20. class TargetCatalog(BaseModel):
  21. model_config = ConfigDict(extra="forbid")
  22. gateway_url: str = ""
  23. targets: list[FanTarget] = Field(default_factory=list, max_length=500)
  24. def load_catalog():
  25. filename = config.get_str("MAIN_FAN_TARGETS_FILE")
  26. if not filename:
  27. return TargetCatalog()
  28. path = Path(filename)
  29. if not path.is_absolute():
  30. path = config.ROOT / path
  31. catalog = TargetCatalog.model_validate(json.loads(path.read_text(encoding="utf-8")))
  32. if catalog.gateway_url and catalog.gateway_url.rstrip("/") != config.get_str("SHANXI_GATEWAY_URL").rstrip("/"):
  33. raise ValueError("主通风机目标目录绑定的网关与当前配置不一致,请配置对应环境的目标目录")
  34. identities = [(t.mine_code, t.fan_id) for t in catalog.targets]
  35. if len(set(identities)) != len(identities):
  36. raise ValueError("主通风机目标目录存在重复身份")
  37. return catalog
  38. def _key(value):
  39. value = re.sub(r"\s+", "", unicodedata.normalize("NFKC", value)).casefold()
  40. value = re.sub(r"^(?:(?:请|帮我|麻烦|对|审核|审查|检查|研判|检测|评估|分析|一下))+", "", value)
  41. value = re.sub(r"(?:进行)?(?:审核|审查|检查|研判|检测|评估|分析)(?:一下)?[。.!!??]*$", "", value)
  42. value = value.strip("。.!!??::,,").replace("的", "")
  43. for cn, number in {"一":"1", "二":"2", "三":"3", "四":"4", "五":"5",
  44. "六":"6", "七":"7", "八":"8", "九":"9", "十":"10"}.items():
  45. value = value.replace(cn+"号", number+"号")
  46. value = re.sub(r"(\d+)#", r"\1号", value)
  47. return re.sub("主通风机|通风机|主扇|风机", "风机", value)
  48. def _fan_aliases(target):
  49. names = [target.fan_name]
  50. numbers = fan_numbers(target.fan_name)
  51. if len(numbers) == 1:
  52. number = next(iter(numbers))
  53. names.extend([f"{number}号", f"{number}号通风机", f"主通{number}号", f"主通{number}号风机"])
  54. return names
  55. def _matches(target, selector):
  56. mines = [target.mine_name, *target.mine_aliases]
  57. fans = _fan_aliases(target)
  58. names = [target.fan_id, *target.aliases, *fans, *mines, "主通风机",
  59. *(mine+"主通风机" for mine in mines),
  60. *(mine+fan for mine in mines for fan in fans)]
  61. return _key(selector) in {_key(name) for name in names if name}
  62. def resolve_request(raw):
  63. if isinstance(raw, str) and raw.lstrip().startswith("{"):
  64. try:
  65. raw = json.loads(raw)
  66. except json.JSONDecodeError as exc:
  67. raise TargetResolutionError("目标输入的JSON格式不完整,请传目标名称或target对象。") from exc
  68. if isinstance(raw, str):
  69. data = {"target": raw}
  70. elif isinstance(raw, dict):
  71. data = dict(raw)
  72. else:
  73. raise TargetResolutionError("请说明要审核的矿井和主通风机。")
  74. for canonical, alias in (("mine_code","mineCode"),("mine_name","mineName"),("fan_id","fanId")):
  75. if alias in data:
  76. if canonical in data and data[canonical] != data[alias]:
  77. raise TargetResolutionError("输入的矿井或风机身份存在冲突,请确认业务对象。")
  78. data[canonical] = data.pop(alias)
  79. identity_fields = ("mine_code", "mine_name", "fan_id", "fan_name")
  80. if any(key in data and not isinstance(data[key], str) for key in identity_fields):
  81. raise TargetResolutionError("矿井与风机身份必须为文字,请确认业务对象。")
  82. provided = {key: value.strip() for key in identity_fields if isinstance((value := data.get(key)), str) and value.strip()}
  83. selector = data.get("target", "")
  84. if not isinstance(selector, str):
  85. raise TargetResolutionError("目标名称必须为文字。")
  86. selector = selector.strip()
  87. compact = bool(selector) or not (provided.get("mine_code") and provided.get("mine_name"))
  88. if compact:
  89. catalog = load_catalog()
  90. candidates = [t for t in catalog.targets if not selector or _matches(t, selector)]
  91. if not selector and not provided:
  92. candidates = []
  93. for key, value in provided.items():
  94. if key in {"mine_code", "fan_id"}:
  95. candidates = [t for t in candidates if getattr(t, key) == value]
  96. elif key == "mine_name":
  97. candidates = [t for t in candidates if _key(value) in {_key(n) for n in [t.mine_name,*t.mine_aliases]}]
  98. else:
  99. candidates = [t for t in candidates if _key(value) in {_key(n) for n in _fan_aliases(t)}]
  100. if len(candidates) > 1:
  101. pairs = {(t.mine_code, tuple(sorted(t.fan_pair))) for t in candidates if len(t.fan_pair) == 2}
  102. if len(pairs) == 1 and all(len(t.fan_pair) == 2 for t in candidates):
  103. candidates = [sorted(candidates, key=lambda t: t.fan_id)[0]]
  104. else:
  105. names = ";".join(t.mine_name+" "+t.fan_name for t in candidates[:8])
  106. raise TargetResolutionError("请明确要审核哪组主通风机:"+names)
  107. if not candidates:
  108. raise TargetResolutionError("未唯一匹配已配置的矿井主通风机,请确认矿井名称和风机编号;新增矿井需管理员补充身份映射。")
  109. target = candidates[0]
  110. data.update({key: getattr(target, key) for key in identity_fields})
  111. if target.fan_pair and not data.get("fan_pair"):
  112. data["fan_pair"] = target.fan_pair
  113. if "use_gateway" not in data:
  114. supplied = any(data.get(key) for key in ("materials", "reports", "devices", "points"))
  115. data["use_gateway"] = config.get_bool("SHANXI_GATEWAY_ENABLED", False) and not supplied
  116. if "document_cache_policy" not in data:
  117. data["document_cache_policy"] = config.get_str("MAIN_FAN_DEFAULT_DOCUMENT_CACHE_POLICY", "refresh")
  118. if not data.get("mine_code") or not data.get("mine_name"):
  119. raise TargetResolutionError("尚未确定矿井身份,请明确矿井名称和主通风机。")
  120. return data