# Phase 2：执行引擎

日期：2026-04-04
状态：待实施

依赖：Phase 1 完成

---

## 1. 本阶段目的

实现代码指挥官的核心执行能力：
- AI CLI Adapter：统一接口适配不同 AI CLI
- Sandbox Executor：沙盒环境执行
- Direct Executor：直接执行低风险任务
- Security Classifier：安全分级

---

## 2. 详细任务

### 2.1 AI CLI Adapter

**新文件**: `backend/app/agents/tools/ai_adapter.py`

```python
from abc import ABC, abstractmethod
from pathlib import Path
from dataclasses import dataclass

@dataclass
class CodeExecutionResult:
    success: bool
    message: str
    files_created: list[str]
    output: str
    error: str | None

class AICLIAdapter(ABC):
    @property
    @abstractmethod
    def cli_name(self) -> str:
        """CLI 命令名称，如 'claude', 'gemini'"""
        pass
    
    @property
    @abstractmethod
    def requires_workspace(self) -> bool:
        """是否需要工作目录"""
        pass
    
    @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):
    cli_name = "claude"
    requires_workspace = True
    
    def build_command(self, prompt: str, workspace: Path | None) -> list[str]:
        return ["claude", "-p", prompt, "--dangerously-skip-permissions"]
    
    # ... 其他方法实现

class GeminiAdapter(AICLIAdapter):
    cli_name = "gemini"
    requires_workspace = False
    # ...

class CodexAdapter(AICLIAdapter):
    cli_name = "codex"
    # ...

class OpenCodeAdapter(AICLIAdapter):
    cli_name = "opencode"
    # ...
```

### 2.2 Security Classifier

**新文件**: `backend/app/agents/tools/security_classifier.py`

```python
from enum import Enum

class RiskLevel(Enum):
    LOW = "low"      # 直接执行
    HIGH = "high"    # 沙盒执行

class SecurityClassifier:
    HIGH_RISK_KEYWORDS = [
        "修改", "编辑", "删除", "移动",
        "Jarvis", "backend", "frontend",
        "git", "config", ".env",
    ]
    
    LOW_RISK_KEYWORDS = [
        "demo", "示例", "贪食蛇", "俄罗斯方块",
        "小游戏", "独立项目", "新项目",
        "创建一个", "写一个",
    ]
    
    def classify(self, task_description: str, target_path: str | None = None) -> RiskLevel:
        # 1. 检查高风险关键词
        if any(kw in task_description for kw in self.HIGH_RISK_KEYWORDS):
            return RiskLevel.HIGH
        
        # 2. 检查目标路径
        if target_path and self._is_project_path(target_path):
            return RiskLevel.HIGH
        
        # 3. 检查低风险关键词
        if any(kw in task_description for kw in self.LOW_RISK_KEYWORDS):
            return RiskLevel.LOW
        
        # 4. 默认高风险
        return RiskLevel.HIGH
    
    def _is_project_path(self, path: str) -> bool:
        # 检查是否指向 Jarvis 项目路径
        return "Jarvis" in path or "backend/app" in path
```

### 2.3 Sandbox Executor

**新文件**: `backend/app/agents/tools/sandbox_executor.py`

