- Phase 1: Infrastructure (state, prompts, registry) - Phase 2: Execution engine (AI adapters, security classifier, executors) - Phase 3: Agent integration (graph nodes, routing) - Phase 4: Streaming interaction (PTY terminal, WebSocket) - Phase 5: Frontend integration (Vue components)
197 lines
5.1 KiB
Python
197 lines
5.1 KiB
Python
"""
|
|
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
|