refactor(aibox): 工具基类 Tool/ToolRegistry 抽离至 tools 包

延续 common 包的拆分模式,形成 common <- tools <- aibox 单向依赖,
为后续工具子类逐步迁移铺路;aibox.py 瘦身约 230 行,工具子类与注册逻辑零改动。
This commit is contained in:
qing committed 2026-08-13 02:50:20 +08:00
1 parent 91b30040c7
commit 14db64f7a2
5 files changed
+285 -233

No files matched your search

+4 -1
View File
@@ -67,6 +67,9 @@ aibox/
├── entry.html # 前端入口页 ├── entry.html # 前端入口页
├── entry1.html # 前端入口页(变体) ├── entry1.html # 前端入口页(变体)
├── frontend.html # 前端主页面 ├── frontend.html # 前端主页面
├── llm_client.py # 大模型客户端抽象层(多厂商切换)
├── common/ # 基础设施包(i18n / 配置 / 常量 / 日志)
├── tools/ # 工具系统基础包(Tool 基类 + ToolRegistry 注册器)
├── skills/ # 内置扩展技能(*.py 可执行脚本 / *.md Markdown 技能) ├── skills/ # 内置扩展技能(*.py 可执行脚本 / *.md Markdown 技能)
│ ├── hello-world.py │ ├── hello-world.py
│ ├── aes-crypto.py │ ├── aes-crypto.py
@@ -621,7 +624,7 @@ Server -> Client: {type: "dialogue_reset", content: "✅ 对话状态已完全
### 添加新工具 ### 添加新工具
1. 继承 `Tool` 基类(位于 `aibox.py`) 1. 继承 `Tool` 基类(位于 `tools/base.py`)
2. 实现 `get_name()`、`get_description()`、`get_parameters()`、`execute()` 2. 实现 `get_name()`、`get_description()`、`get_parameters()`、`execute()`
3. 可选实现 `execute_streaming()` 支持流式输出(通过 `output_queue` 发送 `("line", content)` 和 `("complete", final_result)`) 3. 可选实现 `execute_streaming()` 支持流式输出(通过 `output_queue` 发送 `("line", content)` 和 `("complete", final_result)`)
4. 在 `DeepSeekChat._register_tools()` 中注册 4. 在 `DeepSeekChat._register_tools()` 中注册
+3 -232
View File
@@ -41,6 +41,9 @@ from llm_client import create_llm_client
# 基础设施(多语言 / 配置 / 常量 / 日志)已抽离至 common 模块 # 基础设施(多语言 / 配置 / 常量 / 日志)已抽离至 common 模块
from common import i18n, I18n, ConfigManager, Constants, constants, Logger from common import i18n, I18n, ConfigManager, Constants, constants, Logger
# 工具基类与注册器已抽离至 tools 模块
from tools import Tool, ToolRegistry
# ==================== 提示词管理器 ==================== # ==================== 提示词管理器 ====================
class PromptManager: class PromptManager:
@@ -2959,238 +2962,6 @@ class SubprocessCommandExecutor(CommandExecutor):
with self._process_lock: with self._process_lock:
self._current_process = None self._current_process = None
# ==================== 工具基类 ====================
class Tool(ABC):
"""工具基类"""
def __init__(self, logger: Optional[Logger] = None):
self.logger = logger or Logger(self.__class__.__name__)
self.config = ConfigManager()
self.streaming_output = False # 标记是否正在流式输出
self.output_queue = None # 添加输出队列属性
@abstractmethod
def get_name(self) -> str:
"""返回工具名称"""
pass
@abstractmethod
def get_description(self) -> str:
"""返回工具描述"""
pass
@abstractmethod
def get_parameters(self) -> Dict:
"""返回工具参数定义(JSON Schema格式)"""
pass
@abstractmethod
def execute(self, **kwargs) -> str:
"""执行工具并返回结果"""
pass
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行工具,通过队列输出结果"""
self.output_queue = output_queue # 保存队列引用
self.streaming_output = True
try:
# 执行具体的工具逻辑,子类可以重写这个方法
result = self.execute(**kwargs)
# 如果结果不为空,发送到队列
if result:
# 对于非流式输出,直接发送完整结果
output_queue.put(("complete", result))
finally:
self.streaming_output = False
self.output_queue = None
def validate_params(self, required: List[str], **kwargs) -> Optional[str]:
"""验证必需参数"""
missing = [p for p in required if p not in kwargs or kwargs[p] is None]
if missing:
return f"❌ 错误:缺少必需参数: {', '.join(missing)}"
return None
def to_function_call_format(self) -> Dict:
"""转换为OpenAI函数调用格式"""
return {
"type": "function",
"function": {
"name": self.get_name(),
"description": self.get_description(),
"parameters": self.get_parameters()
}
}
def format_result(self, success: bool, message: str, data: Dict = None) -> str:
"""统一结果格式化"""
result = []
emoji = "✅" if success else "❌"
result.append(f"{emoji} {message}")
if data:
for key, value in data.items():
if isinstance(value, str) and "\n" in value:
result.append(f" {key}:")
for line in value.split("\n"):
result.append(f" {line}")
else:
result.append(f" {key}: {value}")
return "\n".join(result)
def check_path_safety(self, filepath: str) -> Tuple[bool, str, str]:
"""检查路径安全性(简化版)"""
try:
abs_path = os.path.abspath(os.path.expanduser(filepath))
# 检查文件大小
if os.path.exists(abs_path) and os.path.isfile(abs_path):
file_size = os.path.getsize(abs_path)
max_size = constants.MAX_FILE_SIZE
if file_size > max_size:
return False, abs_path, f"❌ 文件太大 ({file_size/1024/1024:.1f}MB)"
return True, abs_path, ""
except Exception as e:
return False, filepath, f"❌ 路径检查错误: {str(e)}"
def send_line(self, line: str):
"""发送一行输出到队列(如果存在)"""
if self.output_queue:
self.output_queue.put(("line", line))
def send_complete(self, result: str):
"""发送完成结果到队列(如果存在)"""
if self.output_queue:
self.output_queue.put(("complete", result))
class ToolRegistry:
"""工具注册器 - 支持技能热重载"""
def __init__(self, logger: Optional[Logger] = None):
self._tools: Dict[str, Tool] = {}
self._builtin_tools: Dict[str, Tool] = {} # 内置工具单独存储
self.logger = logger or Logger("ToolRegistry")
self.current_output_queue = None # 用于流式输出的队列
self.skill_manager: Optional['SkillManager'] = None # 技能管理器引用
def register(self, tool: Tool, is_builtin: bool = False):
"""注册工具,可以标记是否为内置工具"""
self._tools[tool.get_name()] = tool
if is_builtin:
self._builtin_tools[tool.get_name()] = tool
self.logger.info(f"工具已注册: {tool.get_name()} {'(内置)' if is_builtin else ''}")
def register_many(self, tools: List[Tool], is_builtin: bool = False):
"""批量注册工具"""
for tool in tools:
self.register(tool, is_builtin)
def get_tool(self, name: str) -> Optional[Tool]:
"""获取工具"""
return self._tools.get(name)
def get_all_tools(self) -> List[Tool]:
"""获取所有工具"""
return list(self._tools.values())
def get_tools_schemas(self) -> List[Dict]:
"""获取所有工具的OpenAI函数调用格式"""
return [tool.to_function_call_format() for tool in self._tools.values()]
def set_output_queue(self, queue: queue.Queue):
"""设置输出队列"""
self.current_output_queue = queue
# 同时设置所有工具的队列
for tool in self._tools.values():
tool.output_queue = queue
def execute_tool(self, name: str, **kwargs) -> str:
"""执行工具"""
tool = self.get_tool(name)
if not tool:
return f"❌ 错误:找不到工具 '{name}'"
self.logger.debug(f"工具调用: {name}({kwargs})")
start_time = time.time()
try:
# 检查是否有队列可用,并且工具支持流式输出
if self.current_output_queue and hasattr(tool, 'execute_streaming'):
# 设置工具的流式输出属性
tool.streaming_output = True
tool.output_queue = self.current_output_queue
# 创建队列来收集流式输出
output_queue = queue.Queue()
# 执行流式输出
thread = threading.Thread(
target=tool.execute_streaming,
args=(output_queue,),
kwargs=kwargs
)
thread.daemon = True
thread.start()
# 从队列收集输出并发送到当前队列
final_result = None
while thread.is_alive() or not output_queue.empty():
try:
msg_type, content = output_queue.get(timeout=0.1)
if msg_type == "line":
# 发送到当前队列(会通过WebSocket发送)
if self.current_output_queue:
self.current_output_queue.put(("line", content))
elif msg_type == "complete":
final_result = content
# 也发送完整结果
if self.current_output_queue:
self.current_output_queue.put(("complete", content))
except queue.Empty:
continue
thread.join(timeout=5)
if thread.is_alive():
return "❌ 工具执行超时"
result = final_result or ""
tool.streaming_output = False
tool.output_queue = None
else:
# 普通执行
result = tool.execute(**kwargs)
elapsed = time.time() - start_time
self.logger.debug(f"工具结果 ({elapsed:.2f}s): {result[:100]}...")
return result
except TypeError as e:
return f"❌ 工具执行错误: 参数错误 - {str(e)}"
except Exception as e:
return f"❌ 工具执行错误: {str(e)}"
def reload_skills(self, skills_dir: str = None) -> int:
"""
重新加载所有扩展技能(热重载)。
委托给 SkillManager 重新扫描目录并执行 --help 收集简介。
"""
self.logger.info("开始重新加载扩展技能...")
if self.skill_manager is not None:
count = self.skill_manager.reload()
self.logger.info(i18n.get('skills_reloaded', count))
return count
self.logger.warning("skill_manager 未初始化,无法重载技能")
return 0
def get_skills_count(self) -> Tuple[int, int]:
"""获取内置工具和扩展技能的数量"""
builtin_count = len(self._builtin_tools)
skill_count = len(self._tools) - builtin_count
return builtin_count, skill_count
# ==================== 扩展技能管理器(支持 Markdown 格式) ==================== # ==================== 扩展技能管理器(支持 Markdown 格式) ====================
+17
View File
@@ -0,0 +1,17 @@
"""
tools - aibox 工具系统基础模块(包)
抽离自 aibox.py 的工具基类与注册器,供 aibox.py 及后续迁移的
工具子类导入。当前包含:
- base.py : Tool 抽象基类
- registry.py : ToolRegistry 工具注册器
形成单向依赖:common <- tools <- aibox。
采用"重导出"策略:外部统一用 `from tools import Tool, ToolRegistry`,
调用点无需关心具体子模块所在文件。
"""
from .base import Tool
from .registry import ToolRegistry
__all__ = ["Tool", "ToolRegistry"]
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""
tools.base - 工具抽象基类
抽离自 aibox.py 的 Tool 基类。仅依赖 common 与标准库,
不反向依赖 aibox 任何符号。
"""
import os
import queue
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Tuple
from common import ConfigManager, Logger, constants
class Tool(ABC):
"""工具基类"""
def __init__(self, logger: Optional[Logger] = None):
self.logger = logger or Logger(self.__class__.__name__)
self.config = ConfigManager()
self.streaming_output = False # 标记是否正在流式输出
self.output_queue = None # 添加输出队列属性
@abstractmethod
def get_name(self) -> str:
"""返回工具名称"""
pass
@abstractmethod
def get_description(self) -> str:
"""返回工具描述"""
pass
@abstractmethod
def get_parameters(self) -> Dict:
"""返回工具参数定义(JSON Schema格式)"""
pass
@abstractmethod
def execute(self, **kwargs) -> str:
"""执行工具并返回结果"""
pass
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行工具,通过队列输出结果"""
self.output_queue = output_queue # 保存队列引用
self.streaming_output = True
try:
# 执行具体的工具逻辑,子类可以重写这个方法
result = self.execute(**kwargs)
# 如果结果不为空,发送到队列
if result:
# 对于非流式输出,直接发送完整结果
output_queue.put(("complete", result))
finally:
self.streaming_output = False
self.output_queue = None
def validate_params(self, required: List[str], **kwargs) -> Optional[str]:
"""验证必需参数"""
missing = [p for p in required if p not in kwargs or kwargs[p] is None]
if missing:
return f"❌ 错误:缺少必需参数: {', '.join(missing)}"
return None
def to_function_call_format(self) -> Dict:
"""转换为OpenAI函数调用格式"""
return {
"type": "function",
"function": {
"name": self.get_name(),
"description": self.get_description(),
"parameters": self.get_parameters()
}
}
def format_result(self, success: bool, message: str, data: Dict = None) -> str:
"""统一结果格式化"""
result = []
emoji = "✅" if success else "❌"
result.append(f"{emoji} {message}")
if data:
for key, value in data.items():
if isinstance(value, str) and "\n" in value:
result.append(f" {key}:")
for line in value.split("\n"):
result.append(f" {line}")
else:
result.append(f" {key}: {value}")
return "\n".join(result)
def check_path_safety(self, filepath: str) -> Tuple[bool, str, str]:
"""检查路径安全性(简化版)"""
try:
abs_path = os.path.abspath(os.path.expanduser(filepath))
# 检查文件大小
if os.path.exists(abs_path) and os.path.isfile(abs_path):
file_size = os.path.getsize(abs_path)
max_size = constants.MAX_FILE_SIZE
if file_size > max_size:
return False, abs_path, f"❌ 文件太大 ({file_size/1024/1024:.1f}MB)"
return True, abs_path, ""
except Exception as e:
return False, filepath, f"❌ 路径检查错误: {str(e)}"
def send_line(self, line: str):
"""发送一行输出到队列(如果存在)"""
if self.output_queue:
self.output_queue.put(("line", line))
def send_complete(self, result: str):
"""发送完成结果到队列(如果存在)"""
if self.output_queue:
self.output_queue.put(("complete", result))
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""
tools.registry - 工具注册器
抽离自 aibox.py 的 ToolRegistry 类。仅依赖 common 与标准库,
不反向依赖 aibox 任何符号(skill_manager 为前向引用,运行期注入)。
"""
import queue
import threading
import time
from typing import Dict, List, Optional, Tuple
from common import Logger, i18n
from .base import Tool
class ToolRegistry:
"""工具注册器 - 支持技能热重载"""
def __init__(self, logger: Optional[Logger] = None):
self._tools: Dict[str, Tool] = {}
self._builtin_tools: Dict[str, Tool] = {} # 内置工具单独存储
self.logger = logger or Logger("ToolRegistry")
self.current_output_queue = None # 用于流式输出的队列
self.skill_manager: Optional['SkillManager'] = None # 技能管理器引用
def register(self, tool: Tool, is_builtin: bool = False):
"""注册工具,可以标记是否为内置工具"""
self._tools[tool.get_name()] = tool
if is_builtin:
self._builtin_tools[tool.get_name()] = tool
self.logger.info(f"工具已注册: {tool.get_name()} {'(内置)' if is_builtin else ''}")
def register_many(self, tools: List[Tool], is_builtin: bool = False):
"""批量注册工具"""
for tool in tools:
self.register(tool, is_builtin)
def get_tool(self, name: str) -> Optional[Tool]:
"""获取工具"""
return self._tools.get(name)
def get_all_tools(self) -> List[Tool]:
"""获取所有工具"""
return list(self._tools.values())
def get_tools_schemas(self) -> List[Dict]:
"""获取所有工具的OpenAI函数调用格式"""
return [tool.to_function_call_format() for tool in self._tools.values()]
def set_output_queue(self, queue: queue.Queue):
"""设置输出队列"""
self.current_output_queue = queue
# 同时设置所有工具的队列
for tool in self._tools.values():
tool.output_queue = queue
def execute_tool(self, name: str, **kwargs) -> str:
"""执行工具"""
tool = self.get_tool(name)
if not tool:
return f"❌ 错误:找不到工具 '{name}'"
self.logger.debug(f"工具调用: {name}({kwargs})")
start_time = time.time()
try:
# 检查是否有队列可用,并且工具支持流式输出
if self.current_output_queue and hasattr(tool, 'execute_streaming'):
# 设置工具的流式输出属性
tool.streaming_output = True
tool.output_queue = self.current_output_queue
# 创建队列来收集流式输出
output_queue = queue.Queue()
# 执行流式输出
thread = threading.Thread(
target=tool.execute_streaming,
args=(output_queue,),
kwargs=kwargs
)
thread.daemon = True
thread.start()
# 从队列收集输出并发送到当前队列
final_result = None
while thread.is_alive() or not output_queue.empty():
try:
msg_type, content = output_queue.get(timeout=0.1)
if msg_type == "line":
# 发送到当前队列(会通过WebSocket发送)
if self.current_output_queue:
self.current_output_queue.put(("line", content))
elif msg_type == "complete":
final_result = content
# 也发送完整结果
if self.current_output_queue:
self.current_output_queue.put(("complete", content))
except queue.Empty:
continue
thread.join(timeout=5)
if thread.is_alive():
return "❌ 工具执行超时"
result = final_result or ""
tool.streaming_output = False
tool.output_queue = None
else:
# 普通执行
result = tool.execute(**kwargs)
elapsed = time.time() - start_time
self.logger.debug(f"工具结果 ({elapsed:.2f}s): {result[:100]}...")
return result
except TypeError as e:
return f"❌ 工具执行错误: 参数错误 - {str(e)}"
except Exception as e:
return f"❌ 工具执行错误: {str(e)}"
def reload_skills(self, skills_dir: str = None) -> int:
"""
重新加载所有扩展技能(热重载)。
委托给 SkillManager 重新扫描目录并执行 --help 收集简介。
"""
self.logger.info("开始重新加载扩展技能...")
if self.skill_manager is not None:
count = self.skill_manager.reload()
self.logger.info(i18n.get('skills_reloaded', count))
return count
self.logger.warning("skill_manager 未初始化,无法重载技能")
return 0
def get_skills_count(self) -> Tuple[int, int]:
"""获取内置工具和扩展技能的数量"""
builtin_count = len(self._builtin_tools)
skill_count = len(self._tools) - builtin_count
return builtin_count, skill_count