""" AI CLI Adapter - 统一接口适配不同 AI CLI (Claude/Gemini/Codex/OpenCode) """ from abc import ABC, abstractmethod from dataclasses import dataclass, field from pathlib import Path from typing import Literal @dataclass class CodeExecutionResult: """代码执行结果""" success: bool message: str files_created: list[str] = field(default_factory=list) output: str = "" error: str | None = None exit_code: int = 0 class AICLIAdapter(ABC): """AI CLI 适配器抽象基类""" @property @abstractmethod def cli_name(self) -> str: """CLI 命令名称,如 'claude', 'gemini'""" pass @property @abstractmethod def requires_workspace(self) -> bool: """是否需要工作目录""" pass @property def provider(self) -> Literal["claude", "gemini", "codex", "opencode"]: """AI 提供商标识""" return self.cli_name @abstractmethod def build_command(self, prompt: str, workspace: Path | None) -> list[str]: """构建 CLI 命令""" pass @abstractmethod def parse_output(self, output: str) -> CodeExecutionResult: """解析 CLI 输出""" pass @abstractmethod def is_installed(self) -> bool: """检查 CLI 是否已安装""" pass class ClaudeAdapter(AICLIAdapter): """Claude CLI 适配器""" cli_name = "claude" requires_workspace = True def build_command(self, prompt: str, workspace: Path | None) -> list[str]: cmd = ["claude", "-p", prompt] if workspace: cmd.extend(["--output-format", "stream-json"]) cmd.append("--dangerously-skip-permissions") return cmd def parse_output(self, output: str) -> CodeExecutionResult: # Claude CLI 输出可能是纯文本或 JSON # 简化处理:直接返回输出 if not output.strip(): return CodeExecutionResult( success=False, message="No output from Claude CLI", output=output, ) return CodeExecutionResult( success=True, message="Execution completed", output=output, ) def is_installed(self) -> bool: import shutil return shutil.which("claude") is not None class GeminiAdapter(AICLIAdapter): """Gemini CLI 适配器""" cli_name = "gemini" requires_workspace = False def build_command(self, prompt: str, workspace: Path | None) -> list[str]: cmd = ["gemini", "-p", prompt] return cmd def parse_output(self, output: str) -> CodeExecutionResult: if not output.strip(): return CodeExecutionResult( success=False, message="No output from Gemini CLI", output=output, ) return CodeExecutionResult( success=True, message="Execution completed", output=output, ) def is_installed(self) -> bool: import shutil return shutil.which("gemini") is not None class CodexAdapter(AICLIAdapter): """Codex CLI 适配器""" cli_name = "codex" requires_workspace = True def build_command(self, prompt: str, workspace: Path | None) -> list[str]: cmd = ["codex", "-p", prompt] return cmd def parse_output(self, output: str) -> CodeExecutionResult: if not output.strip(): return CodeExecutionResult( success=False, message="No output from Codex CLI", output=output, ) return CodeExecutionResult( success=True, message="Execution completed", output=output, ) def is_installed(self) -> bool: import shutil return shutil.which("codex") is not None class OpenCodeAdapter(AICLIAdapter): """OpenCode CLI 适配器""" cli_name = "opencode" requires_workspace = True def build_command(self, prompt: str, workspace: Path | None) -> list[str]: cmd = ["opencode", "-p", prompt] return cmd def parse_output(self, output: str) -> CodeExecutionResult: if not output.strip(): return CodeExecutionResult( success=False, message="No output from OpenCode CLI", output=output, ) return CodeExecutionResult( success=True, message="Execution completed", output=output, ) def is_installed(self) -> bool: import shutil return shutil.which("opencode") is not None # 提供商注册表 ADAPTER_REGISTRY: dict[str, AICLIAdapter] = { "claude": ClaudeAdapter(), "gemini": GeminiAdapter(), "codex": CodexAdapter(), "opencode": OpenCodeAdapter(), } def get_adapter(provider: str) -> AICLIAdapter: """获取指定提供商的适配器""" adapter = ADAPTER_REGISTRY.get(provider.lower()) if adapter is None: raise ValueError( f"Unknown AI provider: {provider}. Available: {list(ADAPTER_REGISTRY.keys())}" ) return adapter