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"