"""异常与错误分类。 WCF 的错误是 HTML 页面,真正的信息藏在 "The exception message is '...'" 里。 分类的目的是决定「重试 / 跳过 / 整批中止」,而不是把所有失败都当网络问题重试。 """ from __future__ import annotations from enum import StrEnum class Category(StrEnum): NET = "NET" # 网络层:可重试 SP_MISSING = "SP_MISSING" # 存储过程不存在:配置/版本问题,不重试 PARAM = "PARAM" # 参数错误/类型转换:数据问题,不重试 EXCEL = "EXCEL" # Excel 解析失败(Column1 != 0) TFS = "TFS" # 推送 TFS 失败 AUTH = "AUTH" # 登录失败 UNKNOWN = "UNKNOWN" class QtError(RuntimeError): """带分类的接口错误。""" def __init__(self, message: str, category: Category = Category.UNKNOWN, *, retryable: bool = False): super().__init__(message) self.message = message self.category = category self.retryable = retryable def __str__(self) -> str: # pragma: no cover - 直通 return f"[{self.category}] {self.message}" @classmethod def from_http(cls, status: int, body: str) -> "QtError": """把 WCF 的 HTML 错误页转成可读错误。""" message = extract_exception_message(body) or f"HTTP {status}" lowered = message.lower() if "could not find stored procedure" in lowered: return cls(message, Category.SP_MISSING) if "missing parameter" in lowered or "conversion" in lowered or "procedure or function" in lowered: return cls(message, Category.PARAM) if status >= 500: return cls(message, Category.NET, retryable=True) return cls(message, Category.UNKNOWN) def extract_exception_message(body: str) -> str: """从 WCF 错误页里抠出真正的异常文本。""" import html import re match = re.search(r"exception message is '(.*?)'\s*\.?\s*See server logs", body, re.S) if match: return html.unescape(match.group(1)).strip() plain = re.sub(r"<[^>]+>", " ", body) plain = html.unescape(re.sub(r"\s+", " ", plain)).strip() return plain[:400]