Files
JARVIS/backend/app/agents/tools/ai_adapter.py
WIN-JHFT4D3SIVT\caoxiaozhu 5667190abe feat(agents): implement Code Commander module (Phases 1-5)
- 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)
2026-04-05 14:56:45 +08:00

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