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)
This commit is contained in:
196
backend/app/agents/tools/ai_adapter.py
Normal file
196
backend/app/agents/tools/ai_adapter.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user