Files
xiaoli-cli/xcli_core/cli_code_exec.py
T
xiaoli-cli 04e2d26809 refactor: 拆分 ai_cli.py 为模块化 xcli_core 包
将 4130 行的单文件拆分为 13 个职责清晰的模块:
- constants.py: 常量 + 可选依赖导入
- sandbox.py: 安全代码沙箱 (AST检查器 + 子进程隔离)
- config.py: 配置管理 + 日志
- plugin_manager.py: LiuginManager 插件管理器
- cli_base.py: BaseAICLI 基类 (初始化/引擎/插件/命令)
- cli_clawli.py: ClawliMixin 远程模式
- cli_tools.py: ToolMixin 工具调用
- cli_code_exec.py: CodeExecMixin 代码沙箱执行
- cli_display.py: DisplayMixin 显示/输出/图片/帮助
- cli_history.py: HistoryMixin 聊天记录
- cli_core.py: AICLI 主类 + 对话循环 + main()
- tui.py: Textual TUI 界面
- __init__.py: 包导出汇总

ai_cli.py 保留为入口文件 (11行), 根目录整洁。
原逻辑完全不变, 仅做结构拆分。
2026-04-25 22:27:59 +08:00

207 lines
7.4 KiB
Python

import os
import sys
import time
import json
import subprocess
import traceback
from datetime import datetime
from colorama import Fore, Style
from .config import logger
from .sandbox import (
_sandbox_checker, _make_sandbox_worker, _sandbox_work_dir,
_HAS_RESOURCE, _SAFE_WHITELIST,
)
try:
import resource as _resource_mod
except ImportError:
_resource_mod = None
class CodeExecMixin:
"""代码沙箱执行相关方法"""
def request_code_execution(self, liugin_name, code, timeout=None, variables=None, context=None):
"""
插件代码执行请求接口
"""
if not self.code_execution_enabled:
return {
'success': False,
'result': '代码执行功能已禁用',
'stdout': '',
'stderr': '',
'execution_time': 0,
'memory_usage': 0
}
if not self._is_code_safe(code):
return {
'success': False,
'result': '代码包含潜在危险操作,已阻止执行',
'stdout': '',
'stderr': '',
'execution_time': 0,
'memory_usage': 0
}
exec_timeout = timeout if timeout is not None else self.code_execution_timeout
execution_id = f"{liugin_name}_{int(time.time())}"
execution_record = {
'id': execution_id,
'liugin_name': liugin_name,
'code': code,
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
'context': context
}
try:
result = self._execute_code_safely(code, exec_timeout, variables)
execution_record.update({
'success': result['success'],
'execution_time': result['execution_time'],
'memory_usage': result['memory_usage']
})
self.code_execution_history.append(execution_record)
if len(self.code_execution_history) > 100:
self.code_execution_history.pop(0)
if result['success']:
logger.info(f"代码执行成功 - 插件: {liugin_name}, 耗时: {result['execution_time']:.3f}s")
else:
logger.warning(f"代码执行失败 - 插件: {liugin_name}, 错误: {result['result']}")
return result
except Exception as e:
error_msg = f"代码执行异常: {str(e)}"
logger.error(f"{error_msg} - 插件: {liugin_name}")
return {
'success': False,
'result': error_msg,
'stdout': '',
'stderr': error_msg,
'execution_time': 0,
'memory_usage': 0
}
def _is_code_safe(self, code):
"""检查代码是否安全(使用 v2 AST+正则 沙箱检查器)"""
safe, violations = _sandbox_checker.check(code)
if not safe:
logger.warning(f"代码安全检查失败: {violations}")
return safe
def _execute_code_safely(self, code, timeout, variables=None):
"""
在安全沙箱中执行代码(v2 子进程隔离)
"""
safe, violations = _sandbox_checker.check(code)
if not safe:
return {
'success': False,
'result': '代码包含潜在危险操作,已阻止执行',
'stdout': '', 'stderr': '; '.join(violations[:5]),
'execution_time': 0, 'memory_usage': 0
}
vars_json = json.dumps(variables or {}, ensure_ascii=False, default=str)
wl = getattr(self, 'code_execution_whitelist', None) or _SAFE_WHITELIST
whitelist_json = json.dumps(wl)
script = _make_sandbox_worker(code, vars_json, whitelist_json, _sandbox_work_dir)
safe_env = {
'PATH': '/usr/bin:/bin',
'LANG': 'en_US.UTF-8', 'LC_ALL': 'en_US.UTF-8',
'HOME': _sandbox_work_dir,
'TMPDIR': _sandbox_work_dir, 'TEMP': _sandbox_work_dir, 'TMP': _sandbox_work_dir,
'PYTHONNOUSERSITE': '1', 'PYTHONDONTWRITEBYTECODE': '1',
}
for key in ('TERM', 'COLUMNS', 'LINES', 'SHELL'):
val = os.environ.get(key)
if val:
safe_env[key] = val
start_time = time.time()
try:
proc = subprocess.run(
[sys.executable, '-c', script],
capture_output=True, timeout=timeout,
cwd=_sandbox_work_dir, env=safe_env,
preexec_fn=CodeExecMixin._sandbox_set_limits if _HAS_RESOURCE else None,
)
elapsed = time.time() - start_time
stdout = proc.stdout.decode('utf-8', errors='replace')[:1000000]
stderr = proc.stderr.decode('utf-8', errors='replace')[:1000000]
try:
data = json.loads(stdout)
return {
'success': data.get('success', False),
'result': data.get('result', ''),
'stdout': data.get('stdout', ''),
'stderr': data.get('stderr', ''),
'execution_time': elapsed,
'memory_usage': 0
}
except json.JSONDecodeError:
return {
'success': proc.returncode == 0,
'result': stdout[:2000] if stdout else 'no output',
'stdout': stdout, 'stderr': stderr,
'execution_time': elapsed, 'memory_usage': 0
}
except subprocess.TimeoutExpired:
return {
'success': False,
'result': f'代码执行超时(超过{timeout}秒)',
'stdout': '', 'stderr': '',
'execution_time': timeout, 'memory_usage': 0
}
except Exception as e:
return {
'success': False,
'result': f'代码执行异常: {str(e)}',
'stdout': '', 'stderr': traceback.format_exc(),
'execution_time': time.time() - start_time, 'memory_usage': 0
}
@staticmethod
def _sandbox_set_limits():
"""子进程资源限制"""
if not _HAS_RESOURCE or not _resource_mod:
return
try:
mem = 256 * 1024 * 1024 # 256MB
_resource_mod.setrlimit(_resource_mod.RLIMIT_AS, (mem, mem))
_resource_mod.setrlimit(_resource_mod.RLIMIT_CPU, (30, 35))
_resource_mod.setrlimit(_resource_mod.RLIMIT_NPROC, (0, 0))
_resource_mod.setrlimit(_resource_mod.RLIMIT_FSIZE, (50*1024*1024, 50*1024*1024))
except (ValueError, OSError):
pass
def get_code_execution_history(self, limit=10):
"""获取代码执行历史"""
return self.code_execution_history[-limit:]
def clear_code_execution_history(self):
"""清空代码执行历史"""
self.code_execution_history = []
logger.info("代码执行历史已清空")
def set_code_execution_timeout(self, timeout):
"""设置代码执行超时时间"""
if timeout > 0:
self.code_execution_timeout = timeout
logger.info(f"代码执行超时时间已设置为 {timeout} 秒")
def set_code_execution_whitelist(self, modules):
"""设置允许导入的模块白名单"""
self.code_execution_whitelist = modules
logger.info(f"代码执行模块白名单已更新: {modules}")