""" Neural Sync - Auto Merge Service 自动合并服务 - 严格模式 - 审计评分 >= 98 分 - 全量测试通过 - 二次人工确认 - 熔断机制 """ import asyncio import logging from typing import Optional, Dict, List, Any, Tuple from pydantic import BaseModel, Field from datetime import datetime, timedelta from enum import Enum from ..events import Event, EventType logger = logging.getLogger(__name__) class MergeStatus(str, Enum): """合并状态""" PENDING = "pending" WAITING_CONFIRMATION = "waiting_confirmation" APPROVED = "approved" REJECTED = "rejected" FAILED = "failed" COOLDOWN = "cooldown" class MergeRequest(BaseModel): """合并请求""" signal_id: str commit_sha: str task_name: str audit_score: float = Field(ge=0.0, le=100.0) test_results: Dict[str, Any] requester: Optional[str] = None class MergeResponse(BaseModel): """合并响应""" status: MergeStatus success: bool merge_sha: Optional[str] = None reason: Optional[str] = None requires_human_confirmation: bool = False confirmation_token: Optional[str] = None cooldown_remaining: Optional[float] = None details: Dict[str, Any] = Field(default_factory=dict) class ConfirmationRequest(BaseModel): """人工确认请求""" token: str merge_request: MergeRequest created_at: float expires_at: float class AutoMergeService: """ 自动合并服务 - 严格模式 条件: 1. 审计评分 >= 98 分 2. 全量测试通过 3. 二次人工确认 4. 不在熔断期内 特性: - 熔断机制(连续失败后暂停) - 人工确认流程 - 回滚支持 - 事件通知 """ # 严格模式阈值 AUDIT_SCORE_THRESHOLD = 98.0 HUMAN_CONFIRMATION_REQUIRED = True CONFIRMATION_TIMEOUT = 300 # 5 minutes # 熔断机制 MAX_CONSECUTIVE_FAILURES = 3 FAILURE_COOLDOWN_SECONDS = 3600 # 1 hour def __init__(self, git_service=None, plane_service=None, lark_service=None, event_bus=None): self.git = git_service self.plane = plane_service self.lark = lark_service self.event_bus = event_bus # 状态跟踪 self._failure_count = 0 self._last_failure_time: Optional[datetime] = None self._successful_merges = 0 # 待确认的请求 self._pending_confirmations: Dict[str, ConfirmationRequest] = {} async def try_auto_merge(self, request: MergeRequest) -> MergeResponse: """ 尝试自动合并 """ # 1. 检查熔断状态 if self._is_in_cooldown(): return MergeResponse( status=MergeStatus.COOLDOWN, success=False, reason=f"系统处于熔断期,请在 {self._get_cooldown_remaining()}s 后重试", requires_human_confirmation=True, cooldown_remaining=self._get_cooldown_remaining(), ) # 2. 验证审计评分 if request.audit_score < self.AUDIT_SCORE_THRESHOLD: return MergeResponse( status=MergeStatus.REJECTED, success=False, reason=f"审计评分 {request.audit_score} 低于阈值 {self.AUDIT_SCORE_THRESHOLD}", requires_human_confirmation=True, ) # 3. 验证测试结果 if not self._all_tests_passed(request.test_results): return MergeResponse( status=MergeStatus.REJECTED, success=False, reason="测试未全部通过", requires_human_confirmation=True, details={ "test_results": request.test_results, "failed_tests": self._get_failed_tests(request.test_results), }, ) # 4. 创建人工确认请求 if self.HUMAN_CONFIRMATION_REQUIRED: confirmation_token = self._generate_confirmation_token() expires_at = datetime.utcnow().timestamp() + self.CONFIRMATION_TIMEOUT confirmation = ConfirmationRequest( token=confirmation_token, merge_request=request, created_at=datetime.utcnow().timestamp(), expires_at=expires_at, ) self._pending_confirmations[confirmation_token] = confirmation # 发送确认请求事件 await self._notify_confirmation_request(confirmation) return MergeResponse( status=MergeStatus.WAITING_CONFIRMATION, success=False, reason="等待人工二次确认", requires_human_confirmation=True, confirmation_token=confirmation_token, details={ "expires_at": expires_at, "timeout_seconds": self.CONFIRMATION_TIMEOUT, }, ) # 5. 直接执行自动合并(不需要确认) return await self._execute_merge(request) async def confirm_merge(self, confirmation_token: str, requester: str) -> MergeResponse: """ 人工确认合并 """ confirmation = self._pending_confirmations.get(confirmation_token) if not confirmation: return MergeResponse( status=MergeStatus.REJECTED, success=False, reason="确认令牌无效或已过期", ) # 检查是否过期 if datetime.utcnow().timestamp() > confirmation.expires_at: del self._pending_confirmations[confirmation_token] return MergeResponse( status=MergeStatus.REJECTED, success=False, reason="确认请求已过期", ) # 执行合并 result = await self._execute_merge(confirmation.merge_request) # 清理确认请求 if result.success: del self._pending_confirmations[confirmation_token] return result async def reject_merge(self, confirmation_token: str, reason: str) -> MergeResponse: """ 拒绝合并 """ confirmation = self._pending_confirmations.get(confirmation_token) if not confirmation: return MergeResponse( status=MergeStatus.REJECTED, success=False, reason="确认令牌无效", ) # 清理确认请求 del self._pending_confirmations[confirmation_token] return MergeResponse( status=MergeStatus.REJECTED, success=False, reason=f"合并被拒绝: {reason}", ) async def _execute_merge(self, request: MergeRequest) -> MergeResponse: """ 执行实际的合并操作 """ try: # 1. 执行 Git 合并 merge_sha = await self._perform_git_merge(request.commit_sha) # 2. 推送到远程 await self._push_to_remote() # 3. 更新 Plane 任务状态 if self.plane: await self._update_plane_status(request.signal_id, merge_sha) # 4. 发送飞书通知 if self.lark: await self._send_lark_notification(request, merge_sha) # 5. 发送成功事件 await self._publish_merge_success_event(request, merge_sha) # 重置失败计数 self._reset_failure_count() self._successful_merges += 1 return MergeResponse( status=MergeStatus.APPROVED, success=True, merge_sha=merge_sha, reason="自动合并成功", details={ "successful_merges": self._successful_merges, }, ) except Exception as e: logger.error(f"Merge failed: {e}", exc_info=True) self._record_failure() await self._publish_merge_failed_event(request, str(e)) return MergeResponse( status=MergeStatus.FAILED, success=False, reason=f"合并失败: {str(e)}", requires_human_confirmation=True, ) async def _perform_git_merge(self, commit_sha: str) -> str: """执行 Git 合并""" # 如果有 git 服务,使用它;否则使用 subprocess if self.git: return await self.git.merge(commit_sha) # 使用 subprocess 执行 git 命令 import subprocess # 合并提交(不创建合并提交,直接快进) result = subprocess.run( ["git", "merge", "--ff", commit_sha], capture_output=True, text=True, timeout=30, ) if result.returncode != 0: # 尝试 cherry-pick result = subprocess.run( ["git", "cherry-pick", commit_sha], capture_output=True, text=True, timeout=30, ) if result.returncode != 0: raise Exception(f"Git merge/cherry-pick failed: {result.stderr}") # 获取合并后的 HEAD result = subprocess.run( ["git", "rev-parse", "HEAD"], capture_output=True, text=True, check=True, ) return result.stdout.strip() async def _push_to_remote(self): """推送到远程仓库""" import subprocess result = subprocess.run( ["git", "push"], capture_output=True, text=True, timeout=60, ) if result.returncode != 0: raise Exception(f"Git push failed: {result.stderr}") async def _update_plane_status(self, signal_id: str, merge_sha: str): """更新 Plane 任务状态""" if not self.plane: return try: # 这里应该根据 signal_id 找到对应的 Plane issue # 简化实现:假设任务 ID 已知 await self.plane.update_issue_status( signal_id, # 或提取的任务 ID status="Done" ) except Exception as e: logger.warning(f"Failed to update Plane status: {e}") async def _send_lark_notification(self, request: MergeRequest, merge_sha: str): """发送飞书通知""" if not self.lark: return try: await self.lark.send_merge_notification( task_name=request.task_name, merge_sha=merge_sha, audit_score=request.audit_score, ) except Exception as e: logger.warning(f"Failed to send Lark notification: {e}") async def _notify_confirmation_request(self, confirmation: ConfirmationRequest): """通知确认请求""" if self.event_bus: from .events import EventType await self.event_bus.publish( Event( type=EventType.AUTO_MERGE_REQUESTED, data={ "token": confirmation.token, "task_name": confirmation.merge_request.task_name, "commit_sha": confirmation.merge_request.commit_sha, "audit_score": confirmation.merge_request.audit_score, "expires_at": confirmation.expires_at, }, ) ) async def _publish_merge_success_event(self, request: MergeRequest, merge_sha: str): """发布合并成功事件""" if self.event_bus: from .events import EventType await self.event_bus.publish( Event( type=EventType.AUTO_MERGE_SUCCESS, data={ "signal_id": request.signal_id, "commit_sha": request.commit_sha, "merge_sha": merge_sha, "task_name": request.task_name, "audit_score": request.audit_score, }, ) ) async def _publish_merge_failed_event(self, request: MergeRequest, error: str): """发布合并失败事件""" if self.event_bus: from .events import EventType await self.event_bus.publish( Event( type=EventType.AUTO_MERGE_FAILED, data={ "signal_id": request.signal_id, "commit_sha": request.commit_sha, "task_name": request.task_name, "error": error, }, ) ) def _is_in_cooldown(self) -> bool: """检查是否在熔断期""" if self._last_failure_time is None: return False elapsed = (datetime.utcnow() - self._last_failure_time).total_seconds() return elapsed < self.FAILURE_COOLDOWN_SECONDS def _get_cooldown_remaining(self) -> float: """获取熔断剩余时间""" if self._last_failure_time is None: return 0.0 elapsed = (datetime.utcnow() - self._last_failure_time).total_seconds() return max(0, self.FAILURE_COOLDOWN_SECONDS - elapsed) def _all_tests_passed(self, test_results: Dict[str, Any]) -> bool: """检查所有测试是否通过""" if not test_results: return True # 没有测试结果视为通过 # 检查是否有失败的测试 for test_name, result in test_results.items(): if isinstance(result, dict): if result.get("status") == "failed": return False if result.get("passed") is False: return False elif isinstance(result, bool): if not result: return False return True def _get_failed_tests(self, test_results: Dict[str, Any]) -> List[str]: """获取失败的测试列表""" failed = [] for test_name, result in test_results.items(): if isinstance(result, dict) and result.get("status") == "failed": failed.append(test_name) elif isinstance(result, bool) and not result: failed.append(test_name) return failed def _generate_confirmation_token(self) -> str: """生成确认令牌""" import uuid return str(uuid.uuid4()) def _record_failure(self): """记录失败""" self._failure_count += 1 self._last_failure_time = datetime.utcnow() # 检查是否需要进入熔断期 if self._failure_count >= self.MAX_CONSECUTIVE_FAILURES: logger.warning( f"[AutoMerge] {self._failure_count} consecutive failures. " f"Entering cooldown for {self.FAILURE_COOLDOWN_SECONDS}s" ) if self.event_bus: import asyncio asyncio.create_task( self.event_bus.publish( Event( type=EventType.AUTO_MERGE_COOLDOWN, data={ "failure_count": self._failure_count, "cooldown_seconds": self.FAILURE_COOLDOWN_SECONDS, }, ) ) ) def _reset_failure_count(self): """重置失败计数""" self._failure_count = 0 self._last_failure_time = None async def get_pending_confirmations(self) -> List[ConfirmationRequest]: """获取待确认的请求""" # 清理过期的确认请求 now = datetime.utcnow().timestamp() expired = [ token for token, conf in self._pending_confirmations.items() if now > conf.expires_at ] for token in expired: del self._pending_confirmations[token] return list(self._pending_confirmations.values()) def get_status(self) -> Dict[str, Any]: """获取服务状态""" return { "audit_threshold": self.AUDIT_SCORE_THRESHOLD, "human_confirmation_required": self.HUMAN_CONFIRMATION_REQUIRED, "failure_count": self._failure_count, "successful_merges": self._successful_merges, "in_cooldown": self._is_in_cooldown(), "cooldown_remaining": self._get_cooldown_remaining() if self._is_in_cooldown() else 0, "pending_confirmations": len(self._pending_confirmations), } # 全局服务实例 _auto_merger = None def get_auto_merger( git_service=None, plane_service=None, lark_service=None, event_bus=None, ) -> AutoMergeService: """获取自动合并服务实例""" global _auto_merger if _auto_merger is None: _auto_merger = AutoMergeService( git_service=git_service, plane_service=plane_service, lark_service=lark_service, event_bus=event_bus, ) return _auto_merger