This commit is contained in:
165
.gitea/checker/change_logger.py
Normal file
165
.gitea/checker/change_logger.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
变更日志持久化模块。
|
||||
受 log.enabled 开关控制,默认关闭;仅记录接口参数变更及 LLM 审核结果。
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from comparator import EndpointChangeReport
|
||||
from git_utils import CommitInfo
|
||||
|
||||
|
||||
def is_log_enabled(config: Dict[str, Any]) -> bool:
|
||||
"""判断日志总开关是否开启。"""
|
||||
return config.get("log", {}).get("enabled", False)
|
||||
|
||||
|
||||
def _serialize_reports(reports: List[EndpointChangeReport]) -> List[dict]:
|
||||
"""将参数变更报告序列化为 JSON 结构。"""
|
||||
result = []
|
||||
for r in reports:
|
||||
result.append(
|
||||
{
|
||||
"uri": r.uri,
|
||||
"http_method": r.http_method,
|
||||
"controller_class": r.controller_class,
|
||||
"method_name": r.method_name,
|
||||
"is_new_endpoint": r.is_new_endpoint,
|
||||
"is_removed_endpoint": r.is_removed_endpoint,
|
||||
"parameter_changes": [
|
||||
{
|
||||
"change_type": c.change_type.value,
|
||||
"param_name": c.param_name,
|
||||
"param_type": c.param_type,
|
||||
"old_name": c.old_name,
|
||||
"old_type": c.old_type,
|
||||
"required": c.required,
|
||||
"detail": c.detail,
|
||||
}
|
||||
for c in r.parameter_changes
|
||||
],
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def save_to_file(
|
||||
reports: List[EndpointChangeReport],
|
||||
commit_info: CommitInfo,
|
||||
log_dir: str,
|
||||
llm_review: Optional[str] = None,
|
||||
) -> Path:
|
||||
"""写入 JSON 日志文件。"""
|
||||
log_path = Path(log_dir)
|
||||
log_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
short_sha = commit_info.sha[:8]
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output = log_path / f"{timestamp}_{short_sha}.json"
|
||||
|
||||
record = {
|
||||
"commit_sha": commit_info.sha,
|
||||
"author": commit_info.author,
|
||||
"commit_time": commit_info.commit_time,
|
||||
"message": commit_info.message,
|
||||
"detected_at": datetime.now().isoformat(),
|
||||
"change_count": len(reports),
|
||||
"parameter_changes": _serialize_reports(reports),
|
||||
"llm_review": llm_review,
|
||||
}
|
||||
|
||||
with open(output, "w", encoding="utf-8") as f:
|
||||
json.dump(record, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"[日志] 参数变更记录已写入: {output}")
|
||||
return output
|
||||
|
||||
|
||||
def save_to_mysql(
|
||||
reports: List[EndpointChangeReport],
|
||||
commit_info: CommitInfo,
|
||||
mysql_config: Dict[str, Any],
|
||||
llm_review: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""写入 MySQL。"""
|
||||
try:
|
||||
import pymysql
|
||||
except ImportError:
|
||||
print("[错误] MySQL 模式需要: pip install pymysql")
|
||||
return False
|
||||
|
||||
host = mysql_config.get("host", "")
|
||||
if not host or host == "YOUR_MYSQL_HOST":
|
||||
print("[警告] 未配置 MySQL,跳过写入。")
|
||||
return False
|
||||
|
||||
try:
|
||||
conn = pymysql.connect(
|
||||
host=host,
|
||||
port=int(mysql_config.get("port", 3306)),
|
||||
user=mysql_config.get("user"),
|
||||
password=mysql_config.get("password"),
|
||||
database=mysql_config.get("database"),
|
||||
charset="utf8mb4",
|
||||
)
|
||||
table = mysql_config.get("table", "api_change_logs")
|
||||
payload = json.dumps(_serialize_reports(reports), ensure_ascii=False)
|
||||
|
||||
with conn.cursor() as cursor:
|
||||
sql = f"""
|
||||
INSERT INTO `{table}`
|
||||
(commit_sha, author, commit_time, commit_message, change_count, reports_json, llm_review, created_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, NOW())
|
||||
"""
|
||||
cursor.execute(
|
||||
sql,
|
||||
(
|
||||
commit_info.sha,
|
||||
commit_info.author,
|
||||
commit_info.commit_time,
|
||||
commit_info.message,
|
||||
len(reports),
|
||||
payload,
|
||||
llm_review,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"[日志] 已写入 MySQL: {table}")
|
||||
return True
|
||||
except Exception as exc:
|
||||
print(f"[错误] MySQL 写入失败: {exc}")
|
||||
return False
|
||||
|
||||
|
||||
def persist_change_log(
|
||||
reports: List[EndpointChangeReport],
|
||||
commit_info: CommitInfo,
|
||||
config: Dict[str, Any],
|
||||
llm_review: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
根据 log.enabled 决定是否持久化接口参数变更日志。
|
||||
|
||||
:param reports: 参数变更报告
|
||||
:param commit_info: 提交信息
|
||||
:param config: 完整配置
|
||||
:param llm_review: LLM 参数变更审核结论
|
||||
"""
|
||||
if not is_log_enabled(config):
|
||||
print("[日志] 日志开关已关闭(log.enabled=false),跳过写入。")
|
||||
return
|
||||
|
||||
log_cfg = config.get("log", {})
|
||||
if log_cfg.get("storage") == "mysql":
|
||||
save_to_mysql(reports, commit_info, log_cfg.get("mysql", {}), llm_review)
|
||||
else:
|
||||
save_to_file(
|
||||
reports,
|
||||
commit_info,
|
||||
log_cfg.get("file_dir", ".gitea/logs/api-changes"),
|
||||
llm_review,
|
||||
)
|
||||
256
.gitea/checker/comparator.py
Normal file
256
.gitea/checker/comparator.py
Normal file
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
API 参数变更对比模块。
|
||||
对比新旧两个版本的 Controller 端点,识别参数的增、删、改、重命名。
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
|
||||
from controller_parser import ApiEndpoint, ApiParameter
|
||||
|
||||
|
||||
class ChangeType(str, Enum):
|
||||
"""参数变更类型。"""
|
||||
|
||||
ADDED = "added"
|
||||
REMOVED = "removed"
|
||||
MODIFIED = "modified"
|
||||
RENAMED = "renamed"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParameterChange:
|
||||
"""单条参数变更记录。"""
|
||||
|
||||
change_type: ChangeType
|
||||
param_name: str
|
||||
param_type: Optional[str] = None
|
||||
old_name: Optional[str] = None
|
||||
old_type: Optional[str] = None
|
||||
required: Optional[bool] = None
|
||||
old_required: Optional[bool] = None
|
||||
detail: Optional[str] = None
|
||||
|
||||
def to_display_line(self) -> str:
|
||||
"""
|
||||
格式化为通知模板中的一行文本。
|
||||
|
||||
:return: 如 "删除: Boolean userType" 或 "重命名: String userName -> String accountName"
|
||||
"""
|
||||
if self.change_type == ChangeType.REMOVED:
|
||||
return f" - 删除: {self.param_type} {self.param_name}"
|
||||
if self.change_type == ChangeType.ADDED:
|
||||
req_text = f" (是否必填:{str(self.required).lower()})" if self.required is not None else ""
|
||||
return f" - 新增: {self.param_type} {self.param_name}{req_text}"
|
||||
if self.change_type == ChangeType.RENAMED:
|
||||
return f" - 重命名: {self.old_type} {self.old_name} -> {self.param_type} {self.param_name}"
|
||||
if self.change_type == ChangeType.MODIFIED:
|
||||
parts = [f" - 修改: {self.param_name}"]
|
||||
if self.detail:
|
||||
parts.append(f" ({self.detail})")
|
||||
return "".join(parts)
|
||||
return f" - {self.change_type.value}: {self.param_name}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class EndpointChangeReport:
|
||||
"""单个接口的变更报告。"""
|
||||
|
||||
uri: str
|
||||
http_method: str
|
||||
controller_class: str
|
||||
method_name: str
|
||||
parameter_changes: List[ParameterChange] = field(default_factory=list)
|
||||
is_new_endpoint: bool = False
|
||||
is_removed_endpoint: bool = False
|
||||
|
||||
@property
|
||||
def has_changes(self) -> bool:
|
||||
"""是否存在任何变更。"""
|
||||
return (
|
||||
self.is_new_endpoint
|
||||
or self.is_removed_endpoint
|
||||
or len(self.parameter_changes) > 0
|
||||
)
|
||||
|
||||
@property
|
||||
def endpoint_key(self) -> str:
|
||||
return f"{self.http_method} {self.uri}"
|
||||
|
||||
|
||||
def _param_key(p: ApiParameter) -> Tuple[str, str]:
|
||||
"""参数匹配键:(source, name)。"""
|
||||
return (p.source, p.name)
|
||||
|
||||
|
||||
def compare_parameters(
|
||||
old_params: List[ApiParameter], new_params: List[ApiParameter]
|
||||
) -> List[ParameterChange]:
|
||||
"""
|
||||
对比同一接口新旧版本的参数列表,识别增删改及重命名。
|
||||
|
||||
重命名启发式:若删除与新增参数类型相同且 source 相同,则视为重命名。
|
||||
|
||||
:param old_params: 旧版本参数
|
||||
:param new_params: 新版本参数
|
||||
:return: 变更列表
|
||||
"""
|
||||
changes: List[ParameterChange] = []
|
||||
|
||||
old_map: Dict[Tuple[str, str], ApiParameter] = {_param_key(p): p for p in old_params}
|
||||
new_map: Dict[Tuple[str, str], ApiParameter] = {_param_key(p): p for p in new_params}
|
||||
|
||||
old_keys = set(old_map.keys())
|
||||
new_keys = set(new_map.keys())
|
||||
|
||||
removed_keys = old_keys - new_keys
|
||||
added_keys = new_keys - old_keys
|
||||
common_keys = old_keys & new_keys
|
||||
|
||||
# 1. 共同参数:检查类型、必填等属性变更
|
||||
for key in common_keys:
|
||||
old_p = old_map[key]
|
||||
new_p = new_map[key]
|
||||
detail_parts = []
|
||||
if old_p.type != new_p.type:
|
||||
detail_parts.append(f"类型 {old_p.type} -> {new_p.type}")
|
||||
if old_p.required != new_p.required:
|
||||
detail_parts.append(f"必填 {old_p.required} -> {new_p.required}")
|
||||
if detail_parts:
|
||||
changes.append(
|
||||
ParameterChange(
|
||||
change_type=ChangeType.MODIFIED,
|
||||
param_name=new_p.name,
|
||||
param_type=new_p.type,
|
||||
required=new_p.required,
|
||||
old_required=old_p.required,
|
||||
detail=", ".join(detail_parts),
|
||||
)
|
||||
)
|
||||
|
||||
# 2. 重命名检测:在 removed + added 中找同 type + source 的配对
|
||||
unmatched_removed: List[Tuple[Tuple[str, str], ApiParameter]] = []
|
||||
unmatched_added: List[Tuple[Tuple[str, str], ApiParameter]] = []
|
||||
|
||||
for key in removed_keys:
|
||||
unmatched_removed.append((key, old_map[key]))
|
||||
for key in added_keys:
|
||||
unmatched_added.append((key, new_map[key]))
|
||||
|
||||
matched_removed: Set[Tuple[str, str]] = set()
|
||||
matched_added: Set[Tuple[str, str]] = set()
|
||||
|
||||
for r_key, r_param in unmatched_removed:
|
||||
for a_key, a_param in unmatched_added:
|
||||
if a_key in matched_added:
|
||||
continue
|
||||
if r_param.type == a_param.type and r_param.source == a_param.source:
|
||||
changes.append(
|
||||
ParameterChange(
|
||||
change_type=ChangeType.RENAMED,
|
||||
param_name=a_param.name,
|
||||
param_type=a_param.type,
|
||||
old_name=r_param.name,
|
||||
old_type=r_param.type,
|
||||
required=a_param.required,
|
||||
)
|
||||
)
|
||||
matched_removed.add(r_key)
|
||||
matched_added.add(a_key)
|
||||
break
|
||||
|
||||
# 3. 纯删除
|
||||
for key, param in unmatched_removed:
|
||||
if key not in matched_removed:
|
||||
changes.append(
|
||||
ParameterChange(
|
||||
change_type=ChangeType.REMOVED,
|
||||
param_name=param.name,
|
||||
param_type=param.type,
|
||||
)
|
||||
)
|
||||
|
||||
# 4. 纯新增
|
||||
for key, param in unmatched_added:
|
||||
if key not in matched_added:
|
||||
changes.append(
|
||||
ParameterChange(
|
||||
change_type=ChangeType.ADDED,
|
||||
param_name=param.name,
|
||||
param_type=param.type,
|
||||
required=param.required,
|
||||
)
|
||||
)
|
||||
|
||||
return changes
|
||||
|
||||
|
||||
def compare_endpoints(
|
||||
old_endpoints: Dict[str, ApiEndpoint],
|
||||
new_endpoints: Dict[str, ApiEndpoint],
|
||||
) -> List[EndpointChangeReport]:
|
||||
"""
|
||||
对比新旧两个版本的全部 Controller 端点,生成变更报告列表。
|
||||
|
||||
:param old_endpoints: 旧版本 { endpoint_key: ApiEndpoint }
|
||||
:param new_endpoints: 新版本 { endpoint_key: ApiEndpoint }
|
||||
:return: 有变更的接口报告列表
|
||||
"""
|
||||
reports: List[EndpointChangeReport] = []
|
||||
|
||||
all_keys = set(old_endpoints.keys()) | set(new_endpoints.keys())
|
||||
|
||||
for key in sorted(all_keys):
|
||||
old_ep = old_endpoints.get(key)
|
||||
new_ep = new_endpoints.get(key)
|
||||
|
||||
if old_ep is None and new_ep is not None:
|
||||
# 全新接口
|
||||
reports.append(
|
||||
EndpointChangeReport(
|
||||
uri=new_ep.uri,
|
||||
http_method=new_ep.http_method,
|
||||
controller_class=new_ep.controller_class,
|
||||
method_name=new_ep.method_name,
|
||||
is_new_endpoint=True,
|
||||
parameter_changes=[
|
||||
ParameterChange(
|
||||
change_type=ChangeType.ADDED,
|
||||
param_name=p.name,
|
||||
param_type=p.type,
|
||||
required=p.required,
|
||||
)
|
||||
for p in new_ep.parameters
|
||||
],
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if new_ep is None and old_ep is not None:
|
||||
# 接口被删除
|
||||
reports.append(
|
||||
EndpointChangeReport(
|
||||
uri=old_ep.uri,
|
||||
http_method=old_ep.http_method,
|
||||
controller_class=old_ep.controller_class,
|
||||
method_name=old_ep.method_name,
|
||||
is_removed_endpoint=True,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# 同 URI 对比参数
|
||||
param_changes = compare_parameters(old_ep.parameters, new_ep.parameters)
|
||||
if param_changes:
|
||||
reports.append(
|
||||
EndpointChangeReport(
|
||||
uri=new_ep.uri,
|
||||
http_method=new_ep.http_method,
|
||||
controller_class=new_ep.controller_class,
|
||||
method_name=new_ep.method_name,
|
||||
parameter_changes=param_changes,
|
||||
)
|
||||
)
|
||||
|
||||
return [r for r in reports if r.has_changes]
|
||||
114
.gitea/checker/controller_parser.py
Normal file
114
.gitea/checker/controller_parser.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Controller 端点解析模块。
|
||||
调用 Java AST 解析器 JAR,将 Java 源码转为结构化的 API 端点列表。
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApiParameter:
|
||||
"""单个接口参数。"""
|
||||
|
||||
name: str
|
||||
type: str
|
||||
required: bool = True
|
||||
source: str = "query"
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApiEndpoint:
|
||||
"""单个 Controller 接口端点。"""
|
||||
|
||||
http_method: str
|
||||
uri: str
|
||||
controller_class: str
|
||||
method_name: str
|
||||
source_file: str
|
||||
parameters: List[ApiParameter] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def endpoint_key(self) -> str:
|
||||
"""唯一标识:HTTP 方法 + URI,用于跨版本匹配。"""
|
||||
return f"{self.http_method} {self.uri}"
|
||||
|
||||
|
||||
def run_java_parser(source_dir: Path, jar_path: Path, output_json: Path) -> List[ApiEndpoint]:
|
||||
"""
|
||||
调用 JavaParser JAR 扫描源码目录,返回解析结果。
|
||||
|
||||
:param source_dir: Java 源码根目录
|
||||
:param jar_path: controller-parser JAR 路径
|
||||
:param output_json: 临时 JSON 输出路径
|
||||
:return: ApiEndpoint 列表
|
||||
:raises RuntimeError: Java 进程失败或 JAR 不存在
|
||||
"""
|
||||
if not jar_path.exists():
|
||||
raise RuntimeError(
|
||||
f"Java 解析器 JAR 不存在: {jar_path}\n"
|
||||
"请先在 .gitea/java-parser 目录执行: mvn -q package"
|
||||
)
|
||||
|
||||
cmd = ["java", "-jar", str(jar_path), str(source_dir), str(output_json)]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8")
|
||||
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Java 解析器执行失败:\n{result.stderr}")
|
||||
|
||||
with open(output_json, "r", encoding="utf-8") as f:
|
||||
raw = json.load(f)
|
||||
|
||||
return [_dict_to_endpoint(item) for item in raw]
|
||||
|
||||
|
||||
def _dict_to_endpoint(data: dict) -> ApiEndpoint:
|
||||
"""将 JSON 字典转换为 ApiEndpoint 对象。"""
|
||||
params = [
|
||||
ApiParameter(
|
||||
name=p.get("name", ""),
|
||||
type=p.get("type", ""),
|
||||
required=p.get("required", True),
|
||||
source=p.get("source", "query"),
|
||||
description=p.get("description"),
|
||||
)
|
||||
for p in data.get("parameters", [])
|
||||
]
|
||||
return ApiEndpoint(
|
||||
http_method=data.get("httpMethod", "GET"),
|
||||
uri=data.get("uri", "/"),
|
||||
controller_class=data.get("controllerClass", ""),
|
||||
method_name=data.get("methodName", ""),
|
||||
source_file=data.get("sourceFile", ""),
|
||||
parameters=params,
|
||||
)
|
||||
|
||||
|
||||
def endpoints_to_map(endpoints: List[ApiEndpoint]) -> Dict[str, ApiEndpoint]:
|
||||
"""
|
||||
将端点列表转为字典,key 为 endpoint_key。
|
||||
|
||||
:param endpoints: 端点列表
|
||||
:return: { "GET /api/users/{id}": ApiEndpoint, ... }
|
||||
"""
|
||||
return {ep.endpoint_key: ep for ep in endpoints}
|
||||
|
||||
|
||||
def filter_endpoints_by_files(
|
||||
endpoints: List[ApiEndpoint], changed_files: List[str]
|
||||
) -> List[ApiEndpoint]:
|
||||
"""
|
||||
仅保留源文件在变更列表中的端点(缩小对比范围)。
|
||||
|
||||
:param endpoints: 全部端点
|
||||
:param changed_files: 变更文件相对路径列表
|
||||
:return: 过滤后的端点
|
||||
"""
|
||||
if not changed_files:
|
||||
return endpoints
|
||||
changed_set = set(changed_files)
|
||||
return [ep for ep in endpoints if ep.source_file in changed_set]
|
||||
140
.gitea/checker/git_utils.py
Normal file
140
.gitea/checker/git_utils.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
Git 操作工具模块。
|
||||
负责在 CI 环境中检出上一版本代码、获取变更文件列表及提交元信息。
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommitInfo:
|
||||
"""单次 Git 提交的元信息。"""
|
||||
|
||||
sha: str
|
||||
author: str
|
||||
commit_time: str
|
||||
message: str
|
||||
|
||||
|
||||
def run_git(args: List[str], cwd: Optional[Path] = None) -> str:
|
||||
"""
|
||||
执行 git 命令并返回标准输出。
|
||||
|
||||
:param args: git 子命令及参数,如 ["log", "-1", "--format=%H"]
|
||||
:param cwd: 工作目录,默认为当前目录
|
||||
:return: 命令 stdout 文本(已 strip)
|
||||
:raises RuntimeError: git 命令执行失败时抛出
|
||||
"""
|
||||
cmd = ["git"] + args
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Git 命令失败: {' '.join(cmd)}\n{result.stderr}")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def get_current_commit() -> CommitInfo:
|
||||
"""
|
||||
获取当前 HEAD 提交的元信息(推送人、时间等,用于通知模板)。
|
||||
|
||||
:return: CommitInfo 对象
|
||||
"""
|
||||
sha = run_git(["rev-parse", "HEAD"])
|
||||
author = run_git(["log", "-1", "--format=%an"])
|
||||
commit_time = run_git(["log", "-1", "--format=%ci"])
|
||||
message = run_git(["log", "-1", "--format=%s"])
|
||||
return CommitInfo(sha=sha, author=author, commit_time=commit_time, message=message)
|
||||
|
||||
|
||||
def get_previous_commit_sha() -> Optional[str]:
|
||||
"""
|
||||
获取上一次提交的 SHA(HEAD~1)。
|
||||
若是首次提交则返回 None。
|
||||
|
||||
:return: 上一 commit SHA,或 None
|
||||
"""
|
||||
try:
|
||||
return run_git(["rev-parse", "HEAD~1"])
|
||||
except RuntimeError:
|
||||
return None
|
||||
|
||||
|
||||
def checkout_commit(sha: str, worktree_dir: Path) -> None:
|
||||
"""
|
||||
将指定 commit 的代码检出到独立工作目录(不影响当前工作区)。
|
||||
|
||||
:param sha: 目标 commit SHA
|
||||
:param worktree_dir: git worktree 目录
|
||||
"""
|
||||
worktree_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
if worktree_dir.exists():
|
||||
# 已存在则先移除旧 worktree
|
||||
run_git(["worktree", "remove", "--force", str(worktree_dir)])
|
||||
run_git(["worktree", "add", str(worktree_dir), sha])
|
||||
|
||||
|
||||
def get_changed_java_controller_files(base_sha: str, head_sha: str) -> List[str]:
|
||||
"""
|
||||
获取两次提交之间变更的 Controller 相关 Java 文件路径。
|
||||
|
||||
:param base_sha: 基准 commit(旧版本)
|
||||
:param head_sha: 目标 commit(新版本)
|
||||
:return: 相对路径列表,如 ["src/main/java/.../UserController.java"]
|
||||
"""
|
||||
diff_output = run_git(["diff", "--name-only", base_sha, head_sha])
|
||||
if not diff_output:
|
||||
return []
|
||||
|
||||
changed = []
|
||||
for line in diff_output.splitlines():
|
||||
line = line.strip()
|
||||
if line.endswith(".java") and "Controller" in line:
|
||||
changed.append(line.replace("\\", "/"))
|
||||
return changed
|
||||
|
||||
|
||||
def get_controller_files_diff(base_sha: str, head_sha: str, changed_files: List[str]) -> str:
|
||||
"""
|
||||
获取变更 Controller 文件的 Git diff,供 LLM 审核接口参数变更时参考。
|
||||
|
||||
:param base_sha: 旧版本 commit SHA
|
||||
:param head_sha: 新版本 commit SHA
|
||||
:param changed_files: 变更文件相对路径列表
|
||||
:return: diff 文本
|
||||
"""
|
||||
if not changed_files:
|
||||
return ""
|
||||
|
||||
try:
|
||||
return run_git(["diff", base_sha, head_sha, "--"] + changed_files)
|
||||
except RuntimeError as exc:
|
||||
print(f"[警告] 获取 Git diff 失败: {exc}")
|
||||
return ""
|
||||
|
||||
|
||||
def prepare_worktrees(repo_root: Path) -> tuple:
|
||||
"""
|
||||
准备新旧两个版本的代码工作目录,供 AST 解析器分别扫描。
|
||||
|
||||
:param repo_root: 仓库根目录
|
||||
:return: (新版本目录, 旧版本目录, 旧版本SHA);首次提交时旧版本目录为 None
|
||||
"""
|
||||
prev_sha = get_previous_commit_sha()
|
||||
current_dir = repo_root
|
||||
|
||||
if prev_sha is None:
|
||||
return current_dir, None, None
|
||||
|
||||
prev_dir = repo_root / ".gitea" / ".cache" / "prev-worktree"
|
||||
checkout_commit(prev_sha, prev_dir)
|
||||
return current_dir, prev_dir, prev_sha
|
||||
182
.gitea/checker/llm_reviewer.py
Normal file
182
.gitea/checker/llm_reviewer.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""
|
||||
豆包 LLM 接口参数变更审核模块。
|
||||
仅审核 Controller 层接口参数的增删改,不对 Java 源码做通用代码审查。
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from comparator import EndpointChangeReport
|
||||
|
||||
|
||||
def is_llm_enabled(config: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
判断大模型总开关是否开启。
|
||||
|
||||
:param config: 完整配置字典
|
||||
:return: True=启用 LLM 审核
|
||||
"""
|
||||
return config.get("llm", {}).get("enabled", True)
|
||||
|
||||
|
||||
def call_doubao_api(
|
||||
api_key: str,
|
||||
prompt: str,
|
||||
config: Dict[str, Any],
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
调用火山引擎豆包 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":
|
||||
print("[警告] 未配置豆包 API Key,跳过 LLM 审核。")
|
||||
return None
|
||||
|
||||
llm_cfg = config.get("llm", {})
|
||||
model = llm_cfg.get("model") or llm_cfg.get("endpoint_id", "")
|
||||
if not model:
|
||||
print("[警告] 未配置 llm.model 或 llm.endpoint_id,跳过 LLM 审核。")
|
||||
return None
|
||||
|
||||
api_url = llm_cfg.get(
|
||||
"api_url", "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
|
||||
)
|
||||
timeout = llm_cfg.get("timeout")
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"你是 Java Spring Boot Controller 接口参数变更分析专家。"
|
||||
"你的职责是识别并整理 Controller 层接口参数的增、删、改、重命名,"
|
||||
"确认 AST 解析结果是否准确,并指出对调用方的兼容性影响。"
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
"temperature": 0.1,
|
||||
}
|
||||
|
||||
try:
|
||||
kwargs = {"headers": headers, "json": payload}
|
||||
if timeout is not None:
|
||||
kwargs["timeout"] = timeout
|
||||
|
||||
resp = requests.post(api_url, **kwargs)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if "choices" in data and data["choices"]:
|
||||
return data["choices"][0]["message"]["content"]
|
||||
print("[错误] AI 返回格式异常")
|
||||
return None
|
||||
except requests.RequestException as exc:
|
||||
print(f"[错误] 豆包 API 调用失败: {exc}")
|
||||
return None
|
||||
|
||||
|
||||
def build_parameter_change_prompt(
|
||||
reports: List[EndpointChangeReport],
|
||||
changed_files: List[str],
|
||||
git_diff: str = "",
|
||||
) -> str:
|
||||
"""
|
||||
构造接口参数变更审核提示词(对齐第一版需求:识别 Controller 参数增删改)。
|
||||
|
||||
:param reports: AST 解析对比结果
|
||||
:param changed_files: 本次变更的 Controller 文件列表
|
||||
:param git_diff: 相关文件的 Git diff 内容
|
||||
:return: 完整 prompt
|
||||
"""
|
||||
ast_report = []
|
||||
for r in reports:
|
||||
ast_report.append(
|
||||
{
|
||||
"uri": f"{r.http_method} {r.uri}",
|
||||
"controller": r.controller_class,
|
||||
"method": r.method_name,
|
||||
"is_new_endpoint": r.is_new_endpoint,
|
||||
"is_removed_endpoint": r.is_removed_endpoint,
|
||||
"parameter_changes": [
|
||||
{
|
||||
"type": c.change_type.value,
|
||||
"name": c.param_name,
|
||||
"java_type": c.param_type,
|
||||
"old_name": c.old_name,
|
||||
"old_type": c.old_type,
|
||||
"required": c.required,
|
||||
"detail": c.detail,
|
||||
}
|
||||
for c in r.parameter_changes
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
diff_block = git_diff.strip() if git_diff.strip() else "(无 diff 内容)"
|
||||
if len(diff_block) > 8000:
|
||||
diff_block = diff_block[:8000] + "\n... [diff 过长,已截断]"
|
||||
|
||||
return f"""请审核以下 Controller 层接口参数变更,整理并确认变更结果。
|
||||
|
||||
## 变更的 Controller 文件
|
||||
{json.dumps(changed_files, ensure_ascii=False)}
|
||||
|
||||
## AST 自动解析的参数变更报告
|
||||
{json.dumps(ast_report, ensure_ascii=False, indent=2)}
|
||||
|
||||
## Git Diff(Controller 相关)
|
||||
```diff
|
||||
{diff_block}
|
||||
```
|
||||
|
||||
## 审核要求
|
||||
1. 逐条确认 AST 报告的参数变更是否准确(增/删/改/重命名)
|
||||
2. 若 AST 有遗漏,补充遗漏的接口参数变更
|
||||
3. 若 AST 有误报,指出并修正
|
||||
4. 按以下格式整理每个接口的变更(与通知模板一致):
|
||||
URI: GET /api/xxx
|
||||
参数变更:
|
||||
- 删除: Boolean userType
|
||||
- 新增: Boolean includeDisabled (是否必填:false)
|
||||
- 重命名: String userName -> String accountName
|
||||
5. 简要说明是否存在破坏性变更(影响前端/调用方)
|
||||
6. 用中文回复,简洁清晰
|
||||
"""
|
||||
|
||||
|
||||
def review_parameter_changes(
|
||||
reports: List[EndpointChangeReport],
|
||||
config: Dict[str, Any],
|
||||
changed_files: List[str],
|
||||
git_diff: str = "",
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
使用 LLM 审核 Controller 接口参数变更(AST 结果的二次确认与整理)。
|
||||
|
||||
: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
|
||||
|
||||
llm_cfg = config.get("llm", {})
|
||||
prompt = build_parameter_change_prompt(reports, changed_files, git_diff)
|
||||
return call_doubao_api(llm_cfg.get("api_key", ""), prompt, config)
|
||||
182
.gitea/checker/main.py
Normal file
182
.gitea/checker/main.py
Normal file
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI-Check 主入口 — Controller 层接口参数变更检测
|
||||
|
||||
流程(对齐第一版需求):
|
||||
1. Git 检出新旧代码
|
||||
2. JavaParser AST 解析 Controller 方法签名与参数
|
||||
3. 对比增 / 删 / 改 / 重命名
|
||||
4. (可选)LLM 审核参数变更结果
|
||||
5. (可选)写入日志
|
||||
6. 企业微信通知(URI + 参数变更明细)
|
||||
|
||||
用法:
|
||||
python .gitea/checker/main.py [--config .gitea/config.yaml] [--repo-root .] [推送人] [推送时间]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
CHECKER_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(CHECKER_DIR))
|
||||
|
||||
from change_logger import persist_change_log
|
||||
from comparator import compare_endpoints
|
||||
from controller_parser import (
|
||||
endpoints_to_map,
|
||||
filter_endpoints_by_files,
|
||||
run_java_parser,
|
||||
)
|
||||
from git_utils import (
|
||||
get_changed_java_controller_files,
|
||||
get_controller_files_diff,
|
||||
get_current_commit,
|
||||
get_previous_commit_sha,
|
||||
prepare_worktrees,
|
||||
)
|
||||
from llm_reviewer import review_parameter_changes
|
||||
from notifier import send_parameter_change_notification
|
||||
|
||||
|
||||
def load_config(config_path: Path) -> dict:
|
||||
"""加载 YAML 配置文件。"""
|
||||
if not config_path.exists():
|
||||
print(f"[错误] 配置文件不存在: {config_path}")
|
||||
print("请在 .gitea/config.yaml 中填写配置并提交到仓库。")
|
||||
sys.exit(1)
|
||||
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
|
||||
|
||||
def parse_endpoints_for_version(
|
||||
repo_root: Path,
|
||||
source_subdir: str,
|
||||
jar_path: Path,
|
||||
tmp_dir: Path,
|
||||
label: str,
|
||||
) -> dict:
|
||||
"""对指定版本源码运行 Java AST 解析,提取 Controller 接口参数。"""
|
||||
source_dir = repo_root / source_subdir
|
||||
output_json = tmp_dir / f"endpoints_{label}.json"
|
||||
|
||||
print(f"[AST] 扫描 {label} 版本: {source_dir}")
|
||||
endpoints = run_java_parser(source_dir, jar_path, output_json)
|
||||
print(f"[AST] {label} 版本共 {len(endpoints)} 个 Controller 接口")
|
||||
return endpoints_to_map(endpoints)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""主流程入口。"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="AI-Check: Controller 接口参数变更检测"
|
||||
)
|
||||
parser.add_argument("--config", default=".gitea/config.yaml", help="配置文件路径(相对仓库根目录)")
|
||||
parser.add_argument("--repo-root", default=".", help="Git 仓库根目录")
|
||||
parser.add_argument("push_user", nargs="?", default=None, help="推送人(CI 传入)")
|
||||
parser.add_argument("push_time", nargs="?", default=None, help="推送时间(CI 传入)")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = Path(args.repo_root).resolve()
|
||||
config = load_config(repo_root / args.config)
|
||||
|
||||
source_subdir = config.get("source_dir", "src/main/java")
|
||||
jar_path = repo_root / config.get(
|
||||
"java_parser_jar", ".gitea/java-parser/target/controller-parser-1.0.0.jar"
|
||||
)
|
||||
|
||||
commit_info = get_current_commit()
|
||||
push_user = args.push_user or commit_info.author
|
||||
push_time = args.push_time or commit_info.commit_time
|
||||
|
||||
print("Controller 接口参数变更检测")
|
||||
print("=" * 40)
|
||||
print(f"推送人: {push_user}")
|
||||
print(f"推送时间: {push_time}")
|
||||
print(f"LLM 审核: {config.get('llm', {}).get('enabled', True)}")
|
||||
print(f"记录日志: {config.get('log', {}).get('enabled', False)}")
|
||||
print("=" * 40)
|
||||
|
||||
prev_sha = get_previous_commit_sha()
|
||||
if prev_sha is None:
|
||||
print("[Git] 首次提交,无可对比版本,跳过。")
|
||||
return 0
|
||||
|
||||
changed_files = get_changed_java_controller_files(prev_sha, commit_info.sha)
|
||||
if not changed_files:
|
||||
print("[Git] 本次提交未变更 Controller 文件,跳过。")
|
||||
return 0
|
||||
|
||||
print(f"[Git] 变更 Controller 文件 {len(changed_files)} 个:")
|
||||
for f in changed_files:
|
||||
print(f" - {f}")
|
||||
|
||||
git_diff = get_controller_files_diff(prev_sha, commit_info.sha, changed_files)
|
||||
current_dir, prev_dir, _ = prepare_worktrees(repo_root)
|
||||
|
||||
reports = []
|
||||
llm_review = None
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="ai-check-") as tmp:
|
||||
tmp_dir = Path(tmp)
|
||||
|
||||
# 1. AST 解析 + 参数对比
|
||||
new_map = parse_endpoints_for_version(
|
||||
current_dir, source_subdir, jar_path, tmp_dir, "new"
|
||||
)
|
||||
old_map = parse_endpoints_for_version(
|
||||
prev_dir, source_subdir, jar_path, tmp_dir, "old"
|
||||
)
|
||||
|
||||
new_filtered = endpoints_to_map(
|
||||
filter_endpoints_by_files(list(new_map.values()), changed_files)
|
||||
)
|
||||
old_filtered = endpoints_to_map(
|
||||
filter_endpoints_by_files(list(old_map.values()), changed_files)
|
||||
)
|
||||
|
||||
reports = compare_endpoints(old_filtered, new_filtered)
|
||||
print(f"[对比] 检测到 {len(reports)} 个接口存在参数变更")
|
||||
|
||||
# 2. LLM 审核接口参数变更(非代码审查)
|
||||
if reports:
|
||||
llm_review = review_parameter_changes(
|
||||
reports, config, changed_files, git_diff
|
||||
)
|
||||
if llm_review:
|
||||
print(f"[LLM] 参数变更审核完成")
|
||||
|
||||
# 3. 写日志(开关控制)
|
||||
persist_change_log(reports, commit_info, config, llm_review)
|
||||
|
||||
# 4. 企微通知
|
||||
notify_cfg = config.get("notify", {})
|
||||
only_on_change = notify_cfg.get("only_on_change", True)
|
||||
|
||||
if only_on_change and not reports:
|
||||
print("[通知] 无接口参数变更,跳过企微通知。")
|
||||
return 0
|
||||
|
||||
mentioned = notify_cfg.get("mentioned_users", "")
|
||||
mentioned_list = [u.strip() for u in mentioned.split(",") if u.strip()] or None
|
||||
|
||||
wecom_cfg = config.get("wecom", {})
|
||||
send_parameter_change_notification(
|
||||
webhook_url=wecom_cfg.get("webhook_url", ""),
|
||||
reports=reports,
|
||||
push_user=push_user,
|
||||
push_time=push_time,
|
||||
llm_review=llm_review,
|
||||
mentioned_users=mentioned_list,
|
||||
)
|
||||
|
||||
print("\n完成")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
218
.gitea/checker/notifier.py
Normal file
218
.gitea/checker/notifier.py
Normal file
@@ -0,0 +1,218 @@
|
||||
"""
|
||||
企业微信机器人通知模块。
|
||||
按第一版模板发送 Controller 接口参数变更通知,支持超长内容分段发送。
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from comparator import EndpointChangeReport
|
||||
from git_utils import CommitInfo
|
||||
|
||||
# 企微 text 消息字节上限约 2048,留余量按字符分段
|
||||
MAX_TEXT_LENGTH = 2000
|
||||
|
||||
|
||||
def truncate_text(text: str, max_length: int = MAX_TEXT_LENGTH) -> str:
|
||||
"""
|
||||
截断文本,避免超出企微单条消息限制。
|
||||
|
||||
:param text: 原始文本
|
||||
:param max_length: 最大字符数
|
||||
:return: 截断后文本
|
||||
"""
|
||||
if len(text) <= max_length:
|
||||
return text
|
||||
return text[:max_length] + "\n... [消息过长,已截断]"
|
||||
|
||||
|
||||
def build_single_endpoint_message(
|
||||
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 = [
|
||||
"[API变更通知]",
|
||||
f"URI: {report.http_method} {report.uri}",
|
||||
f"修改人:{push_user}",
|
||||
f"修改时间:{push_time}",
|
||||
]
|
||||
|
||||
if report.is_new_endpoint:
|
||||
lines.append("接口状态:新增接口")
|
||||
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())
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_all_notifications(
|
||||
reports: List[EndpointChangeReport],
|
||||
push_user: str,
|
||||
push_time: str,
|
||||
llm_review: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
"""
|
||||
将所有接口变更组装为通知消息列表,超长时自动分段。
|
||||
|
||||
:param reports: 变更报告列表
|
||||
:param push_user: 推送人
|
||||
:param push_time: 推送时间
|
||||
:param llm_review: LLM 参数变更审核结论(可选,附在最后一条或单独一条)
|
||||
:return: 待发送的消息段落列表
|
||||
"""
|
||||
if not reports:
|
||||
return []
|
||||
|
||||
messages: List[str] = []
|
||||
current = ""
|
||||
|
||||
for report in reports:
|
||||
block = build_single_endpoint_message(report, push_user, push_time)
|
||||
separator = "\n\n---\n\n"
|
||||
|
||||
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 current:
|
||||
messages.append(current)
|
||||
|
||||
# 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))
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
def _split_long_text(text: str, max_len: int) -> List[str]:
|
||||
"""按行拆分超长文本。"""
|
||||
lines = text.split("\n")
|
||||
chunks: List[str] = []
|
||||
current = ""
|
||||
for line in lines:
|
||||
candidate = current + line + "\n"
|
||||
if len(candidate) > max_len and current:
|
||||
chunks.append(current.rstrip())
|
||||
current = line + "\n"
|
||||
else:
|
||||
current = candidate
|
||||
if current.strip():
|
||||
chunks.append(current.rstrip())
|
||||
return chunks
|
||||
|
||||
|
||||
def _post_wecom_text(webhook_url: str, content: str) -> bool:
|
||||
"""
|
||||
发送单条 text 消息到企业微信。
|
||||
|
||||
:param webhook_url: Webhook URL
|
||||
:param content: 消息正文
|
||||
:return: 是否成功
|
||||
"""
|
||||
if not webhook_url or "YOUR_WECOM_KEY" in webhook_url:
|
||||
print("[警告] 未配置有效的企业微信 Webhook URL。")
|
||||
print("--- 通知预览 ---")
|
||||
print(content[:800])
|
||||
return False
|
||||
|
||||
payload = {
|
||||
"msgtype": "text",
|
||||
"text": {"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:
|
||||
"""
|
||||
发送 Controller 接口参数变更通知(支持分段)。
|
||||
|
||||
:param webhook_url: 企微 Webhook
|
||||
:param reports: AST 参数变更报告
|
||||
:param push_user: 推送人
|
||||
:param push_time: 推送时间
|
||||
:param llm_review: LLM 参数变更审核(可选)
|
||||
:param mentioned_users: @ 成员 userid 列表
|
||||
: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
|
||||
|
||||
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):
|
||||
sent += 1
|
||||
print(f"第 {sent} 条通知已发送到企业微信")
|
||||
|
||||
if sent > 0:
|
||||
print(f"总共发送 {sent} 条通知到企业微信")
|
||||
return sent
|
||||
3
.gitea/checker/requirements.txt
Normal file
3
.gitea/checker/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# Python 依赖(CI 主流程)
|
||||
PyYAML>=6.0.1
|
||||
requests>=2.31.0
|
||||
Reference in New Issue
Block a user