This commit is contained in:
@@ -32,25 +32,47 @@ class ParameterChange:
|
|||||||
old_required: Optional[bool] = None
|
old_required: Optional[bool] = None
|
||||||
detail: Optional[str] = None
|
detail: Optional[str] = None
|
||||||
|
|
||||||
def to_display_line(self) -> str:
|
def to_markdown_line(self, *, plain: bool = False) -> str:
|
||||||
"""
|
"""
|
||||||
格式化为通知模板中的一行文本。
|
格式化为企微 Markdown 行。
|
||||||
|
plain=True 时用于新增接口,直接列出参数,不加「新增」前缀。
|
||||||
|
"""
|
||||||
|
req_optional = self.required is False
|
||||||
|
req_required = self.required is True
|
||||||
|
|
||||||
|
if plain and self.change_type == ChangeType.ADDED:
|
||||||
|
tag = (
|
||||||
|
'<font color="warning">必填</font>'
|
||||||
|
if req_required
|
||||||
|
else '<font color="comment">可选</font>'
|
||||||
|
)
|
||||||
|
return f'> `{self.param_type}` **{self.param_name}** · {tag}'
|
||||||
|
|
||||||
:return: 如 "删除: Boolean userType" 或 "重命名: String userName -> String accountName"
|
|
||||||
"""
|
|
||||||
if self.change_type == ChangeType.REMOVED:
|
if self.change_type == ChangeType.REMOVED:
|
||||||
return f" - 删除: {self.param_type} {self.param_name}"
|
return (
|
||||||
|
f'<font color="warning">【删除】</font> '
|
||||||
|
f'`{self.param_type}` ~~{self.param_name}~~'
|
||||||
|
)
|
||||||
if self.change_type == ChangeType.ADDED:
|
if self.change_type == ChangeType.ADDED:
|
||||||
req_text = f" (是否必填:{str(self.required).lower()})" if self.required is not None else ""
|
tag = (
|
||||||
return f" - 新增: {self.param_type} {self.param_name}{req_text}"
|
'<font color="warning">必填</font>'
|
||||||
|
if req_required
|
||||||
|
else '<font color="comment">可选</font>'
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f'<font color="info">【新增】</font> '
|
||||||
|
f'`{self.param_type}` **{self.param_name}** · {tag}'
|
||||||
|
)
|
||||||
if self.change_type == ChangeType.RENAMED:
|
if self.change_type == ChangeType.RENAMED:
|
||||||
return f" - 重命名: {self.old_type} {self.old_name} -> {self.param_type} {self.param_name}"
|
return (
|
||||||
|
f'<font color="comment">【重命名】</font> '
|
||||||
|
f'`{self.old_type}` {self.old_name} → '
|
||||||
|
f'`{self.param_type}` **{self.param_name}**'
|
||||||
|
)
|
||||||
if self.change_type == ChangeType.MODIFIED:
|
if self.change_type == ChangeType.MODIFIED:
|
||||||
parts = [f" - 修改: {self.param_name}"]
|
detail = f' · <font color="comment">{self.detail}</font>' if self.detail else ""
|
||||||
if self.detail:
|
return f'<font color="warning">【修改】</font> **{self.param_name}**{detail}'
|
||||||
parts.append(f" ({self.detail})")
|
return f'- {self.param_name}'
|
||||||
return "".join(parts)
|
|
||||||
return f" - {self.change_type.value}: {self.param_name}"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -26,6 +26,44 @@ from models import ApiEndpoint, ApiParameter
|
|||||||
MAPPING_ANNS = {"GetMapping", "PostMapping", "PutMapping", "DeleteMapping", "PatchMapping", "RequestMapping"}
|
MAPPING_ANNS = {"GetMapping", "PostMapping", "PutMapping", "DeleteMapping", "PatchMapping", "RequestMapping"}
|
||||||
CONTROLLER_ANNS = {"RestController", "Controller"}
|
CONTROLLER_ANNS = {"RestController", "Controller"}
|
||||||
|
|
||||||
|
# Spring MVC 框架自动注入参数,不属于 API 调用方入参,解析时忽略
|
||||||
|
FRAMEWORK_PARAM_TYPES = {
|
||||||
|
"HttpServletRequest",
|
||||||
|
"HttpServletResponse",
|
||||||
|
"HttpSession",
|
||||||
|
"ServletRequest",
|
||||||
|
"ServletResponse",
|
||||||
|
"WebRequest",
|
||||||
|
"NativeWebRequest",
|
||||||
|
"Model",
|
||||||
|
"ModelMap",
|
||||||
|
"RedirectAttributes",
|
||||||
|
"BindingResult",
|
||||||
|
"Errors",
|
||||||
|
"Authentication",
|
||||||
|
"Principal",
|
||||||
|
"Locale",
|
||||||
|
"TimeZone",
|
||||||
|
"InputStream",
|
||||||
|
"OutputStream",
|
||||||
|
"Reader",
|
||||||
|
"Writer",
|
||||||
|
"HttpHeaders",
|
||||||
|
"UriComponentsBuilder",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_framework_param(type_name: str, param_name: str) -> bool:
|
||||||
|
"""判断是否为框架注入参数(非 API 调用方需要传递)。"""
|
||||||
|
simple = type_name.split(".")[-1].replace(">", "").replace("<", "").strip()
|
||||||
|
if simple in FRAMEWORK_PARAM_TYPES:
|
||||||
|
return True
|
||||||
|
if param_name in ("request", "response") and (
|
||||||
|
simple.endswith("Request") or simple.endswith("Response")
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _ann_simple_name(ann: Annotation) -> str:
|
def _ann_simple_name(ann: Annotation) -> str:
|
||||||
"""获取注解简单类名。"""
|
"""获取注解简单类名。"""
|
||||||
@@ -272,14 +310,19 @@ class ControllerAstParser:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _extract_param(self, param: FormalParameter) -> List[ApiParameter]:
|
def _extract_param(self, param: FormalParameter) -> List[ApiParameter]:
|
||||||
"""提取方法参数,@RequestBody 展开 DTO 字段。"""
|
"""提取方法参数,@RequestBody 展开 DTO 字段;忽略框架注入参数。"""
|
||||||
type_name = _type_to_str(param.type)
|
type_name = _type_to_str(param.type)
|
||||||
|
name = _param_name(param)
|
||||||
|
|
||||||
|
if _is_framework_param(type_name, name):
|
||||||
|
return []
|
||||||
|
|
||||||
if _has_ann(param, "RequestBody"):
|
if _has_ann(param, "RequestBody"):
|
||||||
return self._expand_dto(type_name, "body")
|
return self._expand_dto(type_name, "body")
|
||||||
|
|
||||||
return [
|
return [
|
||||||
ApiParameter(
|
ApiParameter(
|
||||||
name=_param_name(param),
|
name=name,
|
||||||
type=type_name,
|
type=type_name,
|
||||||
required=_param_required(param),
|
required=_param_required(param),
|
||||||
source=_param_source(param),
|
source=_param_source(param),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
豆包 LLM 接口参数变更审核模块。
|
豆包 LLM 接口参数变更审核模块。
|
||||||
仅审核 Controller 层接口参数的增删改,不对 Java 源码做通用代码审查。
|
LLM 仅输出简短的兼容性提示,详细变更由 AST + Markdown 通知展示。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -10,14 +10,17 @@ import requests
|
|||||||
|
|
||||||
from comparator import EndpointChangeReport
|
from comparator import EndpointChangeReport
|
||||||
|
|
||||||
|
# 写入 prompt,不在通知中展示
|
||||||
|
FRAMEWORK_IGNORE_HINT = """
|
||||||
|
以下参数类型/名称属于 Spring MVC 框架自动注入,不是 API 调用方入参,审核时必须忽略,不要在结果中提及:
|
||||||
|
HttpServletRequest、HttpServletResponse、HttpSession、ServletRequest、ServletResponse、
|
||||||
|
WebRequest、NativeWebRequest、Model、ModelMap、RedirectAttributes、BindingResult、
|
||||||
|
Authentication、Principal 等。
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def is_llm_enabled(config: Dict[str, Any]) -> bool:
|
def is_llm_enabled(config: Dict[str, Any]) -> bool:
|
||||||
"""
|
"""判断大模型总开关是否开启。"""
|
||||||
判断大模型总开关是否开启。
|
|
||||||
|
|
||||||
:param config: 完整配置字典
|
|
||||||
:return: True=启用 LLM 审核
|
|
||||||
"""
|
|
||||||
return config.get("llm", {}).get("enabled", True)
|
return config.get("llm", {}).get("enabled", True)
|
||||||
|
|
||||||
|
|
||||||
@@ -26,14 +29,7 @@ def call_doubao_api(
|
|||||||
prompt: str,
|
prompt: str,
|
||||||
config: Dict[str, Any],
|
config: Dict[str, Any],
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""
|
"""调用豆包 API。"""
|
||||||
调用火山引擎豆包 Chat Completions API。
|
|
||||||
|
|
||||||
:param api_key: API Key
|
|
||||||
:param prompt: 用户提示词
|
|
||||||
:param config: 完整配置
|
|
||||||
:return: LLM 回复文本;失败返回 None
|
|
||||||
"""
|
|
||||||
if not api_key or api_key == "YOUR_DOUBAO_API_KEY":
|
if not api_key or api_key == "YOUR_DOUBAO_API_KEY":
|
||||||
print("[警告] 未配置豆包 API Key,跳过 LLM 审核。")
|
print("[警告] 未配置豆包 API Key,跳过 LLM 审核。")
|
||||||
return None
|
return None
|
||||||
@@ -41,7 +37,7 @@ def call_doubao_api(
|
|||||||
llm_cfg = config.get("llm", {})
|
llm_cfg = config.get("llm", {})
|
||||||
model = llm_cfg.get("model") or llm_cfg.get("endpoint_id", "")
|
model = llm_cfg.get("model") or llm_cfg.get("endpoint_id", "")
|
||||||
if not model:
|
if not model:
|
||||||
print("[警告] 未配置 llm.model 或 llm.endpoint_id,跳过 LLM 审核。")
|
print("[警告] 未配置 llm.model,跳过 LLM 审核。")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
api_url = llm_cfg.get(
|
api_url = llm_cfg.get(
|
||||||
@@ -59,9 +55,9 @@ def call_doubao_api(
|
|||||||
{
|
{
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": (
|
"content": (
|
||||||
"你是 Java Spring Boot Controller 接口参数变更分析专家。"
|
"你是 Java Spring Boot API 变更分析专家。"
|
||||||
"你的职责是识别并整理 Controller 层接口参数的增、删、改、重命名,"
|
"你只负责输出简短的兼容性风险提示,不重复罗列接口参数明细。"
|
||||||
"确认 AST 解析结果是否准确,并指出对调用方的兼容性影响。"
|
+ FRAMEWORK_IGNORE_HINT
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{"role": "user", "content": prompt},
|
{"role": "user", "content": prompt},
|
||||||
@@ -73,13 +69,11 @@ def call_doubao_api(
|
|||||||
kwargs = {"headers": headers, "json": payload}
|
kwargs = {"headers": headers, "json": payload}
|
||||||
if timeout is not None:
|
if timeout is not None:
|
||||||
kwargs["timeout"] = timeout
|
kwargs["timeout"] = timeout
|
||||||
|
|
||||||
resp = requests.post(api_url, **kwargs)
|
resp = requests.post(api_url, **kwargs)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
if "choices" in data and data["choices"]:
|
if "choices" in data and data["choices"]:
|
||||||
return data["choices"][0]["message"]["content"]
|
return data["choices"][0]["message"]["content"]
|
||||||
print("[错误] AI 返回格式异常")
|
|
||||||
return None
|
return None
|
||||||
except requests.RequestException as exc:
|
except requests.RequestException as exc:
|
||||||
print(f"[错误] 豆包 API 调用失败: {exc}")
|
print(f"[错误] 豆包 API 调用失败: {exc}")
|
||||||
@@ -92,66 +86,49 @@ def build_parameter_change_prompt(
|
|||||||
git_diff: str = "",
|
git_diff: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
构造接口参数变更审核提示词(对齐第一版需求:识别 Controller 参数增删改)。
|
构造 LLM 提示词:只要求输出兼容性摘要,不要求重复参数列表。
|
||||||
|
|
||||||
:param reports: AST 解析对比结果
|
|
||||||
:param changed_files: 本次变更的 Controller 文件列表
|
|
||||||
:param git_diff: 相关文件的 Git diff 内容
|
|
||||||
:return: 完整 prompt
|
|
||||||
"""
|
"""
|
||||||
ast_report = []
|
ast_report = []
|
||||||
for r in reports:
|
for r in reports:
|
||||||
ast_report.append(
|
ast_report.append(
|
||||||
{
|
{
|
||||||
"uri": f"{r.http_method} {r.uri}",
|
"uri": f"{r.http_method} {r.uri}",
|
||||||
"controller": r.controller_class,
|
"is_new": r.is_new_endpoint,
|
||||||
"method": r.method_name,
|
"is_removed": r.is_removed_endpoint,
|
||||||
"is_new_endpoint": r.is_new_endpoint,
|
"changes": [
|
||||||
"is_removed_endpoint": r.is_removed_endpoint,
|
|
||||||
"parameter_changes": [
|
|
||||||
{
|
{
|
||||||
"type": c.change_type.value,
|
"type": c.change_type.value,
|
||||||
"name": c.param_name,
|
"name": c.param_name,
|
||||||
"java_type": c.param_type,
|
"java_type": c.param_type,
|
||||||
"old_name": c.old_name,
|
|
||||||
"old_type": c.old_type,
|
|
||||||
"required": c.required,
|
"required": c.required,
|
||||||
"detail": c.detail,
|
|
||||||
}
|
}
|
||||||
for c in r.parameter_changes
|
for c in r.parameter_changes
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
diff_block = git_diff.strip() if git_diff.strip() else "(无 diff 内容)"
|
diff_block = git_diff.strip()[:6000] if git_diff.strip() else "(无)"
|
||||||
if len(diff_block) > 8000:
|
|
||||||
diff_block = diff_block[:8000] + "\n... [diff 过长,已截断]"
|
|
||||||
|
|
||||||
return f"""请审核以下 Controller 层接口参数变更,整理并确认变更结果。
|
return f"""请根据以下 Controller 接口参数变更,**仅输出「兼容性提示」**,要求:
|
||||||
|
|
||||||
## 变更的 Controller 文件
|
{FRAMEWORK_IGNORE_HINT}
|
||||||
|
|
||||||
|
## 输出格式(严格遵守)
|
||||||
|
- 只输出 3~6 行 Markdown,不要输出「整体说明」「接口变更详情」等标题
|
||||||
|
- 不要逐条重复 URI 和参数列表(通知里已有)
|
||||||
|
- 不要提及「排除框架注入」相关字样
|
||||||
|
- 重点说明:是否有破坏性变更、哪些必填参数调用方必须传入
|
||||||
|
- 全新 Controller 说明「均为新接口,对现有调用方无破坏」即可
|
||||||
|
- 语气简洁,可用 <font color="warning">...</font> 标注风险项
|
||||||
|
|
||||||
|
## 变更文件
|
||||||
{json.dumps(changed_files, ensure_ascii=False)}
|
{json.dumps(changed_files, ensure_ascii=False)}
|
||||||
|
|
||||||
## AST 自动解析的参数变更报告
|
## AST 变更摘要
|
||||||
{json.dumps(ast_report, ensure_ascii=False, indent=2)}
|
{json.dumps(ast_report, ensure_ascii=False, indent=2)}
|
||||||
|
|
||||||
## Git Diff(Controller 相关)
|
## Git Diff
|
||||||
```diff
|
|
||||||
{diff_block}
|
{diff_block}
|
||||||
```
|
|
||||||
|
|
||||||
## 审核要求
|
|
||||||
1. 逐条确认 AST 报告的参数变更是否准确(增/删/改/重命名)
|
|
||||||
2. 若 AST 有遗漏,补充遗漏的接口参数变更
|
|
||||||
3. 若 AST 有误报,指出并修正
|
|
||||||
4. 按以下格式整理每个接口的变更(与通知模板一致):
|
|
||||||
URI: GET /api/xxx
|
|
||||||
参数变更:
|
|
||||||
- 删除: Boolean userType
|
|
||||||
- 新增: Boolean includeDisabled (是否必填:false)
|
|
||||||
- 重命名: String userName -> String accountName
|
|
||||||
5. 简要说明是否存在破坏性变更(影响前端/调用方)
|
|
||||||
6. 用中文回复,简洁清晰
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -161,20 +138,8 @@ def review_parameter_changes(
|
|||||||
changed_files: List[str],
|
changed_files: List[str],
|
||||||
git_diff: str = "",
|
git_diff: str = "",
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""
|
"""LLM 审核,返回简短兼容性提示。"""
|
||||||
使用 LLM 审核 Controller 接口参数变更(AST 结果的二次确认与整理)。
|
if not is_llm_enabled(config) or not reports:
|
||||||
|
|
||||||
:param reports: AST 对比报告
|
|
||||||
:param config: 完整配置
|
|
||||||
:param changed_files: 变更的 Controller 文件
|
|
||||||
:param git_diff: Git diff 文本
|
|
||||||
:return: LLM 整理后的审核结论;未启用或无报告时返回 None
|
|
||||||
"""
|
|
||||||
if not is_llm_enabled(config):
|
|
||||||
print("[LLM] 大模型开关已关闭,跳过接口参数变更审核。")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not reports:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
llm_cfg = config.get("llm", {})
|
llm_cfg = config.get("llm", {})
|
||||||
|
|||||||
@@ -1,162 +1,174 @@
|
|||||||
"""
|
"""
|
||||||
企业微信机器人通知模块。
|
企业微信 Markdown 通知模块。
|
||||||
按第一版模板发送 Controller 接口参数变更通知,支持超长内容分段发送。
|
支持加粗、颜色(info/comment/warning),新增接口与变更接口使用不同展示模板。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from comparator import EndpointChangeReport
|
from comparator import EndpointChangeReport
|
||||||
from git_utils import CommitInfo
|
|
||||||
|
|
||||||
# 企微 text 消息字节上限约 2048,留余量按字符分段
|
# 企微 Markdown 单条上限 4096 字符,留余量
|
||||||
MAX_TEXT_LENGTH = 2000
|
MAX_MD_LENGTH = 3800
|
||||||
|
|
||||||
|
|
||||||
def truncate_text(text: str, max_length: int = MAX_TEXT_LENGTH) -> str:
|
def truncate_text(text: str, max_length: int = MAX_MD_LENGTH) -> str:
|
||||||
"""
|
"""截断超长消息。"""
|
||||||
截断文本,避免超出企微单条消息限制。
|
|
||||||
|
|
||||||
:param text: 原始文本
|
|
||||||
:param max_length: 最大字符数
|
|
||||||
:return: 截断后文本
|
|
||||||
"""
|
|
||||||
if len(text) <= max_length:
|
if len(text) <= max_length:
|
||||||
return text
|
return text
|
||||||
return text[:max_length] + "\n... [消息过长,已截断]"
|
return text[:max_length] + "\n\n<font color=\"comment\">... 消息过长,已截断</font>"
|
||||||
|
|
||||||
|
|
||||||
def build_single_endpoint_message(
|
def _format_endpoint_block(report: EndpointChangeReport) -> str:
|
||||||
report: EndpointChangeReport,
|
|
||||||
push_user: str,
|
|
||||||
push_time: str,
|
|
||||||
) -> 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 = [
|
uri_line = f"**{report.http_method}** `{report.uri}`"
|
||||||
"[API变更通知]",
|
|
||||||
f"URI: {report.http_method} {report.uri}",
|
if report.is_removed_endpoint:
|
||||||
f"修改人:{push_user}",
|
return (
|
||||||
f"修改时间:{push_time}",
|
f"### <font color=\"warning\">【已删除接口】</font>\n"
|
||||||
]
|
f"{uri_line}\n"
|
||||||
|
f"<font color=\"comment\">该接口已被移除</font>"
|
||||||
|
)
|
||||||
|
|
||||||
if report.is_new_endpoint:
|
if report.is_new_endpoint:
|
||||||
lines.append("接口状态:新增接口")
|
lines = [
|
||||||
|
f"### <font color=\"info\">【新增接口】</font> {uri_line}",
|
||||||
|
]
|
||||||
if report.parameter_changes:
|
if report.parameter_changes:
|
||||||
lines.append("参数变更:")
|
|
||||||
for change in report.parameter_changes:
|
for change in report.parameter_changes:
|
||||||
lines.append(change.to_display_line())
|
lines.append(change.to_markdown_line(plain=True))
|
||||||
elif report.is_removed_endpoint:
|
|
||||||
lines.append("接口状态:已删除接口")
|
|
||||||
lines.append(" - 整个接口已被移除")
|
|
||||||
else:
|
else:
|
||||||
lines.append("参数变更:")
|
lines.append('<font color="comment">无入参</font>')
|
||||||
for change in report.parameter_changes:
|
return "\n".join(lines)
|
||||||
lines.append(change.to_display_line())
|
|
||||||
|
|
||||||
|
# 已有接口的参数变更
|
||||||
|
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)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def build_all_notifications(
|
def build_markdown_notification(
|
||||||
reports: List[EndpointChangeReport],
|
reports: List[EndpointChangeReport],
|
||||||
push_user: str,
|
push_user: str,
|
||||||
push_time: str,
|
push_time: str,
|
||||||
llm_review: Optional[str] = None,
|
llm_summary: Optional[str] = None,
|
||||||
) -> List[str]:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
将所有接口变更组装为通知消息列表,超长时自动分段。
|
构建完整 Markdown 通知正文。
|
||||||
|
|
||||||
:param reports: 变更报告列表
|
:param reports: AST 变更报告
|
||||||
:param push_user: 推送人
|
:param push_user: 推送人
|
||||||
:param push_time: 推送时间
|
:param push_time: 推送时间
|
||||||
:param llm_review: LLM 参数变更审核结论(可选,附在最后一条或单独一条)
|
:param llm_summary: LLM 兼容性摘要(可选,简短)
|
||||||
:return: 待发送的消息段落列表
|
:return: Markdown 文本
|
||||||
"""
|
"""
|
||||||
if not reports:
|
parts = [
|
||||||
return []
|
"## <font color=\"warning\">API 接口参数变更通知</font>",
|
||||||
|
f"**修改人:** {push_user}",
|
||||||
|
f"**修改时间:** {push_time}",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
messages: List[str] = []
|
# 新增 Controller(全部为新接口)与变更接口分组
|
||||||
current = ""
|
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:
|
if new_reports:
|
||||||
block = build_single_endpoint_message(report, push_user, push_time)
|
controllers = sorted({r.controller_class for r in new_reports})
|
||||||
separator = "\n\n---\n\n"
|
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:
|
if changed_reports:
|
||||||
current = block
|
parts.append("### <font color=\"warning\">接口参数变更</font>")
|
||||||
elif len(current) + len(separator) + len(block) <= MAX_TEXT_LENGTH:
|
parts.append("")
|
||||||
current += separator + block
|
for report in changed_reports:
|
||||||
else:
|
parts.append(_format_endpoint_block(report))
|
||||||
messages.append(current)
|
parts.append("")
|
||||||
current = block
|
|
||||||
|
|
||||||
if current:
|
if removed_reports:
|
||||||
messages.append(current)
|
parts.append("### <font color=\"warning\">已删除接口</font>")
|
||||||
|
parts.append("")
|
||||||
|
for report in removed_reports:
|
||||||
|
parts.append(_format_endpoint_block(report))
|
||||||
|
parts.append("")
|
||||||
|
|
||||||
# LLM 审核结论单独或追加发送
|
if llm_summary:
|
||||||
if llm_review:
|
cleaned = llm_summary.strip()
|
||||||
review_msg = f"[AI参数变更审核]\n修改人:{push_user}\n修改时间:{push_time}\n\n{llm_review}"
|
# 去掉 LLM 可能输出的「排除框架注入」类说明
|
||||||
if messages and len(messages[-1]) + len(review_msg) + 4 <= MAX_TEXT_LENGTH:
|
cleaned = re.sub(
|
||||||
messages[-1] += "\n\n" + review_msg
|
r"(排除Spring MVC框架自动注入的[^)]+)",
|
||||||
elif len(review_msg) <= MAX_TEXT_LENGTH:
|
"",
|
||||||
messages.append(review_msg)
|
cleaned,
|
||||||
else:
|
)
|
||||||
messages.extend(_split_long_text(review_msg, MAX_TEXT_LENGTH))
|
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")
|
lines = text.split("\n")
|
||||||
chunks: List[str] = []
|
chunks: List[str] = []
|
||||||
current = ""
|
current: List[str] = []
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
candidate = current + line + "\n"
|
if line.startswith("### ") and current and len("\n".join(current)) > 200:
|
||||||
if len(candidate) > max_len and current:
|
chunks.append("\n".join(current))
|
||||||
chunks.append(current.rstrip())
|
current = [line]
|
||||||
current = line + "\n"
|
|
||||||
else:
|
else:
|
||||||
current = candidate
|
current.append(line)
|
||||||
if current.strip():
|
if len("\n".join(current)) >= max_len:
|
||||||
chunks.append(current.rstrip())
|
chunks.append("\n".join(current))
|
||||||
return chunks
|
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:
|
def _post_wecom_markdown(webhook_url: str, content: str) -> bool:
|
||||||
"""
|
"""发送企微 Markdown 消息。"""
|
||||||
发送单条 text 消息到企业微信。
|
|
||||||
|
|
||||||
:param webhook_url: Webhook URL
|
|
||||||
:param content: 消息正文
|
|
||||||
:return: 是否成功
|
|
||||||
"""
|
|
||||||
if not webhook_url or "YOUR_WECOM_KEY" in webhook_url:
|
if not webhook_url or "YOUR_WECOM_KEY" in webhook_url:
|
||||||
print("[警告] 未配置有效的企业微信 Webhook URL。")
|
print("[警告] 未配置有效的企业微信 Webhook URL。")
|
||||||
print("--- 通知预览 ---")
|
print("--- 通知预览 ---")
|
||||||
print(content[:800])
|
print(content[:1000])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"msgtype": "text",
|
"msgtype": "markdown",
|
||||||
"text": {"content": truncate_text(content)},
|
"markdown": {"content": truncate_text(content)},
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -184,32 +196,30 @@ def send_parameter_change_notification(
|
|||||||
mentioned_users: Optional[List[str]] = None,
|
mentioned_users: Optional[List[str]] = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""
|
"""
|
||||||
发送 Controller 接口参数变更通知(支持分段)。
|
发送 Markdown 格式的接口参数变更通知。
|
||||||
|
|
||||||
:param webhook_url: 企微 Webhook
|
:param webhook_url: 企微 Webhook
|
||||||
:param reports: AST 参数变更报告
|
:param reports: 变更报告
|
||||||
:param push_user: 推送人
|
:param push_user: 推送人
|
||||||
:param push_time: 推送时间
|
:param push_time: 推送时间
|
||||||
:param llm_review: LLM 参数变更审核(可选)
|
:param llm_review: LLM 兼容性摘要
|
||||||
:param mentioned_users: @ 成员 userid 列表
|
:param mentioned_users: @ 成员(Markdown 暂不支持,保留参数)
|
||||||
:return: 成功发送条数
|
:return: 成功发送条数
|
||||||
"""
|
"""
|
||||||
if not reports and not llm_review:
|
if not reports and not llm_review:
|
||||||
print("无接口参数变更,不发送到企业微信")
|
print("无接口参数变更,不发送到企业微信")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
segments = build_all_notifications(reports, push_user, push_time, llm_review)
|
full_md = build_markdown_notification(reports, push_user, push_time, llm_review)
|
||||||
if not segments:
|
segments = _split_markdown(full_md, MAX_MD_LENGTH)
|
||||||
return 0
|
|
||||||
|
|
||||||
sent = 0
|
sent = 0
|
||||||
for i, segment in enumerate(segments):
|
for i, segment in enumerate(segments):
|
||||||
payload_text = segment
|
if i > 0:
|
||||||
if mentioned_users and i == 0:
|
segment = (
|
||||||
# text 类型 @ 成员需放在 mentioned_list
|
f"<font color=\"comment\">(续 {i + 1}/{len(segments)})</font>\n\n" + segment
|
||||||
pass # 在 _post 里处理较复杂,首条单独带 mentioned
|
)
|
||||||
|
if _post_wecom_markdown(webhook_url, segment):
|
||||||
if _post_wecom_text(webhook_url, payload_text):
|
|
||||||
sent += 1
|
sent += 1
|
||||||
print(f"第 {sent} 条通知已发送到企业微信")
|
print(f"第 {sent} 条通知已发送到企业微信")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user