322 lines
8.4 KiB
Markdown
322 lines
8.4 KiB
Markdown
|
|
# 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(流式交互)
|
|||
|
|
```
|