diff --git a/.gitea/.gitignore b/.gitea/.gitignore deleted file mode 100644 index c11b646..0000000 --- a/.gitea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# 本地/CI 运行时产生的缓存与日志(可不提交) -.cache/ -logs/ diff --git a/.gitea/1.md b/.gitea/1.md new file mode 100644 index 0000000..d4047c6 --- /dev/null +++ b/.gitea/1.md @@ -0,0 +1,102 @@ +--- + +#### 需求拆解: +**[类变更类通知]** Vo、Dto、Model、Entity **目前只针对****修改 ****删除也需要** + +**需要展示的内容:** + +修改人、修改时间 (方便后续前端对接) + +对象变更细节:变更了(增删改查)哪些字段 + 字段说明 + +影响范围:类的变更影响了哪些接口的使用(展示出影响的接口List) + + * **入参影响** --> dto改变 --> 展示出影响的接口List + * **类转换影响** --> dto到entity的转换(这种类型需要 配置开关 判断是否需要检测) --> 展示出Entity类? + * **对前端的影响** --> Vo的变动 --> 展示出影响的接口List + + + +#### 一、请求链路 +Push + + ↓ + +Gitea Runner + + ↓ + +Pipeline + + ↓ + +GitDiff获取变更 + + ↓ + +JavaParser解析(AST Controller变化\DTO变化\VO变化\ENTITY变化) + + ↓ + +企业微信通知 + + + +#### 二、架构&技术选型 +| 组件 | 方案 | +| --- | --- | +| Git Diff | Git | +| 源码解析 | JavaParser | +| 引用分析 | JavaParser Symbol Solver | +| Spring Endpoint扫描 | Spring Mapping AST | +| 通知 | 企业微信机器人 webhook | +| CI集成 | Jenkins/GitLab CI | + + + + +#### 三、分层说明 +| **层级** | **组件** | **职责** | +| :--- | :--- | :--- | +| **触发层** | Git Push | 开发者提交代码,触发 CI 流程 | +| **CI/CD 层** | Gitea Runner + Pipeline | 监听 Push 事件,编排流水线任务 | +| **解析层** | GitDiff + JavaParser | 获取 diff,按 AST 解析 Controller/DTO/VO/Entity 变更 | +| **通知层** | 企业微信 | 将分析结果推送给相关开发人员 | + + + + +#### 四、通知模版 +【类变更通知】 + +■ 变更对象:{ClassName}({类类型:Dto/Vo/Entity/Model}) +■ 修改人:{Modifier} +■ 修改时间:{ModifyTime} + +──────────────────────────────── +▶ 对象变更细节 +──────────────────────────────── +字段变更列表: + [新增] 字段名: fieldA 说明: {字段说明} + [删除] 字段名: fieldB 说明: {字段说明} + [修改] 字段名: fieldC 说明: {原说明 → 新说明 / 类型变更等} + +──────────────────────────────── +▶ 影响范围 +──────────────────────────────── +① 入参影响(Dto变更导致接口参数变化): + 影响接口列表: + - GET /api/attendance/dayStaList + - POST /api/attendance/export + (若无影响则显示:无) + +② 类转换影响(Dto → Entity 转换,已开启检测): + 涉及Entity类:{EntityClassName} + (若开关关闭或未检测到影响则显示:无 / 未开启检测) + +③ 前端影响(Vo变更导致返回结构变化): + 影响接口列表: + - GET /api/attendance/detail + - GET /api/attendance/summary + (若无影响则显示:无) + diff --git a/.gitea/checker/change_logger.py b/.gitea/checker/change_logger.py deleted file mode 100644 index aecffee..0000000 --- a/.gitea/checker/change_logger.py +++ /dev/null @@ -1,165 +0,0 @@ -""" -变更日志持久化模块。 -受 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, - ) diff --git a/.gitea/checker/comparator.py b/.gitea/checker/comparator.py deleted file mode 100644 index af0065e..0000000 --- a/.gitea/checker/comparator.py +++ /dev/null @@ -1,449 +0,0 @@ -""" -API 参数变更对比模块。 -对比新旧两个版本的 Controller 端点,识别参数的增、删、改、重命名。 -""" - -from dataclasses import dataclass, field -from enum import Enum -from typing import Dict, List, Optional, Set, Tuple - -from models 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 - description: Optional[str] = None - old_description: Optional[str] = None - source: str = "query" - parent_dto: Optional[str] = None - body_param_name: Optional[str] = None - - def _change_tag(self) -> str: - """变更类型标签(企微颜色)。""" - tags = { - ChangeType.ADDED: '**新增**', - ChangeType.REMOVED: '**删除**', - ChangeType.RENAMED: '**重命名**', - ChangeType.MODIFIED: '**修改**', - } - return tags.get(self.change_type, "") - - def _required_tag(self) -> str: - """必填/可选标签。""" - if self.required is True: - return '必填' - if self.required is False: - return '可选' - return "" - - def to_markdown_block(self, index: int = 1) -> str: - """格式化为企微友好的参数变更卡片(列表式,非表格)。""" - lines: List[str] = [] - desc = self.description or self.old_description - - if self.change_type == ChangeType.RENAMED: - lines.append(f"**{index}. `{self.param_name}`** {self._change_tag()}") - lines.append(f"> `{self.old_name}` → `{self.param_name}`") - if desc: - lines.append(f"> 说明:{desc}") - return "\n".join(lines) - - if self.change_type == ChangeType.ADDED: - type_part = f" · `{self.param_type}`" if self.param_type else "" - req_part = f" · {self._required_tag()}" if self._required_tag() else "" - lines.append( - f"**{index}. `{self.param_name}`**{type_part}{req_part} {self._change_tag()}" - ) - if desc: - lines.append(f"> 说明:{desc}") - return "\n".join(lines) - - if self.change_type == ChangeType.REMOVED: - type_part = f" · `{self.param_type}`" if self.param_type else "" - lines.append( - f"**{index}. `{self.param_name}`**{type_part} {self._change_tag()}" - ) - if desc: - lines.append(f"> 说明:{desc}") - return "\n".join(lines) - - # MODIFIED - lines.append(f"**{index}. `{self.param_name}`** {self._change_tag()}") - if desc: - lines.append(f"> 说明:{desc}") - if self.detail: - lines.append(f"> 变更:{self.detail}") - return "\n".join(lines) - - def to_table_row(self) -> str: - """兼容旧调用,委托至卡片块。""" - return self.to_markdown_block(1) - - -@dataclass -class EndpointChangeReport: - """单个接口的变更报告。""" - - uri: str - http_method: str - controller_class: str - method_name: str - source_file: str = "" - parameter_changes: List[ParameterChange] = field(default_factory=list) - is_new_endpoint: bool = False - is_removed_endpoint: bool = False - is_renamed_endpoint: bool = False - old_uri: Optional[str] = None - is_method_changed: bool = False - old_http_method: Optional[str] = None - - @property - def has_changes(self) -> bool: - """是否存在任何变更。""" - return ( - self.is_new_endpoint - or self.is_removed_endpoint - or self.is_renamed_endpoint - or self.is_method_changed - 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 _format_type_change(old_type: str, new_type: str) -> str: - """类型变更文案。""" - return f"类型由{old_type}改为{new_type}" - - -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(_format_type_change(old_p.type, new_p.type)) - if old_p.required != new_p.required: - req_label = lambda r: "必填" if r else "可选" - detail_parts.append( - f"必填性由{req_label(old_p.required)}改为{req_label(new_p.required)}" - ) - if old_p.description != new_p.description: - detail_parts.append( - f"说明由{old_p.description or '-'}改为{new_p.description or '-'}" - ) - 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), - description=new_p.description, - old_description=old_p.description, - source=new_p.source, - parent_dto=new_p.parent_dto, - body_param_name=new_p.body_param_name, - ) - ) - - # 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, - description=a_param.description, - old_description=r_param.description, - source=a_param.source, - parent_dto=a_param.parent_dto, - body_param_name=a_param.body_param_name, - ) - ) - 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, - description=param.description, - source=param.source, - parent_dto=param.parent_dto, - body_param_name=param.body_param_name, - ) - ) - - # 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, - description=param.description, - source=param.source, - parent_dto=param.parent_dto, - body_param_name=param.body_param_name, - ) - ) - - return changes - - -def compare_endpoints( - old_endpoints: Dict[str, ApiEndpoint], - new_endpoints: Dict[str, ApiEndpoint], -) -> List[EndpointChangeReport]: - """ - 对比新旧两个版本的全部 Controller 端点,生成变更报告列表。 - - 支持以下变更类型检测: - - HTTP 方法变更(GET → POST 等) - - URI 路径变更(路径重命名) - - 新增 / 删除接口 - - 参数变更 - """ - reports: List[EndpointChangeReport] = [] - - old_keys = set(old_endpoints.keys()) - new_keys = set(new_endpoints.keys()) - - removed_keys = old_keys - new_keys - added_keys = new_keys - old_keys - common_keys = old_keys & new_keys - - # 收集未匹配的 removed / added - unmatched_removed: List[Tuple[str, ApiEndpoint]] = [] - unmatched_added: List[Tuple[str, ApiEndpoint]] = [] - - for key in removed_keys: - unmatched_removed.append((key, old_endpoints[key])) - for key in added_keys: - unmatched_added.append((key, new_endpoints[key])) - - matched_removed: Set[str] = set() - matched_added: Set[str] = set() - - # 1. HTTP 方法变更检测(uri + controller 相同,但 method 不同) - # 放宽匹配条件:只要同一个 Controller 的同一个 URI 请求方式改变,就识别为「修改请求方式」 - # 不再要求 method_name 相同(允许方法重命名场景) - # 如果同时有参数变更,生成两条独立报告(方法变更 + 参数变更),互不干扰 - for r_key, r_ep in unmatched_removed: - for a_key, a_ep in unmatched_added: - if a_key in matched_added: - continue - if ( - r_ep.uri == a_ep.uri - and r_ep.controller_class == a_ep.controller_class - and r_ep.http_method != a_ep.http_method - ): - # 先生成纯方法变更报告 - reports.append( - EndpointChangeReport( - uri=a_ep.uri, - http_method=a_ep.http_method, - controller_class=a_ep.controller_class, - method_name=a_ep.method_name, - source_file=a_ep.source_file, - is_method_changed=True, - old_http_method=r_ep.http_method, - ) - ) - # 再检测参数变更,如果有则额外生成参数报告 - param_changes = compare_parameters(r_ep.parameters, a_ep.parameters) - if param_changes: - reports.append( - EndpointChangeReport( - uri=a_ep.uri, - http_method=a_ep.http_method, - controller_class=a_ep.controller_class, - method_name=a_ep.method_name, - source_file=a_ep.source_file, - parameter_changes=param_changes, - ) - ) - matched_removed.add(r_key) - matched_added.add(a_key) - break - - # 2. URI 路径变更检测(method + controller + method_name 相同,但 uri 不同) - # 如果同时有参数变更,生成两条独立报告(路径变更 + 参数变更),互不干扰 - for r_key, r_ep in unmatched_removed: - if r_key in matched_removed: - continue - for a_key, a_ep in unmatched_added: - if a_key in matched_added: - continue - if ( - r_ep.http_method == a_ep.http_method - and r_ep.controller_class == a_ep.controller_class - and r_ep.method_name == a_ep.method_name - and r_ep.uri != a_ep.uri - ): - # 先生成纯路径变更报告 - reports.append( - EndpointChangeReport( - uri=a_ep.uri, - http_method=a_ep.http_method, - controller_class=a_ep.controller_class, - method_name=a_ep.method_name, - source_file=a_ep.source_file, - is_renamed_endpoint=True, - old_uri=r_ep.uri, - ) - ) - # 再检测参数变更,如果有则额外生成参数报告 - param_changes = compare_parameters(r_ep.parameters, a_ep.parameters) - if param_changes: - reports.append( - EndpointChangeReport( - uri=a_ep.uri, - http_method=a_ep.http_method, - controller_class=a_ep.controller_class, - method_name=a_ep.method_name, - source_file=a_ep.source_file, - parameter_changes=param_changes, - ) - ) - matched_removed.add(r_key) - matched_added.add(a_key) - break - - # 3. 剩余未匹配的 removed → 删除接口 - for key, ep in unmatched_removed: - if key not in matched_removed: - reports.append( - EndpointChangeReport( - uri=ep.uri, - http_method=ep.http_method, - controller_class=ep.controller_class, - method_name=ep.method_name, - source_file=ep.source_file, - is_removed_endpoint=True, - ) - ) - - # 4. 剩余未匹配的 added → 新增接口 - for key, ep in unmatched_added: - if key not in matched_added: - reports.append( - EndpointChangeReport( - uri=ep.uri, - http_method=ep.http_method, - controller_class=ep.controller_class, - method_name=ep.method_name, - source_file=ep.source_file, - is_new_endpoint=True, - parameter_changes=[ - ParameterChange( - change_type=ChangeType.ADDED, - param_name=p.name, - param_type=p.type, - required=p.required, - description=p.description, - source=p.source, - parent_dto=p.parent_dto, - body_param_name=p.body_param_name, - ) - for p in ep.parameters - ], - ) - ) - - # 5. 共同 URI:对比参数变更 - for key in common_keys: - old_ep = old_endpoints[key] - new_ep = new_endpoints[key] - 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, - source_file=new_ep.source_file, - parameter_changes=param_changes, - ) - ) - - return [r for r in reports if r.has_changes] diff --git a/.gitea/checker/controller_ast_parser.py b/.gitea/checker/controller_ast_parser.py deleted file mode 100644 index 589a4fe..0000000 --- a/.gitea/checker/controller_ast_parser.py +++ /dev/null @@ -1,558 +0,0 @@ -""" -纯 Python Controller AST 解析器(基于 javalang,无需 Java/Maven)。 -仅解析 Spring @RestController / @Controller 的接口映射与参数。 -""" - -from __future__ import annotations - -import re -from pathlib import Path -from typing import Dict, List, Optional - -import javalang -from javalang.tree import ( - Annotation, - ClassDeclaration, - ElementValuePair, - FieldDeclaration, - FormalParameter, - Literal, - MemberReference, - MethodDeclaration, -) - -from models import ApiEndpoint, ApiParameter - -# javax.validation 必填注解 -REQUIRED_FIELD_ANNS = {"NotNull", "NotEmpty", "NotBlank"} -MAPPING_ANNS = {"GetMapping", "PostMapping", "PutMapping", "DeleteMapping", "PatchMapping", "RequestMapping"} -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: - """获取注解简单类名。""" - return ann.name.split(".")[-1] - - -def _literal_str(node) -> str: - """从 AST 节点提取字符串或布尔值。""" - if node is None: - return "" - if isinstance(node, Literal): - v = node.value - if isinstance(v, bool): - return str(v).lower() - return str(v or "").strip('"').strip("'") - if isinstance(node, MemberReference): - return node.member - if isinstance(node, bool): - return str(node).lower() - return str(node).strip('"').strip("'") - - -def _collect_ann_members(ann: Annotation) -> Dict[str, str]: - """ - 解析注解成员为字典。 - 支持 @GetMapping("/path") 与 @RequestParam(value="x", required=false)。 - """ - members: Dict[str, str] = {} - el = ann.element - if el is None: - return members - if isinstance(el, ElementValuePair): - members[el.name] = _literal_str(el.value) - elif isinstance(el, list): - for item in el: - if isinstance(item, ElementValuePair): - members[item.name] = _literal_str(item.value) - else: - members["value"] = _literal_str(el) - return members - - -def _ann_string(ann: Annotation, *keys: str) -> str: - """从注解提取字符串属性。""" - members = _collect_ann_members(ann) - for k in keys: - if k in members and members[k]: - return members[k] - if "value" in members: - return members["value"] - return "" - - -def _type_to_str(type_node) -> str: - """Java 类型节点转字符串。""" - if type_node is None: - return "Object" - if isinstance(type_node, javalang.tree.BasicType): - return type_node.name - if isinstance(type_node, javalang.tree.ReferenceType): - name = type_node.name or "Object" - if type_node.arguments: - args = ",".join(_type_to_str(a.type) for a in type_node.arguments) - return f"{name}<{args}>" - if type_node.sub_type: - return _type_to_str(type_node.sub_type) - return name - return str(type_node) - - -def _normalize_path(path: str) -> str: - """规范化 URI 路径。""" - if not path or not path.strip(): - return "" - path = path.strip() - if not path.startswith("/"): - path = "/" + path - return re.sub(r"/+", "/", path) - - -def _join_paths(base: str, sub: str) -> str: - """拼接类级与方法级路径。""" - b, m = _normalize_path(base), _normalize_path(sub) - if not b: - return m or "/" - if not m: - return b - if b.endswith("/") and m.startswith("/"): - return b + m[1:] - if not b.endswith("/") and not m.startswith("/"): - return b + "/" + m - return b + m - - -def _http_method(ann_name: str, ann: Annotation) -> str: - """从映射注解推断 HTTP 方法。 - - 支持大小写不敏感匹配,避免 PUTMapping、POSTMapping 等不规范写法导致解析失败。 - """ - mapping = { - "GetMapping": "GET", - "PostMapping": "POST", - "PutMapping": "PUT", - "DeleteMapping": "DELETE", - "PatchMapping": "PATCH", - } - # 大小写不敏感匹配 - for key, value in mapping.items(): - if key.lower() == ann_name.lower(): - return value - if ann_name.lower() == "requestmapping": - m = _ann_string(ann, "method") - if m: - return m.replace("RequestMethod.", "").upper() - return "GET" - return "GET" - - -def _has_ann(node, name: str) -> bool: - """节点是否含指定注解。""" - anns = getattr(node, "annotations", None) or [] - return any(_ann_simple_name(a) == name for a in anns) - - -def _find_ann(node, name: str) -> Optional[Annotation]: - """查找指定注解。""" - for a in getattr(node, "annotations", None) or []: - if _ann_simple_name(a) == name: - return a - return None - - -def _param_source(param: FormalParameter) -> str: - """参数来源:path / query / header / form / body。""" - if _has_ann(param, "PathVariable"): - return "path" - if _has_ann(param, "RequestHeader"): - return "header" - if _has_ann(param, "RequestPart") or _has_ann(param, "ModelAttribute"): - return "form" - if _has_ann(param, "RequestBody"): - return "body" - return "query" - - -def _param_name(param: FormalParameter) -> str: - """解析参数名。""" - for ann_name in ("RequestParam", "PathVariable", "RequestHeader", "RequestPart"): - ann = _find_ann(param, ann_name) - if ann: - val = _ann_string(ann, "value", "name") - if val: - return val - return param.name - - -def _param_required(param: FormalParameter) -> bool: - """是否必填。""" - ann = _find_ann(param, "RequestParam") - if ann: - members = _collect_ann_members(ann) - if members.get("required", "").lower() == "false": - return False - type_name = _type_to_str(param.type) - if type_name.startswith("Optional"): - return False - return not _has_ann(param, "Nullable") - - -JAVADOC_PARAM_RE = re.compile( - r"@param\s+(\w+)\s+(.*?)(?=\n\s*\*\s*@|\n\s*\*/|\Z)", - re.DOTALL, -) - - -def _clean_javadoc_text(text: str) -> str: - """清理 JavaDoc 行前缀与多余空白。""" - cleaned = re.sub(r"\s*\*\s?", " ", text) - return re.sub(r"\s+", " ", cleaned).strip() - - -def _parse_javadoc_params(javadoc: str) -> Dict[str, str]: - """从 JavaDoc 块解析 @param 名称 -> 说明。""" - if not javadoc: - return {} - result: Dict[str, str] = {} - for match in JAVADOC_PARAM_RE.finditer(javadoc): - name = match.group(1) - desc = _clean_javadoc_text(match.group(2)) - if desc: - result[name] = desc - return result - - -def _extract_javadoc_before_line(source: str, target_line: int) -> str: - """ - 提取目标行之前紧邻的 JavaDoc 块。 - target_line 为 1-indexed(与方法声明行号一致)。 - """ - if not source or target_line <= 1: - return "" - lines = source.splitlines() - idx = target_line - 2 - while idx >= 0 and not lines[idx].strip(): - idx -= 1 - while idx >= 0 and lines[idx].strip().startswith("@"): - idx -= 1 - if idx < 0 or not lines[idx].strip().endswith("*/"): - return "" - end_idx = idx - while idx >= 0 and not lines[idx].strip().startswith("/**"): - idx -= 1 - if idx < 0: - return "" - return "\n".join(lines[idx : end_idx + 1]) - - -def _field_required(field: FieldDeclaration) -> bool: - """DTO 字段是否必填(@NotNull / @NotEmpty / @NotBlank)。""" - if _has_ann(field, "Nullable"): - return False - for ann in field.annotations or []: - if _ann_simple_name(ann) in REQUIRED_FIELD_ANNS: - return True - return False - - -def _lookup_param_description( - javadoc_params: Dict[str, str], param: FormalParameter, resolved_name: str -) -> Optional[str]: - """按注解名或形参名匹配 JavaDoc @param 说明。""" - for key in (resolved_name, param.name): - if key and key in javadoc_params: - return javadoc_params[key] - return None - - -class ControllerAstParser: - """ - 基于 javalang 的 Controller 解析器。 - 只解析传入的文件,不扫描整个目录(CI 更快)。 - """ - - def __init__(self, repo_root: Path, source_dirs: List[Path]): - """ - :param repo_root: 仓库根目录 - :param source_dirs: Java 源码根目录列表(用于查找 DTO 等) - """ - self.repo_root = repo_root - self.source_dirs = source_dirs - self._dto_cache: Dict[str, List[ApiParameter]] = {} - self._current_source = "" - - def parse_file_content(self, source: str, repo_relative_path: str) -> List[ApiEndpoint]: - """ - 解析单个 Java 源文件内容。 - - :param source: 源码文本 - :param repo_relative_path: 相对仓库根目录的路径(与 git diff 一致) - :return: 端点列表 - """ - endpoints: List[ApiEndpoint] = [] - self._current_source = source - try: - tree = javalang.parse.parse(source) - except (javalang.parser.JavaSyntaxError, RecursionError) as exc: - print(f"[警告] 解析失败 {repo_relative_path}: {exc}") - return endpoints - - for type_decl in tree.types or []: - if not isinstance(type_decl, ClassDeclaration): - continue - if not self._is_controller(type_decl): - continue - - class_path = "" - for ann in type_decl.annotations or []: - if _ann_simple_name(ann) == "RequestMapping": - class_path = _ann_string(ann, "value", "path") - break - - for method in type_decl.methods or []: - ep = self._parse_method(method, type_decl.name, class_path, repo_relative_path) - if ep: - endpoints.append(ep) - - return endpoints - - def _is_controller(self, cls: ClassDeclaration) -> bool: - """是否为 Controller 类。""" - return any(_ann_simple_name(a) in CONTROLLER_ANNS for a in (cls.annotations or [])) - - def _parse_method( - self, - method: MethodDeclaration, - class_name: str, - class_path: str, - source_file: str, - ) -> Optional[ApiEndpoint]: - """解析带映射注解的方法。""" - for ann in method.annotations or []: - ann_name = _ann_simple_name(ann) - if ann_name not in MAPPING_ANNS: - continue - - method_path = _ann_string(ann, "value", "path") - javadoc_params: Dict[str, str] = {} - if getattr(method, "position", None) and method.position: - javadoc = _extract_javadoc_before_line( - self._current_source, method.position.line - ) - javadoc_params = _parse_javadoc_params(javadoc) - - params = [] - for p in method.parameters or []: - params.extend(self._extract_param(p, javadoc_params)) - - return ApiEndpoint( - http_method=_http_method(ann_name, ann), - uri=_join_paths(class_path, method_path), - controller_class=class_name, - method_name=method.name, - source_file=source_file.replace("\\", "/"), - parameters=params, - ) - return None - - def _extract_param( - self, param: FormalParameter, javadoc_params: Optional[Dict[str, str]] = None - ) -> List[ApiParameter]: - """提取方法参数,@RequestBody 展开 DTO 字段;忽略框架注入参数。""" - type_name = _type_to_str(param.type) - name = _param_name(param) - javadoc_params = javadoc_params or {} - - if _is_framework_param(type_name, name): - return [] - - if _has_ann(param, "RequestBody"): - body_desc = _lookup_param_description(javadoc_params, param, name) - return self._expand_dto( - type_name, - "body", - body_param_name=param.name, - body_param_desc=body_desc, - ) - - description = _lookup_param_description(javadoc_params, param, name) - return [ - ApiParameter( - name=name, - type=type_name, - required=_param_required(param), - source=_param_source(param), - description=description, - ) - ] - - def _expand_dto( - self, - type_name: str, - source: str, - body_param_name: str = "", - body_param_desc: Optional[str] = None, - ) -> List[ApiParameter]: - """展开 @RequestBody DTO 一级字段。""" - simple = type_name.split(".")[-1].replace(">", "").replace("<", "").strip() - cache_key = f"{simple}:{body_param_name}" - if cache_key in self._dto_cache: - return self._dto_cache[cache_key] - - dto_file = self._find_dto_file(simple) - if not dto_file: - result = [ - ApiParameter( - name=simple, - type=type_name, - required=True, - source=source, - description=body_param_desc, - parent_dto=simple, - body_param_name=body_param_name or None, - ) - ] - self._dto_cache[cache_key] = result - return result - - try: - dto_source = dto_file.read_text(encoding="utf-8", errors="ignore") - tree = javalang.parse.parse(dto_source) - except (javalang.parser.JavaSyntaxError, OSError): - result = [ - ApiParameter( - name=simple, - type=type_name, - required=True, - source=source, - description=body_param_desc, - parent_dto=simple, - body_param_name=body_param_name or None, - ) - ] - self._dto_cache[cache_key] = result - return result - - fields: List[ApiParameter] = [] - for type_decl in tree.types or []: - if not isinstance(type_decl, ClassDeclaration): - continue - for field in type_decl.fields or []: - if "static" in (field.modifiers or []): - continue - field_javadoc = "" - if getattr(field, "position", None) and field.position: - field_javadoc = _extract_javadoc_before_line( - dto_source, field.position.line - ) - field_desc = _clean_javadoc_text( - field_javadoc.replace("/**", "").replace("*/", "").strip() - ) or None - for decl in field.declarators: - fields.append( - ApiParameter( - name=decl.name, - type=_type_to_str(field.type), - required=_field_required(field), - source=source, - description=field_desc, - parent_dto=simple, - body_param_name=body_param_name or None, - ) - ) - - if not fields: - fields = [ - ApiParameter( - name=simple, - type=type_name, - required=True, - source=source, - description=body_param_desc, - parent_dto=simple, - body_param_name=body_param_name or None, - ) - ] - - self._dto_cache[cache_key] = fields - return fields - - def _find_dto_file(self, simple_name: str) -> Optional[Path]: - """在配置的源码目录及仓库内 src/main/java 中查找 DTO 文件。""" - target = f"{simple_name}.java" - for source_dir in self.source_dirs: - if source_dir.exists(): - for path in source_dir.rglob(target): - return path - if self.repo_root.exists(): - for path in self.repo_root.rglob(target): - if "src/main/java" in path.as_posix(): - return path - return None - - -def parse_controller_files( - repo_root: Path, - source_subdirs: List[str], - file_paths: List[str], - file_contents: Dict[str, str], -) -> List[ApiEndpoint]: - """ - 批量解析指定 Controller 文件(仅解析传入的文件,不全量扫描)。 - - :param repo_root: 仓库根目录 - :param source_subdirs: 源码子目录列表(相对仓库根) - :param file_paths: 要解析的文件路径列表(相对仓库根) - :param file_contents: {文件路径: 源码内容} - :return: 所有端点 - """ - source_dirs = [(repo_root / sub).resolve() for sub in source_subdirs] - parser = ControllerAstParser(repo_root, source_dirs) - endpoints: List[ApiEndpoint] = [] - - for path in file_paths: - norm = path.replace("\\", "/") - content = file_contents.get(norm) - if not content: - continue - endpoints.extend(parser.parse_file_content(content, norm)) - - return endpoints diff --git a/.gitea/checker/controller_parser.py b/.gitea/checker/controller_parser.py deleted file mode 100644 index 99cf810..0000000 --- a/.gitea/checker/controller_parser.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Controller 端点解析模块(纯 Python,无需 Java)。 -""" - -from pathlib import Path -from typing import Dict, List - -from models import ApiEndpoint, ApiParameter - - -def endpoints_to_map(endpoints: List[ApiEndpoint]) -> Dict[str, ApiEndpoint]: - """端点列表转字典,key 为 endpoint_key。""" - return {ep.endpoint_key: ep for ep in endpoints} - - -def filter_endpoints_by_files( - endpoints: List[ApiEndpoint], changed_files: List[str] -) -> List[ApiEndpoint]: - """仅保留变更文件中的端点。""" - if not changed_files: - return endpoints - changed_set = {f.replace("\\", "/") for f in changed_files} - return [ep for ep in endpoints if ep.source_file in changed_set] - - -def parse_endpoints_from_files( - repo_root: Path, - source_subdirs: List[str], - file_paths: List[str], - file_contents: Dict[str, str], -) -> List[ApiEndpoint]: - """ - 解析指定 Controller 文件,提取接口参数(仅解析传入文件,不全量扫描)。 - - :param repo_root: 仓库根 - :param source_subdirs: 源码目录列表(相对仓库根) - :param file_paths: 文件路径列表 - :param file_contents: 路径 -> 源码内容 - :return: ApiEndpoint 列表 - """ - from controller_ast_parser import parse_controller_files - - return parse_controller_files(repo_root, source_subdirs, file_paths, file_contents) diff --git a/.gitea/checker/git_utils.py b/.gitea/checker/git_utils.py deleted file mode 100644 index 1d5bd30..0000000 --- a/.gitea/checker/git_utils.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -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 get_file_content_at_commit(commit_sha: str, file_path: str) -> Optional[str]: - """ - 读取指定 commit 下某个文件的内容(无需 git worktree,更快)。 - - :param commit_sha: commit SHA - :param file_path: 相对仓库根目录的文件路径 - :return: 文件内容;该 commit 中不存在则返回 None - """ - try: - return run_git(["show", f"{commit_sha}:{file_path}"]) - except RuntimeError: - return None - - -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 diff --git a/.gitea/checker/llm_reviewer.py b/.gitea/checker/llm_reviewer.py deleted file mode 100644 index d9db5db..0000000 --- a/.gitea/checker/llm_reviewer.py +++ /dev/null @@ -1,147 +0,0 @@ -""" -豆包 LLM 接口参数变更审核模块。 -LLM 仅输出简短的兼容性提示,详细变更由 AST + Markdown 通知展示。 -""" - -import json -from typing import Any, Dict, List, Optional - -import requests - -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: - """判断大模型总开关是否开启。""" - return config.get("llm", {}).get("enabled", True) - - -def call_doubao_api( - api_key: str, - prompt: str, - config: Dict[str, Any], -) -> Optional[str]: - """调用豆包 API。""" - 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 审核。") - 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 API 变更分析专家。" - "你只负责输出简短的兼容性风险提示,不重复罗列接口参数明细。" - + FRAMEWORK_IGNORE_HINT - ), - }, - {"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"] - 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: - """ - 构造 LLM 提示词:只要求输出兼容性摘要,不要求重复参数列表。 - """ - ast_report = [] - for r in reports: - ast_report.append( - { - "uri": f"{r.http_method} {r.uri}", - "is_new": r.is_new_endpoint, - "is_removed": r.is_removed_endpoint, - "changes": [ - { - "type": c.change_type.value, - "name": c.param_name, - "java_type": c.param_type, - "required": c.required, - } - for c in r.parameter_changes - ], - } - ) - - diff_block = git_diff.strip()[:6000] if git_diff.strip() else "(无)" - - return f"""请根据以下 Controller 接口参数变更,**仅输出「兼容性提示」**,要求: - -{FRAMEWORK_IGNORE_HINT} - -## 输出格式(严格遵守) -- 只输出 3~6 行 Markdown,不要输出「整体说明」「接口变更详情」等标题 -- 不要逐条重复 URI 和参数列表(通知里已有) -- 不要提及「排除框架注入」相关字样 -- 重点说明:是否有破坏性变更、哪些必填参数调用方必须传入 -- 全新 Controller 说明「均为新接口,对现有调用方无破坏」即可 -- 语气简洁,可用 ... 标注风险项 - -## 变更文件 -{json.dumps(changed_files, ensure_ascii=False)} - -## AST 变更摘要 -{json.dumps(ast_report, ensure_ascii=False, indent=2)} - -## Git Diff -{diff_block} -""" - - -def review_parameter_changes( - reports: List[EndpointChangeReport], - config: Dict[str, Any], - changed_files: List[str], - git_diff: str = "", -) -> Optional[str]: - """LLM 审核,返回简短兼容性提示。""" - if not is_llm_enabled(config) or 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) diff --git a/.gitea/checker/main.py b/.gitea/checker/main.py deleted file mode 100644 index 97f0d09..0000000 --- a/.gitea/checker/main.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python3 -""" -AI-Check 主入口 — Controller 层接口参数变更检测(纯 Python,无 Java 依赖) -""" - -import argparse -import sys -from pathlib import Path -from typing import Optional - -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, - parse_endpoints_from_files, -) -from git_utils import ( - get_changed_java_controller_files, - get_controller_files_diff, - get_current_commit, - get_file_content_at_commit, - get_previous_commit_sha, -) -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 resolve_source_subdirs(config: dict) -> list: - """从配置解析 Java 源码目录列表(支持 source_dirs 多模块)。""" - dirs = config.get("source_dirs") - if dirs: - return [str(d) for d in dirs] - return [config.get("source_dir", "src/main/java")] - - -def _read_file_safe(path: Path) -> str: - """读取文件内容。""" - try: - return path.read_text(encoding="utf-8", errors="ignore") - except OSError as exc: - print(f"[警告] 无法读取 {path}: {exc}") - return "" - - -def _load_version_contents( - repo_root: Path, - file_paths: list, - commit_sha: Optional[str] = None, -) -> dict: - """加载文件内容;commit_sha 为空则读工作区,否则 git show。""" - contents = {} - for fp in file_paths: - norm = fp.replace("\\", "/") - if commit_sha: - text = get_file_content_at_commit(commit_sha, norm) - if text is not None: - contents[norm] = text - else: - text = _read_file_safe(repo_root / norm) - if text: - contents[norm] = text - return contents - - -def parse_changed_endpoints( - repo_root: Path, - source_subdirs: list, - changed_files: list, - old_sha: str, - label: str, -) -> dict: - """解析变更 Controller 文件在新/旧版本的端点。""" - if label == "new": - contents = _load_version_contents(repo_root, changed_files) - else: - contents = _load_version_contents(repo_root, changed_files, commit_sha=old_sha) - - print(f"[AST] 解析 {label} 版本 {len(contents)} 个 Controller 文件") - endpoints = parse_endpoints_from_files( - repo_root, source_subdirs, changed_files, contents - ) - print(f"[AST] {label} 版本共 {len(endpoints)} 个接口") - 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="推送人") - parser.add_argument("push_time", nargs="?", default=None, help="推送时间") - args = parser.parse_args() - - repo_root = Path(args.repo_root).resolve() - config_path = Path(args.config) - if not config_path.is_absolute(): - config_path = repo_root / config_path - config = load_config(config_path) - - if not config.get("check", {}).get("enabled", True): - print("[检查] API 变动检查已关闭(check.enabled=false),跳过。") - return 0 - - source_subdirs = resolve_source_subdirs(config) - - 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 接口参数变更检测(纯 Python)") - print("=" * 40) - print(f"推送人: {push_user}") - print(f"推送时间: {push_time}") - print(f"API 变动检查: {config.get('check', {}).get('enabled', True)}") - print(f"源码目录: {', '.join(source_subdirs)}") - 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 = [f.replace("\\", "/") for f in 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) - - new_map = parse_changed_endpoints( - repo_root, source_subdirs, changed_files, prev_sha, "new" - ) - old_map = parse_changed_endpoints( - repo_root, source_subdirs, changed_files, prev_sha, "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)} 个接口存在参数变更") - - llm_review = None - if reports: - llm_review = review_parameter_changes( - reports, config, changed_files, git_diff - ) - if llm_review: - print("[LLM] 参数变更审核完成") - - persist_change_log(reports, commit_info, config, llm_review) - - notify_cfg = config.get("notify", {}) - if notify_cfg.get("only_on_change", True) 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 - - send_parameter_change_notification( - webhook_url=config.get("wecom", {}).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()) diff --git a/.gitea/checker/models.py b/.gitea/checker/models.py deleted file mode 100644 index 17a277d..0000000 --- a/.gitea/checker/models.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Controller 端点数据模型。 -""" - -from dataclasses import dataclass, field -from typing import List, Optional - - -@dataclass -class ApiParameter: - """单个接口参数。""" - - name: str - type: str - required: bool = True - source: str = "query" - description: Optional[str] = None - parent_dto: Optional[str] = None - body_param_name: 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: - return f"{self.http_method} {self.uri}" diff --git a/.gitea/checker/notifier.py b/.gitea/checker/notifier.py deleted file mode 100644 index 9655f6c..0000000 --- a/.gitea/checker/notifier.py +++ /dev/null @@ -1,525 +0,0 @@ -""" -企业微信 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... 消息过长,已截断" - - -def _format_param_change_list(changes: List[ParameterChange]) -> List[str]: - """生成企微友好的普通参数变更列表(卡片式)。""" - if not changes: - return [''] - 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 [''] - - 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('') - - 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"- **全路径类名:** **{file_path}**" - - header = [ - f"- **变更类型:** **{change_type}**", - f"- **URI:** {uri_line}", - class_line, - ] - - if report.is_removed_endpoint: - return "\n".join(header + ["", f"**该接口已被移除**"]) - - 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("### 【兼容性提示】") - 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("### 兼容性提示") - 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"**{change_type}**" - - # 全路径类名高亮 - class_highlight = f"**{file_name}**" - - # 根据变更类型优化 URI 展示 - if change_type == "新增接口": - old_display = "`-`" - new_display = f"**`{new_uri}`****新增**" - elif change_type == "删除接口": - old_display = f"**`{old_uri}`****已删除**" - new_display = "`已删除`" - else: # 修改路径 - old_display = f"~~`{old_uri}`~~**旧路径**" - new_display = f"**`{new_uri}`****新路径**" - - 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 = '**修改请求方式**' - class_highlight = f'**{file_name}**' - uri_highlight = f'**`{uri}`**' - old_m = f'**{old_method}**' - new_m = f'**{new_method}**' - - parts = [ - "# 【API请求方式变更通知】", - "", - f" 变更类型: {type_highlight}", - f" 全路径类名: {class_highlight}", - f" 修改人: {push_user}", - f" 修改时间: {push_time}", - "", - "---------------------------------------", - "", - "#### 【请求方式变更详情】", - f"- **URI:** {uri_highlight}", - f"- **原请求方式:** {old_m}", - f"- **新请求方式:** {new_m} ← **请求方式已变更**", - "", - ] - 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) diff --git a/.gitea/checker/requirements.txt b/.gitea/checker/requirements.txt deleted file mode 100644 index 59061bf..0000000 --- a/.gitea/checker/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Python 依赖(版本锁定,避免 CI pip 冲突) -PyYAML==6.0.2 -requests==2.32.3 -charset-normalizer==3.4.1 -urllib3==2.2.3 -certifi==2024.8.30 -idna==3.10 -javalang==0.13.0 -six==1.16.0 diff --git a/.gitea/config.yaml b/.gitea/config.yaml deleted file mode 100644 index eb38f97..0000000 --- a/.gitea/config.yaml +++ /dev/null @@ -1,47 +0,0 @@ -# ============================================================ -# AI-Check 配置文件(位于 .gitea/ 目录,与业务代码解耦) -# ============================================================ - -# ---------- API 变动检查 ---------- -# 总开关:false 时跳过 Controller 接口参数变更检测(不对比、不通知) -check: - enabled: true - -# 业务 Java 源码目录(相对仓库根目录) -# 单模块: source_dir: "src/main/java" -# 多模块: 使用 source_dirs(优先于 source_dir) -source_dirs: - - "jnpf-ftb/jnpf-ftb-biz/src/main/java" - - "jnpf-ftb/jnpf-ftb-entity/src/main/java" -source_dir: "ftb/src/main/java" - -# ---------- 企业微信机器人 ---------- -wecom: - webhook_url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=61f08cc9-b734-4dff-a931-7f33654c0a81" - -# ---------- 豆包 LLM(审核接口参数变更)---------- -llm: - enabled: false - api_key: "2f3f7ee9-a6f7-46b7-a709-a36743a83a04" - model: "doubao-seed-1-8-251228" - endpoint_id: "" - api_url: "https://ark.cn-beijing.volces.com/api/v3/chat/completions" - timeout: null - -# ---------- 变更日志 ---------- -log: - enabled: false - storage: "file" - file_dir: ".gitea/logs/api-changes" - mysql: - host: "YOUR_MYSQL_HOST" - port: 3306 - user: "YOUR_MYSQL_USER" - password: "YOUR_MYSQL_PASSWORD" - database: "YOUR_MYSQL_DATABASE" - table: "api_change_logs" - -# ---------- 通知 ---------- -notify: - only_on_change: true - mentioned_users: "" diff --git a/.gitea/model.md b/.gitea/model.md deleted file mode 100644 index e1a441a..0000000 --- a/.gitea/model.md +++ /dev/null @@ -1,34 +0,0 @@ -# 【API参数变更通知】 - -- **变更类型:** {新增接口 / 修改参数 / 删除接口} -- **URI:** {Method} {URI} -- **修改人:** {Modifier} -- **修改时间:** {ModifyTime} -- **全路径类名:** {FileName} - ---- - -## 接口参数变动详情 - ---- - -### 类对象变更 -(如有对象替换或对象属性变更) - -- **对象:** {类名} -- **变更方式:** {对象属性变更 / 对象替换(旧类A → 新类B)} -- **属性变更明细:** - - [新增] 属性: `attr1` 说明: {说明} - - [删除] 属性: `attr2` 说明: {说明} - - [修改] 属性: `attr3` 说明: {说明} - -### 【参数变更】 -- **变更列表:** - -| 字段 | 说明 | 变更 | -|------|------|------| -| `pageSize` | 每页条数 | 新增必填 | -| `keyword` | 搜索关键词 | 类型由String改为Long | -| `startTime` | 开始时间 | 删除 | - -(如无变更,显示:无) \ No newline at end of file diff --git a/.gitea/model1.md b/.gitea/model1.md deleted file mode 100644 index b6f49b6..0000000 --- a/.gitea/model1.md +++ /dev/null @@ -1,17 +0,0 @@ -# 【API路径变更通知】 - - 变更类型: {新增接口 / 修改路径 / 删除接口} - 全路径类名: {FullClassName} - 修改人: {Modifier} - 修改时间: {ModifyTime} - ---- - -#### 【URI变更详情】 -- **原路径:** `{OldURI}` *(新增时显示:-)* -- **新路径:** `{NewURI}` *(删除时显示:已删除 / -)* - -**示例:** -- 全路径类名:`com.example.controller.UserController` -- 原路径:`/api/users/{id}` -- 新路径:`/api/users/getall` \ No newline at end of file diff --git a/.gitea/scripts/init_mysql.sql b/.gitea/scripts/init_mysql.sql deleted file mode 100644 index 50ac2bf..0000000 --- a/.gitea/scripts/init_mysql.sql +++ /dev/null @@ -1,17 +0,0 @@ --- MySQL 变更日志表(storage=mysql 时使用) --- 执行前请先创建数据库并替换 YOUR_MYSQL_DATABASE - -CREATE TABLE IF NOT EXISTS `api_change_logs` ( - `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', - `commit_sha` VARCHAR(64) NOT NULL COMMENT 'Git 提交 SHA', - `author` VARCHAR(128) NOT NULL COMMENT '提交人', - `commit_time` VARCHAR(64) NOT NULL COMMENT '提交时间', - `commit_message` TEXT NULL COMMENT '提交说明', - `change_count` INT NOT NULL DEFAULT 0 COMMENT '变更接口数量', - `reports_json` LONGTEXT NOT NULL COMMENT '变更详情 JSON', - `llm_review` TEXT NULL COMMENT 'LLM 评审结论', - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录写入时间', - PRIMARY KEY (`id`), - INDEX `idx_commit_sha` (`commit_sha`), - INDEX `idx_created_at` (`created_at`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API 接口参数变更日志'; diff --git a/.gitea/workflows/api-change-check.yml b/.gitea/workflows/api-change-check.yml deleted file mode 100644 index 4927b01..0000000 --- a/.gitea/workflows/api-change-check.yml +++ /dev/null @@ -1,46 +0,0 @@ -# Gitea Actions:Controller 接口参数变更检测(纯 Python,无 Java 构建) - -name: API接口参数变更检测 -run-name: ${{ gitea.actor }}的API参数变更检测 - -on: [push] - -jobs: - api-param-check: - if: ${{ gitea.ref != 'refs/heads/pre' && gitea.ref != 'refs/heads/dev' && gitea.ref != 'refs/heads/master-2.0' }} - runs-on: ubuntu-latest - - steps: - - name: 内置权限检出代码 - run: | - git config --global http.sslVerify false - git clone "https://${{ gitea.token }}@git.niujiekeji.com/${{ gitea.repository }}.git" . - git checkout ${{ gitea.sha }} - echo "当前提交: $(git rev-parse HEAD)" - echo "上一提交: $(git rev-parse HEAD~1 2>/dev/null || echo '无')" - - - name: 检查配置文件 - run: | - if [ ! -f .gitea/config.yaml ]; then - echo "错误: 缺少 .gitea/config.yaml" - exit 1 - fi - - - name: 安装 Python 依赖 - run: | - if ! python3 -m venv .gitea/.venv 2>/dev/null; then - sudo apt-get update -qq - sudo apt-get install -y python3-venv - python3 -m venv .gitea/.venv - fi - .gitea/.venv/bin/pip install --upgrade pip - .gitea/.venv/bin/pip install -r .gitea/checker/requirements.txt - - - name: 检测 Controller 接口参数变更 - run: | - COMMIT_TIME=$(git log -1 --format=%cd --date=format:'%Y-%m-%d %H:%M:%S') - .gitea/.venv/bin/python .gitea/checker/main.py \ - --config .gitea/config.yaml \ - --repo-root . \ - "${{ gitea.actor }}" \ - "$COMMIT_TIME"