```python
import tempfile
import shutil
import asyncio
from pathlib import Path
from dataclasses import dataclass, field
from typing import AsyncGenerator

@dataclass
class SandboxEnvironment:
    workspace_path: Path
    session_id: str
    
    @staticmethod
    async def create() -> "SandboxEnvironment":
        """创建新的沙盒环境"""
        temp_dir = tempfile.mkdtemp(prefix="jarvis_code_")
        session_id = Path(temp_dir).name
        return SandboxEnvironment(
            workspace_path=Path(temp_dir),
            session_id=session_id,
        )
    
    async def cleanup(self):
        """清理沙盒环境"""
        if self.workspace_path.exists():
            shutil.rmtree(self.workspace_path)

@dataclass
class ExecutionResult:
    success: bool
    exit_code: int
    stdout: str
    stderr: str
    files_created: list[str] = field(default_factory=list)

class SandboxExecutor:
    def __init__(self, adapter: AICLIAdapter, timeout: int = 300):
        self.adapter = adapter
        self.timeout = timeout
        self._sessions: dict[str, SandboxEnvironment] = {}
    
    async def execute(
        self, 
        prompt: str, 
        session_id: str | None = None
    ) -> AsyncGenerator[str, None]:
        """执行代码任务，yield 实时输出"""
        # 1. 创建或复用沙盒环境
        if session_id and session_id in self._sessions:
            env = self._sessions[session_id]
        else:
            env = await SandboxEnvironment.create()
            self._sessions[env.session_id] = env
            session_id = env.session_id
        
        # 2. 构建命令
        cmd = self.adapter.build_command(prompt, env.workspace_path)
        
        # 3. 异步执行，实时 yield 输出
        process = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=str(env.workspace_path),
        )
        
        # 4. 实时读取输出
        while True:
            line = await process.stdout.readline()
            if not line:
                break
            yield line.decode()
        
        # 5. 等待完成
        await process.wait()
        
        # 6. 收集结果
        return ExecutionResult(
            success=process.returncode == 0,
            exit_code=process.returncode or 0,
            stdout=...,
            stderr=...,
            files_created=self._list_created_files(env.workspace_path),
        )
    
    async def cleanup_session(self, session_id: str):
        """清理指定会话"""
        if session_id in self._sessions:
            await self._sessions[session_id].cleanup()
            del self._sessions[session_id]
```

### 2.4 Direct Executor

**新文件**: `backend/app/agents/tools/direct_executor.py`

```python
class DirectExecutor:
    def __init__(self, adapter: AICLIAdapter, timeout: int = 60):
        self.adapter = adapter
        self.timeout = timeout
    
    async def execute(self, prompt: str) -> ExecutionResult:
        """直接执行，不需要沙盒"""
        if not self.adapter.is_installed():
            return ExecutionResult(
                success=False,
                exit_code=-1,
                stdout="",
                stderr=f"{self.adapter.cli_name} is not installed",
            )
        
        cmd = self.adapter.build_command(prompt, None)
        
        process = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        
        try:
            stdout, stderr = await asyncio.wait_for(
                process.communicate(),
                timeout=self.timeout,
            )
            return ExecutionResult(
                success=process.returncode == 0,
                exit_code=process.returncode or 0,
                stdout=stdout.decode(),
                stderr=stderr.decode(),
            )
        except asyncio.TimeoutError:
            process.kill()
            return ExecutionResult(
                success=False,
                exit_code=-1,
                stdout="",
                stderr=f"Execution timed out after {self.timeout}s",
            )
```

---

## 3. 核心文件清单

| 文件 | 操作 | 说明 |
|------|------|------|
| `ai_adapter.py` | 新增 | 抽象基类 + 4 个具体实现 |
| `security_classifier.py` | 新增 | 安全分级器 |
| `sandbox_executor.py` | 新增 | 沙盒执行器 |
| `direct_executor.py` | 新增 | 直接执行器 |

---

## 4. 验收标准

- [ ] `AICLIAdapter` 可以正确识别 4 种 CLI
- [ ] `SecurityClassifier` 能正确分类高低风险
- [ ] `SandboxExecutor` 能创建、执行、清理沙盒
- [ ] `DirectExecutor` 能直接执行低风险任务
- [ ] 所有执行器支持流式输出

---

## 5. 风险与缓解

| 风险 | 缓解 |
|------|------|
| AI CLI 未安装 | `is_installed()` 检查 + 友好提示 |
| 执行超时 | `timeout` 参数控制 |
| 沙盒清理遗漏 | 使用 `finally` 块确保清理 |

---

## 6. 依赖关系

```
Phase 1（基础设施）
       ↓
本阶段 → Phase 3（Agent 集成）
       → Phase 4（流式交互）
```
