""" 企业微信 Markdown 通知模块。 支持加粗、颜色(info/comment/warning),新增接口与变更接口使用不同展示模板。 """ import json import re from typing import List, Optional import requests from comparator import EndpointChangeReport # 企微 Markdown 单条上限 4096 字符,留余量 MAX_MD_LENGTH = 3800 def truncate_text(text: str, max_length: int = MAX_MD_LENGTH) -> str: """截断超长消息。""" if len(text) <= max_length: return text return text[:max_length] + "\n\n... 消息过长,已截断" def _format_endpoint_block(report: EndpointChangeReport) -> str: """ 格式化单个接口块。 新增接口:只列参数,不出现「参数变更」字样。 变更接口:用颜色区分增删改。 删除接口:整接口标红。 """ uri_line = f"**{report.http_method}** `{report.uri}`" if report.is_removed_endpoint: return ( f"### 【已删除接口】\n" f"{uri_line}\n" f"该接口已被移除" ) if report.is_new_endpoint: lines = [ f"### 【新增接口】 {uri_line}", ] if report.parameter_changes: for change in report.parameter_changes: lines.append(change.to_markdown_line(plain=True)) else: lines.append('无入参') return "\n".join(lines) # 已有接口的参数变更 lines = [f"### {uri_line}"] if report.parameter_changes: for change in report.parameter_changes: lines.append(change.to_markdown_line(plain=False)) else: lines.append('(无参数变化)') return "\n".join(lines) def build_markdown_notification( reports: List[EndpointChangeReport], push_user: str, push_time: str, llm_summary: Optional[str] = None, ) -> str: """ 构建完整 Markdown 通知正文。 :param reports: AST 变更报告 :param push_user: 推送人 :param push_time: 推送时间 :param llm_summary: LLM 兼容性摘要(可选,简短) :return: Markdown 文本 """ parts = [ "## API 接口参数变更通知", f"**修改人:** {push_user}", f"**修改时间:** {push_time}", "", ] # 新增 Controller(全部为新接口)与变更接口分组 new_reports = [r for r in reports if r.is_new_endpoint] changed_reports = [r for r in reports if not r.is_new_endpoint and not r.is_removed_endpoint] removed_reports = [r for r in reports if r.is_removed_endpoint] if new_reports: controllers = sorted({r.controller_class for r in new_reports}) parts.append(f"### 新增 Controller `{', '.join(controllers)}`") parts.append("") for report in new_reports: parts.append(_format_endpoint_block(report)) parts.append("") if changed_reports: parts.append("### 接口参数变更") parts.append("") for report in changed_reports: parts.append(_format_endpoint_block(report)) parts.append("") if removed_reports: parts.append("### 已删除接口") parts.append("") for report in removed_reports: parts.append(_format_endpoint_block(report)) parts.append("") if llm_summary: cleaned = llm_summary.strip() # 去掉 LLM 可能输出的「排除框架注入」类说明 cleaned = re.sub( r"(排除Spring MVC框架自动注入的[^)]+)", "", cleaned, ) cleaned = re.sub( r"排除Spring MVC框架自动注入的[`\w/]+[`\w/、/]*[。\.]?", "", cleaned, ) if cleaned: parts.append("---") parts.append("### 兼容性提示") parts.append(cleaned) return "\n".join(parts).strip() def _split_markdown(text: str, max_len: int) -> List[str]: """按 ### 标题块拆分超长 Markdown。""" if len(text) <= max_len: return [text] lines = text.split("\n") chunks: List[str] = [] current: List[str] = [] for line in lines: if line.startswith("### ") and current and len("\n".join(current)) > 200: chunks.append("\n".join(current)) current = [line] else: current.append(line) if len("\n".join(current)) >= max_len: chunks.append("\n".join(current)) current = [] if current: if chunks and len("\n".join(current)) < 200: chunks[-1] = chunks[-1] + "\n" + "\n".join(current) else: chunks.append("\n".join(current)) return chunks or [truncate_text(text)] def _post_wecom_markdown(webhook_url: str, content: str) -> bool: """发送企微 Markdown 消息。""" if not webhook_url or "YOUR_WECOM_KEY" in webhook_url: print("[警告] 未配置有效的企业微信 Webhook URL。") print("--- 通知预览 ---") print(content[:1000]) return False payload = { "msgtype": "markdown", "markdown": {"content": truncate_text(content)}, } try: resp = requests.post( webhook_url, headers={"Content-Type": "application/json"}, data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), timeout=10, ) if resp.status_code == 200 and resp.json().get("errcode", 0) == 0: return True print(f"[错误] 企微返回异常: {resp.status_code} {resp.text}") return False except requests.RequestException as exc: print(f"[错误] 发送企微消息失败: {exc}") return False def send_parameter_change_notification( webhook_url: str, reports: List[EndpointChangeReport], push_user: str, push_time: str, llm_review: Optional[str] = None, mentioned_users: Optional[List[str]] = None, ) -> int: """ 发送 Markdown 格式的接口参数变更通知。 :param webhook_url: 企微 Webhook :param reports: 变更报告 :param push_user: 推送人 :param push_time: 推送时间 :param llm_review: LLM 兼容性摘要 :param mentioned_users: @ 成员(Markdown 暂不支持,保留参数) :return: 成功发送条数 """ if not reports and not llm_review: print("无接口参数变更,不发送到企业微信") return 0 full_md = build_markdown_notification(reports, push_user, push_time, llm_review) segments = _split_markdown(full_md, MAX_MD_LENGTH) sent = 0 for i, segment in enumerate(segments): if i > 0: segment = ( f"(续 {i + 1}/{len(segments)})\n\n" + segment ) if _post_wecom_markdown(webhook_url, segment): sent += 1 print(f"第 {sent} 条通知已发送到企业微信") if sent > 0: print(f"总共发送 {sent} 条通知到企业微信") return sent