feat: 增强编码能力 - 新增代码编辑器、代码搜索、Git集成
- 新增 plugins/code_editor.py: 精准代码编辑(搜索替换、批量编辑、diff对比) - 新增 plugins/code_search.py: 代码搜索与理解(跨文件搜索、符号提取、依赖分析) - 新增 plugins/git_tools.py: Git 版本控制(status/diff/log/commit/branch) - 新增 core/ 模块: 配置管理、引擎管理、对话处理、显示输出、代码沙箱 - 修复 requirements.txt: 使用正确的 pip 包名(Pillow、opencv-python 等) - 更新 unified_tool_manager.py: 兼容 Plugin 和 Liugin 两种类名 - 更新系统提示词: 增强编程工作流指导 本次更新由 mimo 自动化执行,与开发者无关。
This commit is contained in:
commit
e22652fbbb
53 files changed
+20067
No files matched your search
@@ -0,0 +1,42 @@
|
||||
"""核心配置管理模块"""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 常量
|
||||
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json")
|
||||
MAX_FILE_SIZE = 52428800 # 50MB
|
||||
DEFAULT_MAX_HISTORY = 999999
|
||||
|
||||
def load_config():
|
||||
"""加载配置文件"""
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
return {"api": {"engines": {}}, "system": {}, "plugins": {}}
|
||||
except json.JSONDecodeError:
|
||||
return {"api": {"engines": {}}, "system": {}, "plugins": {}}
|
||||
|
||||
def save_config(config):
|
||||
"""保存配置文件"""
|
||||
try:
|
||||
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
logger.error(f"保存配置失败: {e}")
|
||||
|
||||
def get_system_config(key, default=None):
|
||||
"""获取系统配置"""
|
||||
config = load_config()
|
||||
return config.get("system", {}).get(key, default)
|
||||
|
||||
def set_system_config(key, value):
|
||||
"""设置系统配置"""
|
||||
config = load_config()
|
||||
if "system" not in config:
|
||||
config["system"] = {}
|
||||
config["system"][key] = value
|
||||
save_config(config)
|
||||
@@ -0,0 +1,315 @@
|
||||
"""
|
||||
对话处理模块 - 核心对话循环和工具调用
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import threading
|
||||
import logging
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from colorama import Fore, Style
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConversationManager:
|
||||
"""对话管理器 - 处理用户输入、AI 响应、工具调用循环"""
|
||||
|
||||
def __init__(self, cli):
|
||||
self.cli = cli
|
||||
self.history = []
|
||||
self.max_history = 999999
|
||||
|
||||
def process(self, user_input: str):
|
||||
"""处理完整的对话循环"""
|
||||
self.history.append({"role": "user", "content": user_input})
|
||||
|
||||
tool_prompts = self._get_tool_prompts()
|
||||
loop_count = 0
|
||||
current_input = user_input
|
||||
max_loops = 20
|
||||
|
||||
while loop_count < max_loops:
|
||||
response = self._generate_with_animation(current_input, tool_prompts)
|
||||
processed = self._process_thinking(response)
|
||||
processed = self._process_code_blocks(processed)
|
||||
|
||||
try:
|
||||
text_content, json_data = self._parse_response(processed)
|
||||
|
||||
if json_data:
|
||||
if text_content:
|
||||
self._add_and_display(text_content)
|
||||
|
||||
if isinstance(json_data, dict):
|
||||
result = self._handle_dict(json_data, text_content, processed)
|
||||
if result == "break":
|
||||
break
|
||||
elif result == "continue":
|
||||
loop_count += 1
|
||||
continue
|
||||
elif isinstance(json_data, list):
|
||||
result = self._handle_list(json_data, text_content, processed)
|
||||
if result == "break":
|
||||
break
|
||||
elif result == "continue":
|
||||
loop_count += 1
|
||||
continue
|
||||
else:
|
||||
display = text_content if text_content else processed
|
||||
self._add_and_display(display)
|
||||
break
|
||||
|
||||
except json.JSONDecodeError:
|
||||
text_content, json_data = self._parse_response(processed)
|
||||
if json_data:
|
||||
# Re-process with parsed data
|
||||
pass
|
||||
else:
|
||||
self._add_and_display(processed)
|
||||
break
|
||||
|
||||
if loop_count >= max_loops:
|
||||
self.cli.display.output(f"AI 已达到最大循环次数 ({max_loops}),已停止")
|
||||
|
||||
def _handle_dict(self, data, text_content, processed):
|
||||
"""处理 dict 类型的 JSON 数据"""
|
||||
if data.get('action') == 'use_tool':
|
||||
self.cli._execute_single_tool(data)
|
||||
return "break"
|
||||
|
||||
if data.get('action') == 'continue':
|
||||
content = data.get('content', '')
|
||||
if content:
|
||||
self._add_and_display(content)
|
||||
return "continue"
|
||||
|
||||
if data.get('continue') or data.get('need_continue') or data.get('think_more'):
|
||||
msg = data.get('message', '')
|
||||
if msg:
|
||||
self.history.append({"role": "assistant", "content": msg})
|
||||
self.cli.display.response(msg, is_continue=True)
|
||||
return "continue"
|
||||
|
||||
msg = data.get('message', '')
|
||||
if msg:
|
||||
self._add_and_display(msg)
|
||||
return "break"
|
||||
|
||||
if text_content:
|
||||
self._add_and_display(text_content)
|
||||
else:
|
||||
self._add_and_display(processed)
|
||||
return "break"
|
||||
|
||||
def _handle_list(self, data_list, text_content, processed):
|
||||
"""处理 list 类型的 JSON 数据"""
|
||||
tool_calls = [item for item in data_list
|
||||
if isinstance(item, dict) and item.get('action') == 'use_tool']
|
||||
|
||||
if tool_calls:
|
||||
self.cli._execute_concurrent_tools(tool_calls)
|
||||
return "break"
|
||||
|
||||
continue_items = [item for item in data_list
|
||||
if isinstance(item, dict) and
|
||||
(item.get('continue') or item.get('need_continue') or item.get('think_more'))]
|
||||
|
||||
if continue_items:
|
||||
msg = continue_items[0].get('message', '')
|
||||
if msg:
|
||||
self.history.append({"role": "assistant", "content": msg})
|
||||
self.cli.display.response(msg, is_continue=True)
|
||||
return "continue"
|
||||
|
||||
if text_content:
|
||||
self._add_and_display(text_content)
|
||||
else:
|
||||
self._add_and_display(processed)
|
||||
return "break"
|
||||
|
||||
def _add_and_display(self, content):
|
||||
"""添加到历史并显示"""
|
||||
self.history.append({"role": "assistant", "content": content})
|
||||
self.cli.display.response(content)
|
||||
|
||||
def _generate_with_animation(self, user_input, tool_prompts=None):
|
||||
"""生成 AI 响应(带动画)"""
|
||||
system_prompt = self._build_system_prompt(tool_prompts)
|
||||
|
||||
if not self.cli.engine_manager.current_engine:
|
||||
return "错误: 当前没有可用的AI引擎"
|
||||
|
||||
# TUI 模式:直接生成
|
||||
if self.cli.display.tui_callback:
|
||||
response = self.cli.engine_manager.generate_response(user_input, system_prompt)
|
||||
if response:
|
||||
self.history.append({"role": "assistant", "content": response})
|
||||
return response
|
||||
|
||||
# CLI 模式:带动画
|
||||
stop_event = threading.Event()
|
||||
esc_event = threading.Event()
|
||||
|
||||
# 加载等待句子
|
||||
sentences = self._load_love_sentences()
|
||||
|
||||
anim_thread = threading.Thread(
|
||||
target=self.cli.display.thinking_animation,
|
||||
args=(sentences, stop_event, esc_event), daemon=True)
|
||||
anim_thread.start()
|
||||
|
||||
try:
|
||||
response = self.cli.engine_manager.generate_response(user_input, system_prompt)
|
||||
if response:
|
||||
self.cli.display.typeprint(response, Fore.CYAN)
|
||||
self.history.append({"role": "assistant", "content": response})
|
||||
return response
|
||||
finally:
|
||||
stop_event.set()
|
||||
anim_thread.join()
|
||||
|
||||
def _build_system_prompt(self, tool_prompts=None):
|
||||
"""构建系统提示词"""
|
||||
base = ("你叫小狸,是一个强大的智能编程助手。你具备精准的代码编辑能力、"
|
||||
"完整的 Git 工作流支持、以及跨文件代码搜索和理解能力。"
|
||||
"你可以帮助用户编写、修改、调试代码,管理版本控制,理解复杂代码库。")
|
||||
|
||||
tool_section = ""
|
||||
if tool_prompts:
|
||||
tool_section = f"\n\n{tool_prompts}"
|
||||
|
||||
call_format = """
|
||||
【工具调用格式】
|
||||
当需要执行操作时,在回复中包含 JSON 指令:
|
||||
|
||||
1. 纯 JSON: {"action": "use_tool", "tool": "工具名", "args": "参数"}
|
||||
2. 文本+JSON: 好的,我来修改这个文件。{"action": "use_tool", "tool": "code_editor", "args": "edit file.py old_code <<<>>> new_code"}
|
||||
|
||||
【核心工具】
|
||||
- code_editor: 精准代码编辑(edit/search/diff/insert/create/write)
|
||||
- code_search: 代码搜索与理解(find/regex/symbols/imports/structure)
|
||||
- git_tools: Git 操作(status/diff/log/add/commit/branch)
|
||||
- cmd_executor: 执行 shell 命令
|
||||
- file_manager: 文件管理
|
||||
|
||||
【编码工作流】
|
||||
1. 先用 code_search 了解代码结构
|
||||
2. 用 code_editor.read_range 查看相关代码
|
||||
3. 用 code_editor.edit 精准修改
|
||||
4. 用 git_tools 提交变更
|
||||
|
||||
【注意事项】
|
||||
- code_editor.edit 的格式: old_text <<<>>> new_text(用 <<<>>> 分隔)
|
||||
- args 必须是字符串
|
||||
- 可以一次返回多个 JSON 指令顺序执行
|
||||
- 使用 continue 补充回答: {"action": "continue", "content": "补充内容"}"""
|
||||
|
||||
return base + tool_section + call_format
|
||||
|
||||
def _process_thinking(self, response):
|
||||
"""处理深度思考标记"""
|
||||
# <think/> 格式
|
||||
if '<think/>' in response:
|
||||
parts = response.split('<think/>', 1)
|
||||
if len(parts) >= 2:
|
||||
thinking = parts[0].strip()
|
||||
reply = parts[1].strip()
|
||||
if thinking:
|
||||
print(f"{Fore.LIGHTBLACK_EX}[思考: {thinking[:200]}...]{Style.RESET_ALL}" if len(thinking) > 200 else f"{Fore.LIGHTBLACK_EX}[思考: {thinking}]{Style.RESET_ALL}")
|
||||
return reply if reply else ""
|
||||
|
||||
# <thinking>...</thinking> 格式
|
||||
engine = self.cli.engine_manager.current_engine
|
||||
if engine:
|
||||
start = getattr(engine, 'thinking_start_marker', None)
|
||||
end = getattr(engine, 'thinking_end_marker', None)
|
||||
if start and end and start in response and end in response:
|
||||
parts = response.split(start, 1)
|
||||
if len(parts) > 1 and end in parts[1]:
|
||||
thinking, reply = parts[1].split(end, 1)
|
||||
if thinking.strip():
|
||||
print(f"{Fore.LIGHTBLACK_EX}[思考: {thinking.strip()[:200]}]{Style.RESET_ALL}")
|
||||
return reply.strip()
|
||||
|
||||
return response
|
||||
|
||||
def _process_code_blocks(self, text):
|
||||
"""处理代码块标记"""
|
||||
pattern = r'(""".*?""")'
|
||||
def replace(m):
|
||||
inner = m.group(1)[3:-3]
|
||||
return f'{Fore.RED}{inner}{Style.RESET_ALL}'
|
||||
return re.sub(pattern, replace, text, flags=re.DOTALL)
|
||||
|
||||
def _parse_response(self, response):
|
||||
"""解析混合响应(文本+JSON)"""
|
||||
response = response.strip()
|
||||
|
||||
# 直接 JSON
|
||||
try:
|
||||
parsed = json.loads(response)
|
||||
if isinstance(parsed, (dict, list)):
|
||||
return "", parsed
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# 代码块中的 JSON
|
||||
pattern = r"```(?:json)?\s*({.*?})\s*```"
|
||||
matches = re.findall(pattern, response, re.DOTALL)
|
||||
if matches:
|
||||
try:
|
||||
data = json.loads(matches[-1])
|
||||
if isinstance(data, (dict, list)):
|
||||
text = re.sub(pattern, "", response, flags=re.DOTALL).strip()
|
||||
return text, data
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
# 内联 JSON
|
||||
markers = ['{"action": "use_tool"', '{"action": "continue"',
|
||||
'{"action":"use_tool"', '{"action":"continue"']
|
||||
for marker in markers:
|
||||
idx = response.find(marker)
|
||||
if idx != -1:
|
||||
brace_count = 0
|
||||
for i in range(idx, len(response)):
|
||||
if response[i] == '{':
|
||||
brace_count += 1
|
||||
elif response[i] == '}':
|
||||
brace_count -= 1
|
||||
if brace_count == 0:
|
||||
try:
|
||||
data = json.loads(response[idx:i+1])
|
||||
if isinstance(data, dict) and data.get('action') in ['use_tool', 'continue']:
|
||||
return response[:idx].strip(), data
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
break
|
||||
|
||||
return response, None
|
||||
|
||||
def _get_tool_prompts(self):
|
||||
"""获取工具提示词"""
|
||||
if not self.cli.tool_manager or not self.cli.tool_manager.tools:
|
||||
return "当前没有可用的工具"
|
||||
|
||||
lines = ["可用工具列表:\n"]
|
||||
for tool in self.cli.tool_manager.tools:
|
||||
name = tool.get('name', '')
|
||||
desc = tool.get('description', '')
|
||||
lines.append(f"- {name}: {desc}")
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
def _load_love_sentences(self):
|
||||
"""加载等待动画句子"""
|
||||
import os
|
||||
path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "love.txt")
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return [l.strip() for l in f.readlines() if l.strip()]
|
||||
except:
|
||||
pass
|
||||
return ["AI正在思考中...", "请稍等片刻...", "正在处理您的请求..."]
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
显示与输出模块 - 统一 CLI/TUI 输出
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import random
|
||||
import shutil
|
||||
import threading
|
||||
from colorama import Fore, Style
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Display:
|
||||
"""统一显示管理器"""
|
||||
|
||||
def __init__(self, user_id="", tui_callback=None):
|
||||
self.user_id = user_id
|
||||
self.tui_callback = tui_callback
|
||||
|
||||
def output(self, message):
|
||||
"""统一输出方法"""
|
||||
if self.tui_callback:
|
||||
self.tui_callback(message)
|
||||
else:
|
||||
print(message)
|
||||
|
||||
def response(self, content, is_continue=False):
|
||||
"""显示 AI 响应"""
|
||||
user_id_display = f"[用户ID: {self.user_id}]"
|
||||
lines = content.split('\n')
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if i == 0:
|
||||
prefix = f"{Fore.YELLOW}✦{Style.RESET_ALL} " if is_continue else "✦ "
|
||||
line = prefix + line
|
||||
if i == len(lines) - 1:
|
||||
print(f"{line} {user_id_display}")
|
||||
else:
|
||||
print(line)
|
||||
print()
|
||||
|
||||
def tool_result(self, tool_name, tool_args, full_result, is_error=False):
|
||||
"""显示工具执行结果"""
|
||||
if full_result is None:
|
||||
full_result = "无结果"
|
||||
|
||||
self.output("")
|
||||
user_id_display = f"[用户ID: {self.user_id}]"
|
||||
|
||||
if is_error:
|
||||
self.output(f"{Fore.RED} X 工具: {tool_name}{Style.RESET_ALL}")
|
||||
self.output(f"{Fore.RED} 参数: {tool_args} {user_id_display}{Style.RESET_ALL}")
|
||||
else:
|
||||
self.output(f"{Fore.GREEN} OK 工具: {tool_name}{Style.RESET_ALL}")
|
||||
self.output(f"{Fore.GREEN} 参数: {tool_args} {user_id_display}{Style.RESET_ALL}")
|
||||
|
||||
display_result = self._limit_lines(full_result, max_lines=2)
|
||||
color = Fore.RED if is_error else Fore.GREEN
|
||||
for line in display_result.split('\n'):
|
||||
self.output(f"{color} {line}{Style.RESET_ALL}")
|
||||
|
||||
def error(self, message):
|
||||
"""显示错误"""
|
||||
self.output(f"{Fore.RED}{message}{Style.RESET_ALL}")
|
||||
|
||||
def success(self, message):
|
||||
"""显示成功"""
|
||||
self.output(f"{Fore.GREEN}{message}{Style.RESET_ALL}")
|
||||
|
||||
def info(self, message):
|
||||
"""显示信息"""
|
||||
self.output(f"{Fore.CYAN}{message}{Style.RESET_ALL}")
|
||||
|
||||
def warning(self, message):
|
||||
"""显示警告"""
|
||||
self.output(f"{Fore.YELLOW}{message}{Style.RESET_ALL}")
|
||||
|
||||
def typeprint(self, text, color=None, delay=0.01):
|
||||
"""打字机效果输出"""
|
||||
for char in text:
|
||||
if color:
|
||||
print(f"{color}{char}{Style.RESET_ALL}", end="", flush=True)
|
||||
else:
|
||||
print(char, end="", flush=True)
|
||||
time.sleep(delay)
|
||||
print()
|
||||
|
||||
def thinking_animation(self, sentences, stop_event, esc_event):
|
||||
"""等待动画"""
|
||||
if not sentences:
|
||||
sentences = ["AI正在思考中...", "请稍等片刻...", "正在处理您的请求..."]
|
||||
|
||||
last_change = time.time()
|
||||
current_sentence = random.choice(sentences)
|
||||
chars = "|/-\\"
|
||||
idx = 0
|
||||
|
||||
while not stop_event.is_set():
|
||||
now = time.time()
|
||||
if now - last_change >= 5:
|
||||
current_sentence = random.choice(sentences)
|
||||
last_change = now
|
||||
|
||||
char = chars[idx % len(chars)]
|
||||
display = current_sentence[:47] + "..." if len(current_sentence) > 50 else current_sentence
|
||||
print(f"\r{Fore.MAGENTA}{display} {char}{Style.RESET_ALL}", end="", flush=True)
|
||||
idx += 1
|
||||
time.sleep(0.1)
|
||||
|
||||
print("\r" + " " * 60 + "\r", end="", flush=True)
|
||||
|
||||
def _limit_lines(self, text, max_lines=2):
|
||||
"""限制输出行数"""
|
||||
lines = text.split('\n')
|
||||
if len(lines) <= max_lines:
|
||||
return text
|
||||
return '\n'.join(lines[:max_lines]) + f"\n... (已截断,共{len(lines)}行)"
|
||||
|
||||
def show_banner(self, engine_name, engines, user_id):
|
||||
"""显示启动横幅"""
|
||||
self.success(f"小狸 Pro-CLI 已启动!")
|
||||
self.success(f"用户ID: {user_id}")
|
||||
print(f"{Fore.GREEN}输入 '/help' 查看帮助信息{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}输入 '/quit' 退出程序{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}当前引擎: {engine_name}{Style.RESET_ALL}")
|
||||
if len(engines) > 1:
|
||||
print(f"{Fore.GREEN}已加载引擎: {', '.join(engines)}{Style.RESET_ALL}")
|
||||
print(f"{Fore.GREEN}{'-' * 50}{Style.RESET_ALL}")
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
AI 引擎管理模块 - 动态加载和切换 AI 引擎
|
||||
"""
|
||||
import os
|
||||
import importlib.util
|
||||
import logging
|
||||
from colorama import Fore, Style
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EngineManager:
|
||||
"""AI 引擎管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.engines = {}
|
||||
self.current_engine = None
|
||||
|
||||
def load_engines(self, engines_dir: str) -> dict:
|
||||
"""动态加载所有 AI 引擎"""
|
||||
print(f"{Fore.GREEN}正在加载AI引擎插件...{Style.RESET_ALL}")
|
||||
|
||||
if not os.path.exists(engines_dir):
|
||||
print(f"{Fore.YELLOW}引擎目录不存在: {engines_dir}{Style.RESET_ALL}")
|
||||
return self.engines
|
||||
|
||||
for filename in os.listdir(engines_dir):
|
||||
if not filename.endswith('.py') or filename == '__init__.py':
|
||||
continue
|
||||
|
||||
engine_name = filename[:-3]
|
||||
engine_path = os.path.join(engines_dir, filename)
|
||||
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(engine_name, engine_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
class_name = self._find_engine_class(module, engine_name)
|
||||
if class_name and hasattr(module, class_name):
|
||||
engine_instance = getattr(module, class_name)()
|
||||
engine_key = getattr(engine_instance, 'name', engine_name)
|
||||
|
||||
if engine_key not in self.engines:
|
||||
self.engines[engine_key] = engine_instance
|
||||
print(f"{Fore.GREEN}已加载AI引擎: {engine_key} ({class_name}){Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"{Fore.YELLOW}AI引擎 {engine_key} 已存在,跳过{Style.RESET_ALL}")
|
||||
else:
|
||||
print(f"{Fore.YELLOW}引擎插件 {filename} 中未找到合适的引擎类{Style.RESET_ALL}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载引擎失败 {filename}: {e}")
|
||||
print(f"{Fore.RED}加载引擎失败 {filename}: {e}{Style.RESET_ALL}")
|
||||
|
||||
print(f"{Fore.GREEN}总共加载了 {len(self.engines)} 个AI引擎{Style.RESET_ALL}")
|
||||
return self.engines
|
||||
|
||||
def _find_engine_class(self, module, engine_name):
|
||||
"""查找引擎类"""
|
||||
possible_names = [
|
||||
engine_name.capitalize() + "AI",
|
||||
engine_name.replace('_', '').replace('-', '').capitalize() + "AI",
|
||||
''.join(w.capitalize() for w in engine_name.replace('-', ' ').replace('_', ' ').split()) + "AI",
|
||||
''.join(w.capitalize() for w in engine_name.replace('_engine', '').replace('-', ' ').replace('_', ' ').split()) + "AI",
|
||||
]
|
||||
|
||||
for name in possible_names:
|
||||
if hasattr(module, name):
|
||||
return name
|
||||
|
||||
# 通用查找
|
||||
for attr_name in dir(module):
|
||||
attr = getattr(module, attr_name)
|
||||
if (isinstance(attr, type) and
|
||||
attr_name.endswith('AI') and
|
||||
hasattr(attr, 'generate_response')):
|
||||
return attr_name
|
||||
return None
|
||||
|
||||
def set_default(self, name: str) -> bool:
|
||||
"""设置默认引擎"""
|
||||
if name in self.engines:
|
||||
self.current_engine = self.engines[name]
|
||||
return True
|
||||
return False
|
||||
|
||||
def switch(self, name: str) -> bool:
|
||||
"""切换引擎"""
|
||||
if name not in self.engines:
|
||||
return False
|
||||
|
||||
old_engine = self.current_engine
|
||||
self.current_engine = self.engines[name]
|
||||
|
||||
if hasattr(self.current_engine, 'cli'):
|
||||
self.current_engine.cli = getattr(old_engine, 'cli', None)
|
||||
|
||||
return True
|
||||
|
||||
def get_current_name(self) -> str:
|
||||
"""获取当前引擎名称"""
|
||||
if self.current_engine:
|
||||
return getattr(self.current_engine, 'name', '未设置')
|
||||
return '未设置'
|
||||
|
||||
def list_engines(self) -> list:
|
||||
"""列出所有引擎"""
|
||||
return list(self.engines.keys())
|
||||
|
||||
def generate_response(self, user_input: str, system_prompt: str = None) -> str:
|
||||
"""使用当前引擎生成响应"""
|
||||
if not self.current_engine:
|
||||
return "错误: 没有可用的AI引擎"
|
||||
return self.current_engine.generate_response(user_input, system_prompt=system_prompt)
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
安全代码沙箱 - 在隔离环境中执行用户/插件代码
|
||||
"""
|
||||
import io
|
||||
import ast
|
||||
import time
|
||||
import re
|
||||
import threading
|
||||
import traceback
|
||||
from contextlib import redirect_stdout, redirect_stderr
|
||||
from datetime import datetime
|
||||
from colorama import Fore, Style
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CodeSandbox:
|
||||
"""安全代码执行沙箱"""
|
||||
|
||||
# 危险模块黑名单
|
||||
DANGEROUS_MODULES = {
|
||||
'os', 'sys', 'subprocess', 'shutil', 'socket', 'pickle',
|
||||
'marshal', 'ctypes', 'multiprocessing', 'threading',
|
||||
'signal', 'resource', 'posix', 'nt', 'builtins',
|
||||
'importlib', 'code', 'codeop', 'commands', 'popen2',
|
||||
}
|
||||
|
||||
# 危险函数/属性黑名单
|
||||
DANGEROUS_FUNCTIONS = {
|
||||
'exec', 'eval', 'compile', 'execfile', 'open',
|
||||
'input', 'raw_input', 'globals', 'locals', 'vars',
|
||||
'dir', 'getattr', 'setattr', 'delattr', 'hasattr',
|
||||
'__import__', '__builtins__', '__class__', '__bases__',
|
||||
'__subclasses__', '__mro__', '__dict__', '__getattribute__',
|
||||
}
|
||||
|
||||
def __init__(self, timeout=30, whitelist=None):
|
||||
self.enabled = True
|
||||
self.timeout = timeout
|
||||
self.whitelist = whitelist or [
|
||||
'json', 'math', 'datetime', 'random', 'collections',
|
||||
'itertools', 'functools', 'operator', 'typing', 're'
|
||||
]
|
||||
self.history = []
|
||||
self.max_history = 100
|
||||
|
||||
def is_safe(self, code: str) -> bool:
|
||||
"""检查代码是否安全(AST 分析 + 字符串快速检查)"""
|
||||
# 快速字符串检查
|
||||
dangerous_patterns = [
|
||||
r'__', r'\beval\b', r'\bexec\b', r'\bcompile\b',
|
||||
r'\bglobals\b', r'\blocals\b', r'\bgetattr\b',
|
||||
r'\bsetattr\b', r'\b__import__\b',
|
||||
]
|
||||
for pattern in dangerous_patterns:
|
||||
if re.search(pattern, code, re.IGNORECASE):
|
||||
return False
|
||||
|
||||
# AST 分析
|
||||
try:
|
||||
tree = ast.parse(code)
|
||||
except SyntaxError:
|
||||
return False
|
||||
|
||||
class SafetyVisitor(ast.NodeVisitor):
|
||||
def __init__(self):
|
||||
self.is_safe = True
|
||||
|
||||
def _check_name(self, name):
|
||||
if name in CodeSandbox.DANGEROUS_FUNCTIONS:
|
||||
self.is_safe = False
|
||||
|
||||
def visit_Import(self, node):
|
||||
for alias in node.names:
|
||||
if alias.name.split('.')[0] in CodeSandbox.DANGEROUS_MODULES:
|
||||
self.is_safe = False
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_ImportFrom(self, node):
|
||||
if node.module:
|
||||
if node.module.split('.')[0] in CodeSandbox.DANGEROUS_MODULES:
|
||||
self.is_safe = False
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Call(self, node):
|
||||
if isinstance(node.func, ast.Name):
|
||||
self._check_name(node.func.id)
|
||||
elif isinstance(node.func, ast.Attribute):
|
||||
self._check_name(node.func.attr)
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Attribute(self, node):
|
||||
self._check_name(node.attr)
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Name(self, node):
|
||||
self._check_name(node.id)
|
||||
self.generic_visit(node)
|
||||
|
||||
visitor = SafetyVisitor()
|
||||
visitor.visit(tree)
|
||||
return visitor.is_safe
|
||||
|
||||
def execute(self, code: str, variables: dict = None) -> dict:
|
||||
"""
|
||||
在安全沙箱中执行代码
|
||||
|
||||
Returns:
|
||||
{
|
||||
'success': bool,
|
||||
'result': str,
|
||||
'stdout': str,
|
||||
'stderr': str,
|
||||
'execution_time': float,
|
||||
}
|
||||
"""
|
||||
if not self.enabled:
|
||||
return {'success': False, 'result': '代码执行功能已禁用',
|
||||
'stdout': '', 'stderr': '', 'execution_time': 0}
|
||||
|
||||
if not self.is_safe(code):
|
||||
return {'success': False, 'result': '代码包含潜在危险操作,已阻止执行',
|
||||
'stdout': '', 'stderr': '', 'execution_time': 0}
|
||||
|
||||
# 准备变量
|
||||
exec_vars = {}
|
||||
if variables:
|
||||
exec_vars.update(variables)
|
||||
|
||||
# 添加安全的内置函数
|
||||
safe_builtins = {
|
||||
'print': print, 'len': len, 'str': str, 'int': int,
|
||||
'float': float, 'list': list, 'dict': dict, 'tuple': tuple,
|
||||
'set': set, 'range': range, 'enumerate': enumerate,
|
||||
'zip': zip, 'sorted': sorted, 'sum': sum, 'max': max,
|
||||
'min': min, 'abs': abs, 'round': round, 'bool': bool,
|
||||
'type': type, 'isinstance': isinstance,
|
||||
}
|
||||
exec_vars.update(safe_builtins)
|
||||
|
||||
# 添加白名单模块
|
||||
for module_name in self.whitelist:
|
||||
try:
|
||||
exec_vars[module_name] = __import__(module_name)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# 捕获输出
|
||||
stdout_capture = io.StringIO()
|
||||
stderr_capture = io.StringIO()
|
||||
|
||||
start_time = time.time()
|
||||
result = {'success': True, 'error': None, 'traceback': None}
|
||||
|
||||
def _exec():
|
||||
try:
|
||||
exec(code, exec_vars)
|
||||
except Exception as e:
|
||||
result['success'] = False
|
||||
result['error'] = str(e)
|
||||
result['traceback'] = traceback.format_exc()
|
||||
|
||||
exec_thread = threading.Thread(target=_exec, daemon=True)
|
||||
exec_thread.start()
|
||||
exec_thread.join(timeout=self.timeout)
|
||||
|
||||
exec_time = time.time() - start_time
|
||||
|
||||
if exec_thread.is_alive():
|
||||
return {
|
||||
'success': False,
|
||||
'result': f'代码执行超时(超过 {self.timeout} 秒)',
|
||||
'stdout': stdout_capture.getvalue(),
|
||||
'stderr': stderr_capture.getvalue(),
|
||||
'execution_time': self.timeout,
|
||||
}
|
||||
|
||||
if result['success']:
|
||||
exec_result = exec_vars.get('result', None)
|
||||
result_str = str(exec_result) if exec_result is not None else '代码执行成功(无返回值)'
|
||||
return {
|
||||
'success': True,
|
||||
'result': result_str,
|
||||
'stdout': stdout_capture.getvalue(),
|
||||
'stderr': stderr_capture.getvalue(),
|
||||
'execution_time': exec_time,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'success': False,
|
||||
'result': f"代码执行错误: {result['error']}",
|
||||
'stdout': stdout_capture.getvalue(),
|
||||
'stderr': stderr_capture.getvalue() + '\n' + (result['traceback'] or ''),
|
||||
'execution_time': exec_time,
|
||||
}
|
||||
|
||||
def add_to_history(self, record: dict):
|
||||
"""添加执行记录"""
|
||||
self.history.append(record)
|
||||
if len(self.history) > self.max_history:
|
||||
self.history.pop(0)
|
||||
|
||||
def get_history(self, limit=10):
|
||||
"""获取执行历史"""
|
||||
return self.history[-limit:]
|
||||
|
||||
def clear_history(self):
|
||||
"""清空执行历史"""
|
||||
self.history = []
|
||||
Reference in new issue
Block a user