Files
awoooi/apps/api/src/services/openclaw_http_service.py
OG T 8313a3787b
Some checks failed
E2E Health Check / e2e-health (push) Has been cancelled
refactor(api): Phase 22 P0 leWOOOgo 模組化修復
Router 層禁止直接 httpx.AsyncClient,抽取到 Service 層:

新增 Services:
- OpenClawHttpService: Error 分析/Code Review/CI 診斷
- GitHubApiService: PR Diff 取得
- HealthCheckService: HTTP/PostgreSQL/Redis 健康檢查

修改 Routers:
- sentry_webhook.py: 使用 OpenClawHttpService
- github_webhook.py: 使用 GitHubApiService + OpenClawHttpService
- health.py: 使用 HealthCheckService

遵循規範:
- Skill 09: Router 層禁止直接外部 API 呼叫
- feedback_lewooogo_modular_enforcement.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-31 16:06:35 +08:00

281 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
OpenClaw HTTP Service - 統一 OpenClaw API 呼叫
==============================================
Phase 22 P0 修復: Router 層禁止直接 httpx.AsyncClient
遵循規範:
- Skill 09: Router 層禁止直接外部 API 呼叫
- feedback_lewooogo_modular_enforcement.md: Service 層封裝
功能:
- Error Analysis (Sentry 錯誤分析)
- Code Review (PR 代碼審查)
- Push Review (Push 審查)
- CI Diagnosis (CI 失敗診斷)
設計原則:
- 返回 dict | None由調用者決定如何轉換為具體類型
- 保持與現有調用者的相容性
版本: v1.0
建立: 2026-03-31 (台北時區)
建立者: Claude Code (首席架構師 P0 修復)
"""
import httpx
import structlog
from src.core.config import settings
logger = structlog.get_logger(__name__)
# =============================================================================
# OpenClaw HTTP Service
# =============================================================================
class OpenClawHttpService:
"""
OpenClaw HTTP Service
統一 OpenClaw API 呼叫,符合 leWOOOgo 積木化原則
2026-03-31 Claude Code (Phase 22 P0 修復)
"""
def __init__(
self,
base_url: str | None = None,
default_timeout: float = 60.0,
):
self._base_url = base_url or settings.OPENCLAW_URL
self._default_timeout = default_timeout
async def analyze_error(
self,
error_context: dict,
prefer_local: bool = True,
timeout: float | None = None,
) -> dict | None:
"""
呼叫 OpenClaw Error Analyzer Agent
Args:
error_context: 錯誤上下文
prefer_local: 優先使用 Ollama (本地,零成本)
timeout: 超時秒數 (預設 60s)
Returns:
dict | None: API 回應 JSON由調用者轉換為具體類型
"""
try:
async with httpx.AsyncClient(
timeout=timeout or self._default_timeout
) as client:
response = await client.post(
f"{self._base_url}/api/v1/analyze/error",
json={
"error_context": error_context,
"prefer_local": prefer_local,
},
)
if response.status_code == 200:
return response.json()
else:
logger.warning(
"openclaw_analyze_error_failed",
status=response.status_code,
)
return None
except httpx.TimeoutException:
logger.warning("openclaw_analyze_error_timeout")
return None
except Exception as e:
logger.exception("openclaw_analyze_error_exception", error=str(e))
return None
async def code_review(
self,
repo_name: str,
pr_title: str,
pr_body: str,
diff_content: str,
changed_files: int,
additions: int,
deletions: int,
prefer_local: bool = True,
timeout: float = 120.0,
) -> dict | None:
"""
呼叫 OpenClaw 進行 PR 代碼審查
Args:
repo_name: 倉庫名稱
pr_title: PR 標題
pr_body: PR 描述
diff_content: diff 內容
changed_files: 變更檔案數
additions: 新增行數
deletions: 刪除行數
prefer_local: 優先 Ollama
timeout: 超時秒數 (預設 120s)
Returns:
dict | None: API 回應 JSON
"""
try:
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.post(
f"{self._base_url}/api/v1/analyze/code-review",
json={
"repo": repo_name,
"pr_title": pr_title,
"pr_body": pr_body,
"diff": diff_content,
"changed_files": changed_files,
"additions": additions,
"deletions": deletions,
"prefer_local": prefer_local,
},
)
if response.status_code == 200:
return response.json()
else:
logger.warning(
"openclaw_code_review_failed",
status=response.status_code,
response=response.text[:200],
)
return None
except httpx.TimeoutException:
logger.warning("openclaw_code_review_timeout")
return None
except Exception as e:
logger.exception("openclaw_code_review_exception", error=str(e))
return None
async def push_review(
self,
repo_name: str,
ref: str,
commits: list[dict],
files_changed: dict,
prefer_local: bool = True,
timeout: float = 120.0,
) -> dict | None:
"""
呼叫 OpenClaw 進行 Push 代碼審查
Args:
repo_name: 倉庫名稱
ref: Git ref
commits: commit 列表
files_changed: 變更檔案
prefer_local: 優先 Ollama
timeout: 超時秒數
Returns:
dict | None: API 回應 JSON
"""
try:
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.post(
f"{self._base_url}/api/v1/analyze/push-review",
json={
"repo": repo_name,
"ref": ref,
"commits": commits,
"files_changed": files_changed,
"prefer_local": prefer_local,
},
)
if response.status_code == 200:
return response.json()
else:
logger.warning(
"openclaw_push_review_failed",
status=response.status_code,
)
return None
except httpx.TimeoutException:
logger.warning("openclaw_push_review_timeout")
return None
except Exception as e:
logger.exception("openclaw_push_review_exception", error=str(e))
return None
async def ci_diagnosis(
self,
repo_name: str,
failure_context: dict,
prefer_local: bool = True,
timeout: float = 120.0,
) -> dict | None:
"""
呼叫 OpenClaw 進行 CI 失敗診斷
Args:
repo_name: 倉庫名稱
failure_context: 失敗上下文
prefer_local: 優先 Ollama
timeout: 超時秒數
Returns:
dict | None: API 回應 JSON
"""
try:
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.post(
f"{self._base_url}/api/v1/analyze/ci-failure",
json={
"repo": repo_name,
"workflow_name": failure_context.get("workflow_name"),
"conclusion": failure_context.get("conclusion"),
"head_sha": failure_context.get("head_sha"),
"head_branch": failure_context.get("head_branch"),
"event_trigger": failure_context.get("event_trigger"),
"run_number": failure_context.get("run_number"),
"run_attempt": failure_context.get("run_attempt"),
"workflow_url": failure_context.get("html_url"),
"prefer_local": prefer_local,
},
)
if response.status_code == 200:
return response.json()
else:
logger.warning(
"openclaw_ci_diagnosis_failed",
status=response.status_code,
)
return None
except httpx.TimeoutException:
logger.warning("openclaw_ci_diagnosis_timeout")
return None
except Exception as e:
logger.exception("openclaw_ci_diagnosis_exception", error=str(e))
return None
# =============================================================================
# Singleton
# =============================================================================
_openclaw_http_service: OpenClawHttpService | None = None
def get_openclaw_http_service() -> OpenClawHttpService:
"""取得 OpenClawHttpService singleton"""
global _openclaw_http_service
if _openclaw_http_service is None:
_openclaw_http_service = OpenClawHttpService()
return _openclaw_http_service