174 lines
5.2 KiB
Python
174 lines
5.2 KiB
Python
|
|
"""
|
|||
|
|
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)
|