py修改
All checks were successful
API接口参数变更检测 / api-param-check (push) Successful in 16s

This commit is contained in:
2026-06-04 09:53:36 +08:00
parent a8cde16c17
commit 1b19e8366e
4 changed files with 245 additions and 205 deletions

View File

@@ -1,162 +1,174 @@
"""
企业微信机器人通知模块。
按第一版模板发送 Controller 接口参数变更通知,支持超长内容分段发送
企业微信 Markdown 通知模块。
支持加粗、颜色info/comment/warning新增接口与变更接口使用不同展示模板
"""
import json
import re
from typing import List, Optional
import requests
from comparator import EndpointChangeReport
from git_utils import CommitInfo
# 企微 text 消息字节上限约 2048留余量按字符分段
MAX_TEXT_LENGTH = 2000
# 企微 Markdown 单条上限 4096 字符,留余量
MAX_MD_LENGTH = 3800
def truncate_text(text: str, max_length: int = MAX_TEXT_LENGTH) -> str:
"""
截断文本,避免超出企微单条消息限制。
:param text: 原始文本
:param max_length: 最大字符数
:return: 截断后文本
"""
def truncate_text(text: str, max_length: int = MAX_MD_LENGTH) -> str:
"""截断超长消息。"""
if len(text) <= max_length:
return text
return text[:max_length] + "\n... [消息过长,已截断]"
return text[:max_length] + "\n\n<font color=\"comment\">... 消息过长,已截断</font>"
def build_single_endpoint_message(
report: EndpointChangeReport,
push_user: str,
push_time: str,
) -> str:
def _format_endpoint_block(report: EndpointChangeReport) -> str:
"""
按第一版模板构建单个接口的通知正文
格式化单个接口块
模板示例:
[API变更通知]
URI: GET /api/users/{id}
修改人:张三
修改时间2026-06-03 10:00:00
参数变更:
- 删除: Boolean userType
- 新增: Boolean includeDisabled (是否必填false)
:param report: 单个接口变更报告
:param push_user: 代码推送人
:param push_time: 推送时间
:return: 通知文本
新增接口:只列参数,不出现「参数变更」字样。
变更接口:用颜色区分增删改。
删除接口:整接口标红。
"""
lines = [
"[API变更通知]",
f"URI: {report.http_method} {report.uri}",
f"修改人:{push_user}",
f"修改时间:{push_time}",
]
uri_line = f"**{report.http_method}** `{report.uri}`"
if report.is_removed_endpoint:
return (
f"### <font color=\"warning\">【已删除接口】</font>\n"
f"{uri_line}\n"
f"<font color=\"comment\">该接口已被移除</font>"
)
if report.is_new_endpoint:
lines.append("接口状态:新增接口")
lines = [
f"### <font color=\"info\">【新增接口】</font> {uri_line}",
]
if report.parameter_changes:
lines.append("参数变更:")
for change in report.parameter_changes:
lines.append(change.to_display_line())
elif report.is_removed_endpoint:
lines.append("接口状态:已删除接口")
lines.append(" - 整个接口已被移除")
else:
lines.append("参数变更:")
for change in report.parameter_changes:
lines.append(change.to_display_line())
lines.append(change.to_markdown_line(plain=True))
else:
lines.append('<font color="comment">无入参</font>')
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('<font color="comment">(无参数变化)</font>')
return "\n".join(lines)
def build_all_notifications(
def build_markdown_notification(
reports: List[EndpointChangeReport],
push_user: str,
push_time: str,
llm_review: Optional[str] = None,
) -> List[str]:
llm_summary: Optional[str] = None,
) -> str:
"""
将所有接口变更组装为通知消息列表,超长时自动分段
构建完整 Markdown 通知正文
:param reports: 变更报告列表
:param push_user: 推送人
:param push_time: 推送时间
:param llm_review: LLM 参数变更审核结论(可选,附在最后一条或单独一条
:return: 待发送的消息段落列表
:param reports: AST 变更报告
:param push_user: 推送人
:param push_time: 推送时间
:param llm_summary: LLM 兼容性摘要(可选,简短
:return: Markdown 文本
"""
if not reports:
return []
parts = [
"## <font color=\"warning\">API 接口参数变更通知</font>",
f"**修改人:** {push_user}",
f"**修改时间:** {push_time}",
"",
]
messages: List[str] = []
current = ""
# 新增 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]
for report in reports:
block = build_single_endpoint_message(report, push_user, push_time)
separator = "\n\n---\n\n"
if new_reports:
controllers = sorted({r.controller_class for r in new_reports})
parts.append(f"### <font color=\"info\">新增 Controller</font> `{', '.join(controllers)}`")
parts.append("")
for report in new_reports:
parts.append(_format_endpoint_block(report))
parts.append("")
if not current:
current = block
elif len(current) + len(separator) + len(block) <= MAX_TEXT_LENGTH:
current += separator + block
else:
messages.append(current)
current = block
if changed_reports:
parts.append("### <font color=\"warning\">接口参数变更</font>")
parts.append("")
for report in changed_reports:
parts.append(_format_endpoint_block(report))
parts.append("")
if current:
messages.append(current)
if removed_reports:
parts.append("### <font color=\"warning\">已删除接口</font>")
parts.append("")
for report in removed_reports:
parts.append(_format_endpoint_block(report))
parts.append("")
# LLM 审核结论单独或追加发送
if llm_review:
review_msg = f"[AI参数变更审核]\n修改人:{push_user}\n修改时间:{push_time}\n\n{llm_review}"
if messages and len(messages[-1]) + len(review_msg) + 4 <= MAX_TEXT_LENGTH:
messages[-1] += "\n\n" + review_msg
elif len(review_msg) <= MAX_TEXT_LENGTH:
messages.append(review_msg)
else:
messages.extend(_split_long_text(review_msg, MAX_TEXT_LENGTH))
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("### <font color=\"comment\">兼容性提示</font>")
parts.append(cleaned)
return messages
return "\n".join(parts).strip()
def _split_long_text(text: str, max_len: int) -> List[str]:
"""行拆分超长文本"""
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 = ""
current: List[str] = []
for line in lines:
candidate = current + line + "\n"
if len(candidate) > max_len and current:
chunks.append(current.rstrip())
current = line + "\n"
if line.startswith("### ") and current and len("\n".join(current)) > 200:
chunks.append("\n".join(current))
current = [line]
else:
current = candidate
if current.strip():
chunks.append(current.rstrip())
return chunks
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_text(webhook_url: str, content: str) -> bool:
"""
发送单条 text 消息到企业微信。
:param webhook_url: Webhook URL
:param content: 消息正文
:return: 是否成功
"""
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[:800])
print(content[:1000])
return False
payload = {
"msgtype": "text",
"text": {"content": truncate_text(content)},
"msgtype": "markdown",
"markdown": {"content": truncate_text(content)},
}
try:
@@ -184,32 +196,30 @@ def send_parameter_change_notification(
mentioned_users: Optional[List[str]] = None,
) -> int:
"""
发送 Controller 接口参数变更通知(支持分段)
发送 Markdown 格式的接口参数变更通知。
:param webhook_url: 企微 Webhook
:param reports: AST 参数变更报告
:param reports: 变更报告
:param push_user: 推送人
:param push_time: 推送时间
:param llm_review: LLM 参数变更审核(可选)
:param mentioned_users: @ 成员 userid 列表
:param llm_review: LLM 兼容性摘要
:param mentioned_users: @ 成员Markdown 暂不支持,保留参数)
:return: 成功发送条数
"""
if not reports and not llm_review:
print("无接口参数变更,不发送到企业微信")
return 0
segments = build_all_notifications(reports, push_user, push_time, llm_review)
if not segments:
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):
payload_text = segment
if mentioned_users and i == 0:
# text 类型 @ 成员需放在 mentioned_list
pass # 在 _post 里处理较复杂,首条单独带 mentioned
if _post_wecom_text(webhook_url, payload_text):
if i > 0:
segment = (
f"<font color=\"comment\">(续 {i + 1}/{len(segments)}</font>\n\n" + segment
)
if _post_wecom_markdown(webhook_url, segment):
sent += 1
print(f"{sent} 条通知已发送到企业微信")