result_output.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. """主通风机结果平台映射:结构化事实决定编码,报告正文保留整体结论。"""
  2. from datetime import datetime
  3. from html import unescape
  4. import re
  5. from algorithms.main_fan.analysis import LOCAL_ZONE, local_time, same_mine
  6. from algorithms.main_fan.equivalence import three_check_statuses
  7. from runtime.result_models import PlatformJudgment, PlatformResultPayload
  8. def judgment_result(assessment, analysis):
  9. """编码只采用三项业务规则;OCR说明、表图差异和补充指标单独呈现。"""
  10. assessment = assessment or {}
  11. if assessment.get("mode") == "sandbox_exploration":
  12. # 自主模式采用脚本交付的结论;此处只映射平台格式,不重跑业务计算。
  13. from extensions.main_fan.schemas import AgentAssessment
  14. submitted = AgentAssessment.model_validate(assessment["agent_result"])
  15. return submitted.result_code, submitted.summary
  16. decision = assessment.get("decision", {})
  17. basic = assessment.get("basic_comparison", {})
  18. switches = assessment.get("switch_comparison", {})
  19. items = three_check_statuses(basic, switches)
  20. code = ("NORMAL" if all(status == "pass" for status in items.values()) else
  21. "ABNORMAL" if decision.get("severe_findings") else
  22. "WARNING" if "fail" in items.values() else "UNDETERMINED")
  23. if code in {"WARNING", "ABNORMAL"}:
  24. reasons = []
  25. mismatches = [r["label"] for r in basic.get("rows", []) if r["status"] == "mismatch"]
  26. if mismatches:
  27. reasons.append("两台风机的" + "、".join(mismatches) + "不一致")
  28. for event in switches.get("events", []):
  29. metrics, timing, checks = event["metrics"], event["timing"], event["checks"]
  30. if checks.get("switch_interval") == "fail":
  31. reasons.append(f"切换间隔{metrics['switch_interval_minutes']:g}分钟,超过{timing['limit_minutes']:g}分钟")
  32. if checks.get("flow_change") == "fail":
  33. reasons.append(f"稳定风量差异{metrics['flow_deviation_percent']:.2f}%,超过{event['limits']['flow_deviation_percent']:g}%")
  34. prefix = "研判预警:" if code == "WARNING" else "研判异常:"
  35. suffix = "。" if code == "WARNING" else ",其中已核实问题达到配置的严重异常条件。"
  36. return code, prefix + ";".join(dict.fromkeys(reasons)) + suffix
  37. if code == "NORMAL":
  38. events = switches.get("events", [])
  39. details = []
  40. if len(events) == 1:
  41. metrics = events[0]["metrics"]
  42. if metrics.get("switch_interval_minutes") is not None:
  43. details.append(f"切换间隔{metrics['switch_interval_minutes']:g}分钟")
  44. if metrics.get("flow_deviation_percent") is not None:
  45. details.append(f"稳定风量差异{metrics['flow_deviation_percent']:.2f}%")
  46. sentence = "研判正常:两台风机报告基础参数一致"
  47. return code, sentence + ("," + "、".join(details) if details else "") + ",三项均通过,判定主备主要通风机在本次核验范围内具备同等能力。"
  48. scopes = {"report_parameters": "报告参数", "report_performance": "性能测定数据", "switch_performance": "切换工况"}
  49. compared = [scopes[key] for key in decision.get("reported_scope", []) if key in scopes]
  50. prefix = "已完成" + "、".join(compared) + "分析," if compared else ""
  51. return code, prefix + "关键比较数据未能形成有效对照,本次不作主备主要通风机同等能力的确定结论。"
  52. def report_text(markdown):
  53. """移除图像编码与 Markdown 标记,将每张表独立转换为可阅读的键值行。"""
  54. lines, headers = [], None
  55. for raw in markdown.splitlines():
  56. line = raw.strip()
  57. if re.match(r"^!\[[^]]*\]\(", line):
  58. headers = None
  59. continue
  60. if line.startswith("|") and line.endswith("|"):
  61. values = [value.strip() for value in line.strip("|").split("|")]
  62. if all(re.fullmatch(r":?-{3,}:?", value) for value in values):
  63. continue
  64. if headers is None:
  65. headers = values
  66. continue
  67. line = ";".join(f"{key}:{value}" for key, value in zip(headers, values))
  68. else:
  69. headers = None
  70. line = re.sub(r"^#{1,6}\s+", "", line)
  71. line = re.sub(r"\[([^]]+)\]\([^)]*\)", r"\1", line)
  72. line = line.replace("**", "").replace("`", "")
  73. line = unescape(line)
  74. if line or (lines and lines[-1]):
  75. lines.append(line)
  76. return "\n".join(lines).strip()
  77. def _data_detail(data, assessment):
  78. if (assessment or {}).get("mode") == "sandbox_exploration":
  79. return assessment["agent_result"]["data_detail"]
  80. parts = []
  81. basic = (assessment or {}).get("basic_comparison", {})
  82. curves = (assessment or {}).get("performance_comparison", {}).get("results", [])
  83. events = (assessment or {}).get("switch_comparison", {}).get("events", [])
  84. pair = (assessment or {}).get("fan_pair", [])
  85. ids = {fan["fan_id"] for fan in pair}
  86. report_ids = {fan["report_id"] for fan in pair if fan.get("report_id")}
  87. reports = [r for r in data.reports if same_mine(r, data) and r.report_id in report_ids]
  88. sources = []
  89. if reports:
  90. sources.append(f"双机性能测定资料({len(reports)}份)" if len(reports) == 2 else f"性能测定资料({len(reports)}份)")
  91. if basic.get("backup_availability"):
  92. sources.append("设备台账")
  93. if events:
  94. if any(event.get("timing", {}).get("interval_minutes") is not None for event in events):
  95. sources.append("整机开停记录")
  96. if any(event.get("changes") for event in events):
  97. sources.append("运行监测序列")
  98. if sources:
  99. parts.append("资料来源:" + "、".join(sources))
  100. if events:
  101. used_series = [s for s in data.operating_series if same_mine(s, data) and s.fan_id in ids]
  102. times = [local_time(p.observed_at) for series in used_series for p in series.samples]
  103. if times:
  104. parts.append(f"监测时段:{min(times):%Y-%m-%d %H:%M:%S}至{max(times):%Y-%m-%d %H:%M:%S}")
  105. backup = basic.get("backup_availability")
  106. if backup:
  107. parts.append("备用状态:" + {"available": "可用", "unavailable": "不可用"}.get(backup["status"], backup["status"]))
  108. angles = sorted({c["blade_angle_deg"] for c in curves if c.get("blade_angle_deg") is not None})
  109. if angles:
  110. parts.append("测试叶片角度:" + "、".join(f"{value:g}°" for value in angles))
  111. if curves:
  112. parts.append("共同频率:" + "、".join(f"{value:g}Hz" for value in sorted({c["frequency_hz"] for c in curves})))
  113. for index, event in enumerate(events, 1):
  114. prefix = f"第{index}次切换:" if len(events) > 1 else ""
  115. metrics = event["metrics"]
  116. if metrics.get("switch_interval_minutes") is not None:
  117. parts.append(f"{prefix}切换间隔:{metrics['switch_interval_minutes']:g}分钟(阈值≤{event['timing']['limit_minutes']:g}分钟)")
  118. if "flow" in event.get("changes", {}):
  119. change = event["changes"]["flow"]
  120. parts.extend([f"{prefix}切换前稳定风量:{change['before']*60:g}m³/min",
  121. f"{prefix}切换后稳定风量:{change['after']*60:g}m³/min",
  122. f"{prefix}风量差异:{metrics['flow_deviation_percent']:.2f}%(阈值≤{event['limits']['flow_deviation_percent']:g}%)"])
  123. return ";".join(parts)
  124. def build_payload(value, data, analysis, binding, *, task_id, checked_at=None):
  125. assessment = value.get("assessment") or {}
  126. pair = assessment.get("fan_pair", [])
  127. names = "、".join(fan["fan_name"] for fan in pair) or value["check_object"]
  128. ids = "、".join(fan["fan_id"] for fan in pair) or data.fan_id
  129. object_detail = "研判对象:" + names
  130. if ids:
  131. object_detail += ";风机标识:" + ids
  132. object_detail += ";研判内容:主备风机同等能力"
  133. code, short_result = judgment_result(assessment, analysis)
  134. return PlatformResultPayload(
  135. agentCode=binding.agent_code, agentName=binding.agent_name, taskId=task_id,
  136. checkedAt=checked_at or datetime.now(LOCAL_ZONE).strftime("%Y-%m-%d %H:%M:%S"),
  137. mineCode=value["mine_code"], mineName=value["mine_name"], objectDetail=object_detail,
  138. dataDetail=_data_detail(data, assessment),
  139. judgment=PlatformJudgment(resultCode=code, result=short_result,
  140. reason=report_text(value["result"]), evidenceLink=""),
  141. ).model_dump(mode="json")