""" GitHub API Service - 統一 GitHub API 呼叫 ========================================= Phase 22 P0 修復: Router 層禁止直接 httpx.AsyncClient 遵循規範: - Skill 09: Router 層禁止直接外部 API 呼叫 - feedback_lewooogo_modular_enforcement.md: Service 層封裝 功能: - Fetch PR Diff (取得 PR 差異內容) - 未來擴展: PR Comments, Status Checks, etc. 版本: v1.0 建立: 2026-03-31 (台北時區) 建立者: Claude Code (首席架構師 P0 修復) """ import httpx import structlog logger = structlog.get_logger(__name__) # ============================================================================= # Constants # ============================================================================= # 最大 diff 字元數 (約 12500 tokens) MAX_DIFF_CHARS = 50000 # ============================================================================= # GitHub API Service # ============================================================================= class GitHubApiService: """ GitHub API Service 統一 GitHub API 呼叫,符合 leWOOOgo 積木化原則 2026-03-31 Claude Code (Phase 22 P0 修復) """ def __init__( self, default_timeout: float = 30.0, max_diff_chars: int = MAX_DIFF_CHARS, ): self._default_timeout = default_timeout self._max_diff_chars = max_diff_chars async def fetch_pr_diff( self, diff_url: str, timeout: float | None = None, ) -> str: """ 取得 PR diff 內容 Args: diff_url: GitHub diff URL timeout: 超時秒數 (預設 30s) Returns: str: diff 內容 (超過限制會截斷) """ try: async with httpx.AsyncClient( timeout=timeout or self._default_timeout ) as client: response = await client.get( diff_url, headers={"Accept": "application/vnd.github.v3.diff"}, ) if response.status_code == 200: diff_content = response.text # 限制 diff 大小 (避免 LLM token 過多) if len(diff_content) > self._max_diff_chars: diff_content = ( diff_content[: self._max_diff_chars] + "\n... (truncated)" ) return diff_content else: logger.warning( "github_diff_fetch_failed", url=diff_url, status=response.status_code, ) return "" except httpx.TimeoutException: logger.warning("github_diff_fetch_timeout", url=diff_url) return "" except Exception as e: logger.warning("github_diff_fetch_error", error=str(e)) return "" # ============================================================================= # Singleton # ============================================================================= _github_api_service: GitHubApiService | None = None def get_github_api_service() -> GitHubApiService: """取得 GitHubApiService singleton""" global _github_api_service if _github_api_service is None: _github_api_service = GitHubApiService() return _github_api_service