""" Sandbox Executor - 沙盒执行器 在高风险任务在隔离的临时目录中执行 """ import asyncio import os import shutil import tempfile from dataclasses import dataclass, field from pathlib import Path from typing import AsyncGenerator from app.agents.tools.ai_adapter import AICLIAdapter, CodeExecutionResult @dataclass class SandboxEnvironment: """沙盒环境""" workspace_path: Path session_id: str @staticmethod async def create(prefix: str = "jarvis_code_") -> "SandboxEnvironment": """创建新的沙盒环境""" temp_dir = tempfile.mkdtemp(prefix=prefix) session_id = Path(temp_dir).name return SandboxEnvironment( workspace_path=Path(temp_dir), session_id=session_id, ) async def cleanup(self) -> None: """清理沙盒环境""" if self.workspace_path.exists(): shutil.rmtree(self.workspace_path) def list_created_files(self) -> list[str]: """列出沙盒中创建的文件""" if not self.workspace_path.exists(): return [] files = [] for root, dirs, filenames in os.walk(self.workspace_path): for filename in filenames: full_path = os.path.join(root, filename) rel_path = os.path.relpath(full_path, self.workspace_path) files.append(rel_path) return files @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 实时输出 Args: prompt: 任务描述 session_id: 会话 ID(可选,用于复用沙盒) Yields: str: 实时输出 """ # 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), env={**os.environ, "TERM": "xterm-256color"}, ) # 4. 实时读取输出 stdout_lines = [] stderr_lines = [] while True: try: line_bytes = await asyncio.wait_for( process.stdout.readline(), timeout=self.timeout, ) if not line_bytes: break line = line_bytes.decode("utf-8", errors="replace") stdout_lines.append(line) yield line except asyncio.TimeoutError: process.kill() yield f"\n[ERROR] Execution timed out after {self.timeout}s\n" break # 5. 读取 stderr stderr_bytes = await process.communicate() if stderr_bytes[1]: stderr = stderr_bytes[1].decode("utf-8", errors="replace") stderr_lines.append(stderr) yield f"\n[STDERR]\n{stderr}\n" # 6. 返回结果(通过 yield 完成标记) result = ExecutionResult( success=process.returncode == 0, exit_code=process.returncode or 0, stdout="".join(stdout_lines), stderr="".join(stderr_lines), files_created=env.list_created_files(), ) yield f"\n[EXIT_CODE] {result.exit_code}\n" yield f"\n[COMPLETE] success={result.success}\n" async def get_result( self, prompt: str, session_id: str | None = None, ) -> ExecutionResult: """同步执行并返回完整结果""" output_parts = [] async for line in self.execute(prompt, session_id): output_parts.append(line) # 解析最后的结果 # 实际使用中可能需要更复杂的结果收集 return ExecutionResult( success="[COMPLETE] success=True" in "".join(output_parts), exit_code=0, stdout="".join(output_parts), stderr="", files_created=[], ) async def cleanup_session(self, session_id: str) -> bool: """清理指定会话""" if session_id in self._sessions: await self._sessions[session_id].cleanup() del self._sessions[session_id] return True return False def get_session(self, session_id: str) -> SandboxEnvironment | None: """获取会话环境""" return self._sessions.get(session_id)