Files
AI-Check-Test/.gitea/checker/notifier.py
dongzi 4ebb71f7a0
All checks were successful
API接口参数变更检测 / api-param-check (push) Successful in 24s
测试:普通参数--新增&修改&删除
2026-06-05 17:24:59 +08:00

526 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
企业微信 Markdown 通知模块。
支持加粗、颜色info/comment/warning新增接口与变更接口使用不同展示模板。
"""
import json
import re
from collections import OrderedDict
from typing import List, Optional, Tuple
import requests
from comparator import EndpointChangeReport, ParameterChange
# 企微 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<font color=\"comment\">... 消息过长,已截断</font>"
def _format_param_change_list(changes: List[ParameterChange]) -> List[str]:
"""生成企微友好的普通参数变更列表(卡片式)。"""
if not changes:
return ['<font color="comment">无</font>']
lines = ["", f"共 **{len(changes)}** 项变更", ""]
for i, change in enumerate(changes, 1):
lines.append(change.to_markdown_block(i))
if i < len(changes):
lines.append("")
return lines
def _body_dto_group_key(change: ParameterChange) -> Tuple[str, str]:
"""类对象变更分组键:(body 参数名, DTO 类名)。"""
return (change.body_param_name or "body", change.parent_dto or "")
def _format_body_field_line(change: ParameterChange, *, is_last: bool) -> List[str]:
"""格式化 DTO 一级字段变更行。"""
branch = "└─" if is_last else "├─"
desc = change.description or change.old_description
type_part = f" · `{change.param_type}`" if change.param_type else ""
req_part = f" · {change._required_tag()}" if change._required_tag() else ""
lines = [f"{branch} `{change.param_name}`{type_part}{req_part} {change._change_tag()}"]
if desc:
lines.append(f"> 说明:{desc}")
if change.change_type.value == "modified" and change.detail:
lines.append(f"> 变更:{change.detail}")
if change.change_type.value == "renamed":
lines.append(f"> `{change.old_name}` → `{change.param_name}`")
return lines
def _format_body_dto_groups(changes: List[ParameterChange]) -> List[str]:
"""按 DTO 分组展示 @RequestBody 一级字段。"""
if not changes:
return ['<font color="comment">无</font>']
groups: OrderedDict[Tuple[str, str], List[ParameterChange]] = OrderedDict()
for change in changes:
key = _body_dto_group_key(change)
groups.setdefault(key, []).append(change)
lines: List[str] = ["", f"共 **{len(groups)}** 个类对象 · **{len(changes)}** 项字段变更", ""]
for (param_name, dto_name), group in groups.items():
label = param_name or "body"
dto_part = f" · `{dto_name}`" if dto_name else ""
lines.append(f"**{label}**{dto_part}")
lines.append("")
for i, change in enumerate(group):
lines.extend(_format_body_field_line(change, is_last=(i == len(group) - 1)))
lines.append("")
if lines and lines[-1] == "":
lines.pop()
return lines
def _format_param_details_section(report: EndpointChangeReport) -> List[str]:
"""生成接口参数变动详情区块。"""
body_changes = [c for c in report.parameter_changes if c.source == "body"]
regular_changes = [c for c in report.parameter_changes if c.source != "body"]
lines = ["", "---------------------------------------", "", "#### 【接口参数变动详情】", ""]
if body_changes:
lines.append("**类对象变更(一级字段)**")
lines.extend(_format_body_dto_groups(body_changes))
lines.append("")
if regular_changes:
lines.append("**普通参数变更**")
lines.extend(_format_param_change_list(regular_changes))
lines.append("")
if not body_changes and not regular_changes:
lines.append('<font color="comment">无</font>')
return lines
def _format_endpoint_block(report: EndpointChangeReport) -> str:
"""
格式化单个接口块,按模板匹配格式输出。
全路径类名显示为 source_file相对仓库根的完整 .java 路径)。
"""
change_type = "新增接口" if report.is_new_endpoint else ("删除接口" if report.is_removed_endpoint else "修改参数")
uri_line = f"**{report.http_method}** `{report.uri}`"
file_path = report.source_file or report.controller_class
class_line = f"- **全路径类名:** <font color=\"info\">**{file_path}**</font>"
header = [
f"- **变更类型:** <font color=\"warning\">**{change_type}**</font>",
f"- **URI** {uri_line}",
class_line,
]
if report.is_removed_endpoint:
return "\n".join(header + ["", f"<font color=\"warning\">**该接口已被移除**</font>"])
return "\n".join(header + _format_param_details_section(report))
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: List[str] = []
# 所有 API 级变更(新增、修改路径、修改请求方式、删除、参数变更)统一走 model1.md 路径变更通知
method_changed_reports = [r for r in reports if r.is_method_changed]
renamed_reports = [r for r in reports if r.is_renamed_endpoint]
new_reports = [r for r in reports if r.is_new_endpoint]
# 参数变更报告只包含「URI/方法未变,仅参数变化」的报告
# 路径变更 + 参数变更、方法变更 + 参数变更 场景已在上层 comparator 中拆分为独立报告
changed_reports = [
r for r in reports
if not r.is_new_endpoint
and not r.is_removed_endpoint
and not r.is_renamed_endpoint
and not r.is_method_changed
]
removed_reports = [r for r in reports if r.is_removed_endpoint]
# 1. 新增接口 → 走 API路径变更通知
for report in new_reports:
path_md = build_path_change_markdown(
old_uri="-",
new_uri=report.uri,
change_type="新增接口",
push_user=push_user,
push_time=push_time,
file_name=report.source_file or report.controller_class,
)
parts.append(path_md)
parts.append("")
# 2. 修改请求方式 → 使用独立的新模板 【API请求方式变更通知】
for report in method_changed_reports:
method_md = build_method_change_markdown(
uri=report.uri,
old_method=report.old_http_method or "?",
new_method=report.http_method,
push_user=push_user,
push_time=push_time,
file_name=report.source_file or report.controller_class,
)
parts.append(method_md)
parts.append("")
# 3. 修改路径 → 走 API路径变更通知
for report in renamed_reports:
path_md = build_path_change_markdown(
old_uri=report.old_uri or "-",
new_uri=report.uri,
change_type="修改路径",
push_user=push_user,
push_time=push_time,
file_name=report.source_file or report.controller_class,
)
parts.append(path_md)
parts.append("")
# 4. 删除接口 → 走 API路径变更通知
for report in removed_reports:
path_md = build_path_change_markdown(
old_uri=report.uri,
new_uri="已删除",
change_type="删除接口",
push_user=push_user,
push_time=push_time,
file_name=report.source_file or report.controller_class,
)
parts.append(path_md)
parts.append("")
# 4. 普通参数变更(非路径变更)仍使用 model.md 格式
if changed_reports:
parts.append("# 【API参数变更通知】")
parts.append(f"- **修改人:** {push_user}")
parts.append(f"- **修改时间:** {push_time}")
parts.append("")
for report in changed_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("### <font color=\"comment\">【兼容性提示】</font>")
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 格式的接口变更通知。
严格按变更类型拆分,各自独立构建和发送企微通知:
- 方法变更 → 独立调用 build_method_change_markdown
- 路径变更(新增/修改/删除) → 独立调用 build_path_change_markdown
- 参数变更 → 独立调用 _format_endpoint_block
不同类型之间完全互不干扰,各自走独立分支。
"""
if not reports and not llm_review:
print("无接口参数变更,不发送到企业微信")
return 0
# 按类型严格分组(互不重叠)
method_changed_reports = [r for r in reports if r.is_method_changed]
renamed_reports = [r for r in reports if r.is_renamed_endpoint]
new_reports = [r for r in reports if r.is_new_endpoint]
removed_reports = [r for r in reports if r.is_removed_endpoint]
changed_reports = [
r for r in reports
if not r.is_new_endpoint
and not r.is_removed_endpoint
and not r.is_renamed_endpoint
and not r.is_method_changed
]
sent = 0
# ========== 1. 请求方式变更通知(独立分支) ==========
for report in method_changed_reports:
md = build_method_change_markdown(
uri=report.uri,
old_method=report.old_http_method or "?",
new_method=report.http_method,
push_user=push_user,
push_time=push_time,
file_name=report.source_file or report.controller_class,
)
if _post_wecom_markdown(webhook_url, md):
sent += 1
print(f"{sent} 条通知已发送到企业微信(请求方式变更)")
# ========== 2. 路径变更通知(新增/修改/删除) ==========
# 新增接口
for report in new_reports:
md = build_path_change_markdown(
old_uri="-",
new_uri=report.uri,
change_type="新增接口",
push_user=push_user,
push_time=push_time,
file_name=report.source_file or report.controller_class,
)
if report.parameter_changes:
param_section = "\n".join(_format_param_details_section(report)).strip()
md = f"{md}\n\n{param_section}"
if _post_wecom_markdown(webhook_url, md):
sent += 1
print(f"{sent} 条通知已发送到企业微信(新增接口)")
# 修改路径
for report in renamed_reports:
md = build_path_change_markdown(
old_uri=report.old_uri or "-",
new_uri=report.uri,
change_type="修改路径",
push_user=push_user,
push_time=push_time,
file_name=report.source_file or report.controller_class,
)
if _post_wecom_markdown(webhook_url, md):
sent += 1
print(f"{sent} 条通知已发送到企业微信(修改路径)")
# 删除接口
for report in removed_reports:
md = build_path_change_markdown(
old_uri=report.uri,
new_uri="已删除",
change_type="删除接口",
push_user=push_user,
push_time=push_time,
file_name=report.source_file or report.controller_class,
)
if _post_wecom_markdown(webhook_url, md):
sent += 1
print(f"{sent} 条通知已发送到企业微信(删除接口)")
# ========== 3. 参数变更通知(独立分支) ==========
if changed_reports:
# 构建参数变更通知(只包含参数变更报告,对齐 model.md
parts: List[str] = []
parts.append("# 【API参数变更通知】")
parts.append(f"- **修改人:** {push_user}")
parts.append(f"- **修改时间:** {push_time}")
parts.append("")
for report in changed_reports:
parts.append(_format_endpoint_block(report))
parts.append("")
if llm_review:
parts.append("---")
parts.append("### <font color=\"comment\">兼容性提示</font>")
parts.append(llm_review.strip())
md = "\n".join(parts).strip()
if _post_wecom_markdown(webhook_url, md):
sent += 1
print(f"{sent} 条通知已发送到企业微信(参数变更)")
if sent > 0:
print(f"总共发送 {sent} 条通知到企业微信")
return sent
def build_path_change_markdown(
old_uri: str,
new_uri: str,
change_type: str,
push_user: str,
push_time: str,
file_name: str,
) -> str:
"""构建 API路径变更通知完全匹配 model1.md 模板,并加强视觉区分。
支持的 change_type
- 新增接口 / 删除接口 / 修改路径 / 修改请求方式
改进点:
- 标题使用【】风格
- 头部信息缩进 + 颜色高亮
- URI 详情使用列表(更直观)
- 「修改请求方式」额外展示方法变更
"""
# 变更类型高亮
type_highlight = f"<font color=\"warning\">**{change_type}**</font>"
# 全路径类名高亮
class_highlight = f"<font color=\"info\">**{file_name}**</font>"
# 根据变更类型优化 URI 展示
if change_type == "新增接口":
old_display = "`-`"
new_display = f"<font color=\"info\">**`{new_uri}`**</font> ← <font color=\"info\">**新增**</font>"
elif change_type == "删除接口":
old_display = f"<font color=\"warning\">**`{old_uri}`**</font> ← <font color=\"warning\">**已删除**</font>"
new_display = "`已删除`"
else: # 修改路径
old_display = f"<font color=\"warning\">~~`{old_uri}`~~</font> ← <font color=\"warning\">**旧路径**</font>"
new_display = f"<font color=\"info\">**`{new_uri}`**</font> ← <font color=\"info\">**新路径**</font>"
parts = [
"# 【API路径变更通知】",
"",
f" 变更类型: {type_highlight}",
f" 全路径类名: {class_highlight}",
f" 修改人: {push_user}",
f" 修改时间: {push_time}",
"",
"---------------------------------------",
"",
"#### 【URI变更详情】",
f"- **原路径:** {old_display}",
f"- **新路径:** {new_display}",
"",
]
return "\n".join(parts).strip()
def build_method_change_markdown(
uri: str,
old_method: str,
new_method: str,
push_user: str,
push_time: str,
file_name: str,
) -> str:
"""构建【API请求方式变更通知】独立模板。
格式参考 model1.md但专门针对 HTTP 方法变更场景设计,
突出「原请求方式 → 新请求方式」的对比。
"""
type_highlight = '<font color="warning">**修改请求方式**</font>'
class_highlight = f'<font color="info">**{file_name}**</font>'
uri_highlight = f'<font color="info">**`{uri}`**</font>'
old_m = f'<font color="warning">**{old_method}**</font>'
new_m = f'<font color="info">**{new_method}**</font>'
parts = [
"# 【API请求方式变更通知】",
"",
f" 变更类型: {type_highlight}",
f" 全路径类名: {class_highlight}",
f" 修改人: {push_user}",
f" 修改时间: {push_time}",
"",
"---------------------------------------",
"",
"#### 【请求方式变更详情】",
f"- **URI** {uri_highlight}",
f"- **原请求方式:** {old_m}",
f"- **新请求方式:** {new_m} ← <font color=\"info\">**请求方式已变更**</font>",
"",
]
return "\n".join(parts).strip()
def send_path_change_notification(
webhook_url: str,
old_uri: str,
new_uri: str,
change_type: str,
push_user: str,
push_time: str,
file_name: str,
) -> bool:
"""发送路径变更通知。"""
md = build_path_change_markdown(old_uri, new_uri, change_type, push_user, push_time, file_name)
return _post_wecom_markdown(webhook_url, md)