From 6db0cf017eafb4a977ab5ad92ccbaecbb0a5f4c4 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Wed, 12 Aug 2026 09:41:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(aibox):=20=E8=A1=A5=E5=85=A8=20common/logge?= =?UTF-8?q?r=20=E7=BC=BA=E5=A4=B1=E7=9A=84=20logging=20=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E5=B9=B6=E5=9B=BA=E5=8C=96=20common=20=E5=8C=85=E6=8A=BD?= =?UTF-8?q?=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - common/logger.py 漏导 logging / RotatingFileHandler,导致 main() 在 Logger.init_file_logging() 处抛 NameError;现已补回(抽取自 aibox.py 时 未携带该 import) - 固化 aibox 模块化重构成果:common/ 包(i18n/config/constants/logger + __init__ 重导出)已落地,aibox.py 删除内联 I18n 改从 common 导入 - requirements.txt 新增 PySocks 依赖 --- aibox/aibox.py | 941 +------------------------------------- aibox/common/__init__.py | 21 + aibox/common/config.py | 250 ++++++++++ aibox/common/constants.py | 241 ++++++++++ aibox/common/i18n.py | 397 ++++++++++++++++ aibox/common/logger.py | 97 ++++ aibox/requirements.txt | 1 + 7 files changed, 1009 insertions(+), 939 deletions(-) create mode 100644 aibox/common/__init__.py create mode 100644 aibox/common/config.py create mode 100644 aibox/common/constants.py create mode 100644 aibox/common/i18n.py create mode 100644 aibox/common/logger.py diff --git a/aibox/aibox.py b/aibox/aibox.py index 8fae687..f053dd0 100644 --- a/aibox/aibox.py +++ b/aibox/aibox.py @@ -38,397 +38,8 @@ import requests # 大模型客户端抽象层(支持多厂商切换) from llm_client import create_llm_client -# ==================== 语言检测和多语言支持 ==================== - - -class I18n: - """多语言支持类 - 单例模式""" - - _instance = None - _lang = None - _strings = {} - - def __new__(cls): - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance - - def __init__(self): - if not self._strings: - self._detect_language() - self._load_strings() - - def _detect_language(self): - # 首先尝试从环境变量获取(Linux/Mac) - lang = os.environ.get('LANG') or os.environ.get('LC_ALL') - if lang: - if lang.startswith('en_'): - self._lang = 'en' - else: - self._lang = 'zh' - else: - # Windows: 使用 locale.getlocale()(无需 setlocale) - try: - sys_locale = locale.getlocale()[0] - if sys_locale and sys_locale.startswith('en_'): - self._lang = 'en' - else: - self._lang = 'zh' - except: - self._lang = 'zh' - # 强制覆盖 - force_lang = os.environ.get("AI_LANGUAGE") - if force_lang in ['en', 'zh']: - self._lang = force_lang - - def _load_strings(self): - """加载多语言字符串""" - self._strings = { - 'zh': { - # 欢迎信息 - 'welcome_title': "🤖 单AI智能体系统 - WebSocket服务器已启动", - 'data_dir': "📁 数据存储目录: {}", - 'skills_dir': "📁 技能目录: {}", - 'prompts_dir': "📁 提示词目录: {}", - 'config_file': "📋 配置文件: {}", - 'memo_stats': "📋 任务统计:", - 'total_memos': " 📝 总任务: {} 个", - 'pending_memos': " ⏰ 待执行: {} 个", - 'overdue_memos': " ⚠️ 已过期: {} 个", - 'completed_memos': " ✅ 已完成: {} 个", - 'loaded_skills': "🔧 已加载扩展技能 ({} 个):", - 'skill_item': " • {}", - 'more_skills': " ... 还有 {} 个", - 'loaded_prompts': "📝 已加载提示词 ({} 个):", - 'prompt_item': " • {}", - 'websocket_started': "🌐 WebSocket服务器已启动在 ws://{}:{}", - 'client_connected': "🔌 客户端已连接: {}", - 'client_disconnected': "🔌 客户端断开连接: {}", - - # 任务调度器相关 - 'scheduler_started': "⏰ 任务调度器已启动", - 'scheduler_stopped': "⏰ 任务调度器已停止", - 'task_added': "📋 已添加任务: {} (ID: {})", - 'task_removed': "📋 已移除任务: {}", - 'task_executing': "⚙️ 正在执行任务: {} (ID: {})", - 'task_executed': "✅ 任务执行成功: {}", - 'task_failed': "❌ 任务执行失败: {} - {}", - 'task_stats': "📊 任务统计: 总数={}, 待执行={}, 已过期={}, 已完成={}", - 'check_memos_task': "检查过期任务", - 'broadcast_tasks': "📢 广播: {} 个任务已到期", - 'next_execution': "⏱️ 下次执行: {}", - 'task_completed': "✅ 任务已完成: {}", - - # 命令处理 - 'goodbye': "👋 再见!", - 'new_conversation': "🔄 开始新对话(历史已保存)", - 'new_conversation_reset': "🔄 开始新对话(历史已保存,对话记忆已重置)", - 'saved_conversations': "📚 已保存的对话:", - 'conv_status_current': "当前", - 'conv_status_archived': "归档", - 'recent_memories': "📚 最近的记忆:", - 'pending_memos_title': "📋 待执行的任务:", - 'memo_overdue': "⚠️", - 'memo_pending': "⏰", - 'search_results': "🔍 搜索 '{}' 找到 {} 条记忆:", - 'current_config': "⚙️ 当前配置:", - 'config_debug': " 调试级别: {}", - 'config_data_dir': " 数据目录: {}", - 'config_skills_dir': " 技能目录: {}", - 'config_prompts_dir': " 提示词目录: {}", - 'config_commands': " 命令执行: {}", - 'config_history': " 加载历史: {}", - 'config_executor': " 命令执行器: {}", - 'config_scheduler': " 任务调度器: {}", - 'enabled': "启用", - 'disabled': "禁用", - 'config_reloaded': "✅ 配置已重新加载", - 'prompts_reloaded': "✅ 提示词已重新加载 ({} 个)", - 'checking_memos': "🔍 检查任务...", - 'overdue_found': "发现 {} 个到期任务:", - 'no_overdue': "没有到期的任务", - 'memo_tool_usage': "📝 任务工具使用:", - 'memo_usage_in_dialogue': " 在对话中可以直接使用 memo 工具:", - 'memo_add_example': " - memo add title=\"任务标题\" content=\"任务内容\" reminder_time=\"+1h\" repeat_type=\"none\"", - 'memo_list_example': " - memo list", - 'memo_complete_example': " - memo complete memo_id=1", - 'memory_habit': "🧠 记忆习惯:", - 'memory_habit_desc': " AI会自动检查相关记忆,保持对话连贯性", - 'memory_save_desc': " 对话结束时,AI会自动保存重要记忆", - 'command_stream': "⚙️ 命令执行实时输出:", - 'command_stream_desc': " run_command 工具会自动实时显示命令输出", - 'command_stream_disable': " 可以通过 stream=false 参数关闭实时输出", - 'skills_extension': "🔧 扩展技能:", - 'skills_desc': " 将Python脚本放入 ~/.aibox/skills/ 目录", - 'skills_interface_desc': " 可执行文件需支持 --help 选项,第一行为简介,其余为完整使用说明", - 'skills_reloaded': "✅ 技能已重新加载 ({} 个)", - 'skills_reload_failed': "❌ 技能重载失败: {}", - 'skills_reload_success': "✅ 技能热重载成功,当前共有 {} 个扩展技能", - - # 对话相关 - 'input_prompt': "💬 请输入问题: ", - 'thinking': "🤔 思考中... [{}]", - 'assistant_prefix': " 💬 助手: ", - 'calling_tools': "\n 🔧 需要调用工具...", - 'analyzing_tool_results': "\n 🤔 分析工具结果...", - 'conversation_start': "🎭 对话开始", - 'conversation_ended': "✅ AI保存记忆并结束了对话", - 'conversation_stats': "\n📊 对话结束,总轮数: {}", - 'user_interrupted': "\n\n👋 用户中断", - 'error_prefix': "❌ 错误: ", - - # 工具相关 - 'command_output_title': "\n" + "=" * 50 + "\n📟 命令实时输出:\n" + "=" * 50, - 'command_output_end': "\n" + "=" * 50, - - # 备忘录检查 - 'memo_check_title': "⏰ 检查到期任务...", - 'memo_check_found': "发现 {} 个到期任务:", - 'memo_check_none': "没有到期的任务", - 'memo_list_title': "📋 待执行任务 (共 {} 个):", - 'memo_added': "✅ 任务已添加,ID: {}", - 'memo_add_failed': "❌ 添加失败: {}", - 'memo_completed': "✅ 任务 #{} 已完成", - 'memo_complete_failed': "❌ 无法完成任务 #{}", - - # 即时任务相关 - 'immediate_memo_added': "⚡ 已添加为即时任务并立即检查", - 'immediate_memo_executing': "⚡ 正在执行即时任务: {}", - - # 新对话 - 'new_dialogue_start': "🎭 开始新对话: {}", - - # 记忆相关 - 'memory_check': "习惯性检查相关记忆...", - 'memory_found': "找到 {} 条相关记忆", - - # 配置文件 - 'config_loaded': "📋 已加载配置文件: {}", - 'config_not_found': "📋 配置文件不存在: {},使用默认配置", - 'config_created': "📋 已创建默认配置文件: {}", - - # WebSocket消息类型 - 'ws_connected': "✅ 已连接到AI服务器", - 'ws_disconnected': "❌ 与服务器断开连接", - 'ws_error': "⚠️ WebSocket错误: {}", - 'ws_unknown_command': "❌ 未知命令: {}", - 'ws_command_processed': "✅ 命令已处理", - - # 消息类型 - 'msg_type_chunk': "chunk", - 'msg_type_complete': "complete", - 'msg_type_error': "error", - 'msg_type_command_result': "command_result", - 'msg_type_connection_ack': "connection_ack", - 'msg_type_notification': "notification", - 'msg_type_task_status': "task_status", - 'msg_type_task_executing': "task_executing", - 'msg_type_task_complete': "task_complete", - - # 新增提示词 - 'memory_search_prompt': "【重要习惯】在每次回答用户问题前,请先使用 memory search 工具搜索记忆库中与当前话题相关的记忆,确保回答的连贯性和准确性。", - 'identity_updated': "✅ 用户身份已更新,ID: {}", - 'status_updated': "✅ 智能体状态已更新", - - # 对话结束相关 - 'dialogue_ended': "🔚 对话已结束,请开始新对话", - 'dialogue_force_reset': "🔄 对话状态已重置", - 'dialogue_reset_complete': "✅ 对话状态已完全重置,系统提示词已重新加载", - - # 对话压缩相关(静默执行,不输出信息) - 'conversation_compress': "📦 对话内容压缩", - 'conversation_compressed': "✅ 对话已压缩: 从 {} 条消息压缩为 {} 条", - 'compress_threshold_reached': "⚠️ 对话已达到压缩阈值 ({} 条消息),正在压缩...", - 'compress_token_threshold_reached': "⚠️ 对话token数已达到 {},正在压缩...", - 'compress_failed': "❌ 对话压缩失败: {}", - 'compressed_summary': "【对话摘要】{}", - 'last_n_messages': "【最近消息】{}" - }, - 'en': { - # Welcome messages - 'welcome_title': "🤖 Single AI Agent System - WebSocket Server Started", - 'data_dir': "📁 Data directory: {}", - 'skills_dir': "📁 Skills directory: {}", - 'prompts_dir': "📁 Prompts directory: {}", - 'config_file': "📋 Configuration file: {}", - 'memo_stats': "📋 Task statistics:", - 'total_memos': " 📝 Total tasks: {}", - 'pending_memos': " ⏰ Pending: {}", - 'overdue_memos': " ⚠️ Overdue: {}", - 'completed_memos': " ✅ Completed: {}", - 'loaded_skills': "🔧 Loaded extension skills ({}):", - 'skill_item': " • {}", - 'more_skills': " ... {} more", - 'loaded_prompts': "📝 Loaded prompts ({}):", - 'prompt_item': " • {}", - 'websocket_started': "🌐 WebSocket server started at ws://{}:{}", - 'client_connected': "🔌 Client connected: {}", - 'client_disconnected': "🔌 Client disconnected: {}", - - # Scheduler related - 'scheduler_started': "⏰ Task scheduler started", - 'scheduler_stopped': "⏰ Task scheduler stopped", - 'task_added': "📋 Task added: {} (ID: {})", - 'task_removed': "📋 Task removed: {}", - 'task_executing': "⚙️ Executing task: {} (ID: {})", - 'task_executed': "✅ Task executed successfully: {}", - 'task_failed': "❌ Task execution failed: {} - {}", - 'task_stats': "📊 Task stats: total={}, pending={}, overdue={}, completed={}", - 'check_memos_task': "Check overdue tasks", - 'broadcast_tasks': "📢 Broadcast: {} tasks overdue", - 'next_execution': "⏱️ Next execution: {}", - 'task_completed': "✅ Task completed: {}", - - # Command processing - 'goodbye': "👋 Goodbye!", - 'new_conversation': "🔄 Starting new conversation (history saved)", - 'new_conversation_reset': "🔄 Starting new conversation (history saved, conversation memory reset)", - 'saved_conversations': "📚 Saved conversations:", - 'conv_status_current': "current", - 'conv_status_archived': "archived", - 'recent_memories': "📚 Recent memories:", - 'pending_memos_title': "📋 Pending tasks:", - 'memo_overdue': "⚠️", - 'memo_pending': "⏰", - 'search_results': "🔍 Searching '{}' found {} memories:", - 'current_config': "⚙️ Current configuration:", - 'config_debug': " Debug level: {}", - 'config_data_dir': " Data directory: {}", - 'config_skills_dir': " Skills directory: {}", - 'config_prompts_dir': " Prompts directory: {}", - 'config_commands': " Command execution: {}", - 'config_history': " Load history: {}", - 'config_executor': " Command executor: {}", - 'config_scheduler': " Task scheduler: {}", - 'enabled': "enabled", - 'disabled': "disabled", - 'config_reloaded': "✅ Configuration reloaded", - 'prompts_reloaded': "✅ Prompts reloaded ({})", - 'checking_memos': "🔍 Checking tasks...", - 'overdue_found': "Found {} overdue tasks:", - 'no_overdue': "No overdue tasks", - 'memo_tool_usage': "📝 Task tool usage:", - 'memo_usage_in_dialogue': " You can use memo tool directly in dialogue:", - 'memo_add_example': " - memo add title=\"Task\" content=\"Content\" reminder_time=\"+1h\" repeat_type=\"none\"", - 'memo_list_example': " - memo list", - 'memo_complete_example': " - memo complete memo_id=1", - 'memory_habit': "🧠 Memory habits:", - 'memory_habit_desc': " AI automatically checks relevant memories for conversation continuity", - 'memory_save_desc': " AI automatically saves important memories when conversation ends", - 'command_stream': "⚙️ Command execution real-time output:", - 'command_stream_desc': " run_command tool automatically displays command output in real-time", - 'command_stream_disable': " You can disable real-time output with stream=false parameter", - 'skills_extension': "🔧 Extension skills:", - 'skills_desc': " Put Python scripts in ~/.aibox/skills/ directory", - 'skills_interface_desc': " Executables must support --help; first line is the summary, rest is the full manual", - 'skills_reloaded': "✅ Skills reloaded ({})", - 'skills_reload_failed': "❌ Skills reload failed: {}", - 'skills_reload_success': "✅ Skills hot-reloaded successfully, now {} extension skills available", - - # Dialogue related - 'input_prompt': "💬 Please enter your question: ", - 'thinking': "🤔 Thinking... [{}]", - 'assistant_prefix': " 💬 Assistant: ", - 'calling_tools': "\n 🔧 Need to call tools...", - 'analyzing_tool_results': "\n 🤔 Analyzing tool results...", - 'conversation_start': "🎭 Conversation started", - 'conversation_ended': "✅ AI saved memory and ended conversation", - 'conversation_stats': "\n📊 Conversation ended, total turns: {}", - 'user_interrupted': "\n\n👋 User interrupted", - 'error_prefix': "❌ Error: ", - - # Tool related - 'command_output_title': "\n" + "=" * 50 + "\n📟 Command real-time output:\n" + "=" * 50, - 'command_output_end': "\n" + "=" * 50, - - # Memo check - 'memo_check_title': "⏰ Checking overdue tasks...", - 'memo_check_found': "Found {} overdue tasks:", - 'memo_check_none': "No overdue tasks", - 'memo_list_title': "📋 Pending tasks (total {}):", - 'memo_added': "✅ Task added, ID: {}", - 'memo_add_failed': "❌ Failed to add: {}", - 'memo_completed': "✅ Task #{} completed", - 'memo_complete_failed': "❌ Cannot complete task #{}", - - # Immediate task related - 'immediate_memo_added': "⚡ Added as immediate task and checked now", - 'immediate_memo_executing': "⚡ Executing immediate task: {}", - - # New dialogue - 'new_dialogue_start': "🎭 Starting new dialogue: {}", - - # Memory related - 'memory_check': "Habitually checking relevant memories...", - 'memory_found': "Found {} relevant memories", - - # Configuration file - 'config_loaded': "📋 Configuration file loaded: {}", - 'config_not_found': "📋 Configuration file not found: {}, using default configuration", - 'config_created': "📋 Default configuration file created: {}", - - # WebSocket message types - 'ws_connected': "✅ Connected to AI server", - 'ws_disconnected': "❌ Disconnected from server", - 'ws_error': "⚠️ WebSocket error: {}", - 'ws_unknown_command': "❌ Unknown command: {}", - 'ws_command_processed': "✅ Command processed", - - # Message types - 'msg_type_chunk': "chunk", - 'msg_type_complete': "complete", - 'msg_type_error': "error", - 'msg_type_command_result': "command_result", - 'msg_type_connection_ack': "connection_ack", - 'msg_type_notification': "notification", - 'msg_type_task_status': "task_status", - 'msg_type_task_executing': "task_executing", - 'msg_type_task_complete': "task_complete", - - # New prompts - 'memory_search_prompt': "【Important Habit】Before answering each user question, please use the memory search tool to search the memory database for information related to the current topic, ensuring coherence and accuracy of responses.", - 'identity_updated': "✅ User identity updated, ID: {}", - 'status_updated': "✅ Agent status updated", - - # Dialogue end related - 'dialogue_ended': "🔚 Dialogue ended, please start a new one", - 'dialogue_force_reset': "🔄 Dialogue state reset", - 'dialogue_reset_complete': "✅ Dialogue state fully reset, system prompts reloaded", - - # Conversation compression related (silent execution) - 'conversation_compress': "📦 Conversation compression", - 'conversation_compressed': "✅ Conversation compressed: from {} messages to {}", - 'compress_threshold_reached': "⚠️ Conversation reached compression threshold ({} messages), compressing...", - 'compress_token_threshold_reached': "⚠️ Conversation token count reached {}, compressing...", - 'compress_failed': "❌ Conversation compression failed: {}", - 'compressed_summary': "【Conversation Summary】{}", - 'last_n_messages': "【Recent Messages】{}" - } - } - - def get(self, key: str, *args) -> str: - """获取指定语言的字符串,支持格式化参数""" - if key in self._strings[self._lang]: - text = self._strings[self._lang][key] - if args: - return text.format(*args) - return text - if key in self._strings['zh']: - text = self._strings['zh'][key] - if args: - return text.format(*args) - return text - return key - - @property - def lang(self) -> str: - return self._lang - - - -# 创建全局多语言实例 -i18n = I18n() - +# 基础设施(多语言 / 配置 / 常量 / 日志)已抽离至 common 模块 +from common import i18n, I18n, ConfigManager, Constants, constants, Logger # ==================== 提示词管理器 ==================== @@ -1160,557 +771,9 @@ update_soul content="我是...(新的自我定义)" return self._prompts_dir -# ==================== 配置管理 ==================== -class ConfigManager: - """配置管理器 - 单例模式,负责加载和保存配置""" - - _instance = None - _config = None - _config_path = None - - def __new__(cls): - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance - - def __init__(self): - if self._config is None: - self._load_config() - - def _get_default_config_path(self) -> str: - env_config = os.environ.get("AI_CONFIG_PATH") - if env_config: - return env_config - return os.path.expanduser("~/.aibox/config.json") - - def _get_default_config(self) -> Dict: - return { - # 系统参数 - "system": { - "save_dir": "~/.aibox", # 储存目录 - "datetime_format": "%Y-%m-%d %H:%M:%S", # 时间格式 - "date_format": "%Y%m%d_%H%M%S", # 另一个时间格式 - "debug_level": 1, # 调试等级 - "load_history_by_default": True, # 默认加载历史记录 - "log_file": "~/.aibox/ai.log", # 日志文件 - "log_max_size": 10485760, # 日志最大大小 - "log_backup_count": 5, # 日志备份数量 - "skills_dir": "~/.aibox/skills", # 技能目录 - "prompts_dir": "~/.aibox/prompts", # 提示词目录 - "workspace_dir": "~/.aibox/workspace", # 工作空间目录 - "websocket_host": "localhost", # websocket监听地址 - "websocket_port": 8765, # websocket监听端口 - }, - - # 调度程序 - "scheduler": { - "enabled": True, # 开启调度 - "check_interval": 1.0, # 检查间隔 - "max_tasks_per_run": 10, # 每次运行最大任务量 - "broadcast_enabled": True # 开启广播 - }, - - - # 大模型调用接口 - "api": { - "deepseek_api_url": "https://api.deepseek.com/chat/completions", # deepseek API URL - "deepseek_model": "deepseek-v4-flash", # 模型名字 - "reasoning_enabled": False, # 开启思维链 - "api_key_env": "DEEPSEEK_API_KEY", # API-KEY所在的环境变量 - "timeout": 120, # 调用大模型请求超时时间 - "temperature": 0.7 # 大模型对话温度(类似思维发散程度) - }, - - # 大模型上下文 - "context": { - "max_history_messages": 500, # 最大历史消息数量 - "max_context_messages": 120, # 最大内容消息数量(?) - "max_tokens": 8192, # 最大输出token量 - "max_history_conversations": 5, # 最大历史对话 - "preserve_system_prompts": True, # 开启前置系统提示词 - # 对话压缩配置 - "compress_enabled": True, # 开启压缩 - "compress_message_threshold": 200, # 压缩消息条数阈值 - "compress_token_threshold": 120000, # 压缩消息词元阈值 - "compress_ratio": 0.2, # 压缩率 - "compress_keep_recent": 16 # 压缩保留最近的几条 - }, - - - # 定时任务参数 - "memo": { - "default_reminder_minutes": 60, - "max_memos_per_check": 10, # 每次检查定时任务数量 - "allow_user_tasks": True # 允许用户任务 - }, - - # 文件 - "files": { - "max_file_size": 10485710 # 最大文件大小 - }, - - # 指令 - "commands": { - "enabled": True # 开启指令 - }, - - # 网页 - "web": { - "timeout": 10, # 超时时间 - "delay": 0, # 延时 - "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", - "respect_robots": False, # - "max_download_size": 10485760 # 最大下载大小 - }, - - # 记忆 - "memory": { - "db_name": "memories.db", # 数据库名 - "max_search_results": 100, # 最大搜索结果 - "default_importance": 2 # ? - }, - - # 对话 - "conversation": { - "current_file": "current_conversation.json", # 当前对话文件 - "history_file": "conversation_history.json", # 历史对话文件 - "archive_dir": "archive" # ? - }, - - # 工具 - "tools": { - "max_result_length": 10000, # 结果最大长度 - "save_result_to_file": False, # 保存结果到文件 - "tool_results_dir": "tool_results" # 工具结果存储目录 - }, - - # 压缩器 - "compressor": { - "min_messages_to_compress": 5, # 最小压缩数量 - "weight_decay_factor": 0.01, # ? - "max_input_chars": 6000, # 最大输入字符 - "llm_max_tokens": 1000 # 大模型最大输出词元 - } - } - - def _load_config(self): - config_path = self._get_default_config_path() - self._config_path = config_path - - if os.path.exists(config_path): - try: - with open(config_path, 'r', encoding='utf-8') as f: - user_config = json.load(f) - default_config = self._get_default_config() - self._config = self._merge_configs(default_config, user_config) - print(i18n.get('config_loaded', config_path)) - except Exception as e: - print(f"⚠️ {i18n.get('error_prefix')}{e},使用默认配置") - self._config = self._get_default_config() - else: - print(i18n.get('config_not_found', config_path)) - self._config = self._get_default_config() - self._save_default_config(config_path) - - def _merge_configs(self, default: Dict, user: Dict) -> Dict: - result = default.copy() - for key, value in user.items(): - if key in result and isinstance(result[key], dict) and isinstance(value, dict): - result[key] = self._merge_configs(result[key], value) - else: - result[key] = value - return result - - def _save_default_config(self, config_path: str): - try: - os.makedirs(os.path.dirname(config_path), exist_ok=True) - with open(config_path, 'w', encoding='utf-8') as f: - json.dump(self._get_default_config(), f, ensure_ascii=False, indent=2) - print(i18n.get('config_created', config_path)) - except: - pass - - def get(self, key_path: str, default=None): - keys = key_path.split('.') - value = self._config - try: - for key in keys: - value = value[key] - return value - except (KeyError, TypeError): - return default - - def reload(self): - self._load_config() - - - def get_proxy(self) -> Optional[Dict[str, str]]: - """获取当前代理配置(返回 requests 可用的 proxies 字典)""" - proxy_conf = self.get("proxy.socks5") - if not proxy_conf or not proxy_conf.get("enabled", False): - return None - host = proxy_conf.get("host", "127.0.0.1") - port = proxy_conf.get("port", 1080) - username = proxy_conf.get("username") - password = proxy_conf.get("password") - if username and password: - proxy_url = f"socks5://{username}:{password}@{host}:{port}" - else: - proxy_url = f"socks5://{host}:{port}" - return {"http": proxy_url, "https": proxy_url} - - def set_proxy(self, host: str, port: int, username: str = None, password: str = None, enabled: bool = True, save: bool = True): - """动态设置代理配置(内存中更新,并可选择写入配置文件)""" - # 确保 proxy 节点存在 - if "proxy" not in self._config: - self._config["proxy"] = {} - self._config["proxy"]["socks5"] = { - "enabled": enabled, - "host": host, - "port": port, - "username": username, - "password": password - } - if save: - self._save_config() - return True - - def _save_config(self): - """将当前配置保存到配置文件(覆盖)""" - config_path = self._config_path - if config_path: - try: - os.makedirs(os.path.dirname(config_path), exist_ok=True) - with open(config_path, 'w', encoding='utf-8') as f: - json.dump(self._config, f, ensure_ascii=False, indent=2) - print(f"✅ 配置已保存到 {config_path}") - except Exception as e: - print(f"⚠️ 保存配置失败: {e}") - - def get_proxy_info(self) -> Dict: - """获取代理配置的详细信息(用于显示)""" - proxy_conf = self.get("proxy.socks5", {}) - return { - "enabled": proxy_conf.get("enabled", False), - "host": proxy_conf.get("host", ""), - "port": proxy_conf.get("port", 0), - "username": proxy_conf.get("username"), - "password": proxy_conf.get("password") is not None # 只显示是否存在 - } -# ==================== 常量定义 ==================== - - -class Constants: - """系统常量 - 从配置管理器获取""" - - _config = ConfigManager() - - @classmethod - def _get_config(cls): - return cls._config - - @property - def DATETIME_FORMAT(cls): - return cls._get_config().get("system.datetime_format", "%Y-%m-%d %H:%M:%S") - - @property - def DATE_FORMAT(cls): - return cls._get_config().get("system.date_format", "%Y%m%d_%H%M%S") - - @property - def DEEPSEEK_API_URL(cls): - return cls._get_config().get("api.deepseek_api_url") - - @property - def DEEPSEEK_MODEL(cls): - return cls._get_config().get("api.deepseek_model", "deepseek-v4-flash") - - @property - def REASONING_ENABLED(cls): - return cls._get_config().get("api.reasoning_enabled", False) - - @property - def DEFAULT_TIMEOUT(cls): - return cls._get_config().get("api.timeout", 30) - - @property - def DEFAULT_MAX_HISTORY(cls): - return cls._get_config().get("context.max_history_messages", 500) - - @property - def DEFAULT_TEMPERATURE(cls): - return cls._get_config().get("api.temperature", 0.7) - - @property - def DEFAULT_MAX_TOKENS(cls): - return cls._get_config().get("context.max_tokens", 8000) - - @property - def MAX_FILE_SIZE(cls): - return cls._get_config().get("files.max_file_size", 10485710) - - @property - def SAVE_DIR(cls): - return os.path.expanduser(cls._get_config().get("system.save_dir", "~/.aibox")) - - @property - def SKILLS_DIR(cls): - return os.path.expanduser(cls._get_config().get("system.skills_dir", "~/.aibox/skills")) - - @property - def WORKSPACE_DIR(cls): - return os.path.expanduser(cls._get_config().get("system.workspace_dir", "~/.aibox/workspace")) - - @property - def PROMPTS_DIR(cls): - return os.path.expanduser(cls._get_config().get("system.prompts_dir", "~/.aibox/prompts")) - - @property - def MEMORY_DB(cls): - return cls._get_config().get("memory.db_name", "memories.db") - - @property - def CURRENT_CONVERSATION_FILE(cls): - return cls._get_config().get("conversation.current_file", "current_conversation.json") - - @property - def HISTORY_FILE(cls): - return cls._get_config().get("conversation.history_file", "conversation_history.json") - - @property - def ARCHIVE_DIR(cls): - return cls._get_config().get("conversation.archive_dir", "archive") - - @property - def USER_PREFIX(cls): - return "[用户]" - - @property - def SYSTEM_PREFIX(cls): - return "[系统]" - - @property - def ASSISTANT_PREFIX(cls): - return "[助手]" - - @property - def MAX_HISTORY_CONTEXT(cls): - return cls._get_config().get("context.max_context_messages", 50) - - @property - def MAX_HISTORY_CONVERSATIONS(cls): - return cls._get_config().get("context.max_history_conversations", 5) - - @property - def PRESERVE_SYSTEM_PROMPTS(cls): - return cls._get_config().get("context.preserve_system_prompts", True) - - @property - def WEB_TIMEOUT(cls): - return cls._get_config().get("web.timeout", 10) - - @property - def WEB_DELAY(cls): - return cls._get_config().get("web.delay", 0) - - @property - def WEB_USER_AGENT(cls): - return cls._get_config().get("web.user_agent", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") - - @property - def WEB_MAX_DOWNLOAD_SIZE(cls): - return cls._get_config().get("web.max_download_size", 104857600) - - @property - def MEMORY_MAX_SEARCH_RESULTS(cls): - return cls._get_config().get("memory.max_search_results", 100) - - @property - def MEMORY_DEFAULT_IMPORTANCE(cls): - return cls._get_config().get("memory.default_importance", 2) - - @property - def MEMO_DEFAULT_REMINDER_MINUTES(cls): - return cls._get_config().get("memo.default_reminder_minutes", 60) - - @property - def MEMO_MAX_PER_CHECK(cls): - return cls._get_config().get("memo.max_memos_per_check", 10) - - @property - def LOG_FILE(cls): - return os.path.expanduser(cls._get_config().get("system.log_file", "~/.aibox/ai.log")) - - @property - def LOG_MAX_SIZE(cls): - return cls._get_config().get("system.log_max_size", 10485760) - - @property - def LOG_BACKUP_COUNT(cls): - return cls._get_config().get("system.log_backup_count", 5) - - @property - def COMMANDS_ENABLED(cls): - return cls._get_config().get("commands.enabled", True) - - @property - def WEBSOCKET_HOST(cls): - return cls._get_config().get("system.websocket_host", "localhost") - - @property - def WEBSOCKET_PORT(cls): - return cls._get_config().get("system.websocket_port", 8765) - - @property - def SCHEDULER_ENABLED(cls): - return cls._get_config().get("scheduler.enabled", True) - - @property - def SCHEDULER_CHECK_INTERVAL(cls): - return cls._get_config().get("scheduler.check_interval", 1.0) - - @property - def SCHEDULER_MAX_TASKS(cls): - return cls._get_config().get("scheduler.max_tasks_per_run", 10) - - @property - def SCHEDULER_BROADCAST(cls): - return cls._get_config().get("scheduler.broadcast_enabled", True) - - @property - def MEMO_ALLOW_USER_TASKS(cls): - return cls._get_config().get("memo.allow_user_tasks", True) - - @property - def COMPRESS_ENABLED(cls): - return cls._get_config().get("context.compress_enabled", True) - - @property - def COMPRESS_MESSAGE_THRESHOLD(cls): - return cls._get_config().get("context.compress_message_threshold", 100) - - @property - def COMPRESS_TOKEN_THRESHOLD(cls): - return cls._get_config().get("context.compress_token_threshold", 4000) - - @property - def COMPRESS_RATIO(cls): - return cls._get_config().get("context.compress_ratio", 0.4) - - @property - def COMPRESS_KEEP_RECENT(cls): - return cls._get_config().get("context.compress_keep_recent", 20) - - @property - def TOOL_MAX_RESULT_LENGTH(cls): - return cls._get_config().get("tools.max_result_length", 100000) # 0.1MB 字符限制 - - @property - def COMPRESSOR_MIN_MESSAGES(cls): - return cls._get_config().get("compressor.min_messages_to_compress", 5) - - @property - def COMPRESSOR_WEIGHT_DECAY_FACTOR(cls): - return cls._get_config().get("compressor.weight_decay_factor", 0.01) - - @property - def COMPRESSOR_MAX_INPUT_CHARS(cls): - return cls._get_config().get("compressor.max_input_chars", 6000) - - @property - def COMPRESSOR_LLM_MAX_TOKENS(cls): - return cls._get_config().get("compressor.llm_max_tokens", 1000) - - -# 创建常量实例 -constants = Constants() - - -# ==================== 日志系统 ==================== - -class Logger: - """统一日志系统 - 支持文件和终端输出""" - - LEVELS = { - "DEBUG": 3, - "INFO": 2, - "WARNING": 1, - "ERROR": 0 - } - - _file_handler = None - _log_initialized = False - - @classmethod - def init_file_logging(cls, log_file: str = None, max_size: int = None, backup_count: int = None): - """初始化文件日志""" - if cls._log_initialized: - return - - if log_file is None: - log_file = constants.LOG_FILE - if max_size is None: - max_size = constants.LOG_MAX_SIZE - if backup_count is None: - backup_count = constants.LOG_BACKUP_COUNT - - log_dir = os.path.dirname(log_file) - if log_dir: - os.makedirs(log_dir, exist_ok=True) - - cls._file_handler = RotatingFileHandler( - log_file, - maxBytes=max_size, - backupCount=backup_count, - encoding='utf-8' - ) - - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - cls._file_handler.setFormatter(formatter) - - cls._log_initialized = True - - def __init__(self, name: str = "System", level: int = None): - self.name = name - config = ConfigManager() - if level is None: - level = config.get("system.debug_level", 1) - self.level = level - - self.py_logger = logging.getLogger(f"AI.{name}") - self.py_logger.setLevel(logging.DEBUG) - - if Logger._file_handler and not self.py_logger.handlers: - self.py_logger.addHandler(Logger._file_handler) - - def debug(self, msg: str): - if self.level >= 3: - self._log("🐛", "DEBUG", msg) - self.py_logger.debug(msg) - - def info(self, msg: str): - if self.level >= 2: - self._log("📋", "INFO", msg) - self.py_logger.info(msg) - - def warning(self, msg: str): - if self.level >= 1: - self._log("⚠️", "WARNING", msg) - self.py_logger.warning(msg) - - def error(self, msg: str): - if self.level >= 0: - self._log("❌", "ERROR", msg) - self.py_logger.error(msg) - - def _log(self, emoji: str, level: str, msg: str): - """输出到终端""" - timestamp = datetime.now().strftime("%H:%M:%S") - print(f"{emoji} [{timestamp}] [{self.name}] {msg}", flush=True) class ConversationCompressor: """对话压缩管理器 - 优先压缩较早的消息,将tool调用信息一并压缩""" diff --git a/aibox/common/__init__.py b/aibox/common/__init__.py new file mode 100644 index 0000000..fba2097 --- /dev/null +++ b/aibox/common/__init__.py @@ -0,0 +1,21 @@ +""" +common - aibox 底层基础设施公共模块(包) + +抽离自 aibox.py 的零依赖基础层,供 aibox.py 及后续 tools.py 等模块导入。 +当前包含: + - i18n.py : I18n 类 + i18n 实例(多语言) + - config.py : ConfigManager(配置管理) + - constants.py : Constants 类 + constants 实例(系统常量) + - logger.py : Logger(统一日志) +形成单向依赖:common <- tools <- aibox。 + +采用"重导出"策略:外部统一用 `from common import i18n, I18n, ...`, +调用点无需关心具体子模块所在文件。 +""" + +from .i18n import i18n, I18n +from .config import ConfigManager +from .constants import Constants, constants +from .logger import Logger + +__all__ = ["i18n", "I18n", "ConfigManager", "Constants", "constants", "Logger"] diff --git a/aibox/common/config.py b/aibox/common/config.py new file mode 100644 index 0000000..06b471b --- /dev/null +++ b/aibox/common/config.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +""" +common.config - aibox 配置管理器 + +抽离自 aibox.py 的 ConfigManager。仅依赖 common.i18n(在运行期方法内部使用), +不反向依赖 aibox 任何符号,可安全独立导入。 +""" + +import os +import json +from typing import Dict, Optional + +from .i18n import i18n +class ConfigManager: + """配置管理器 - 单例模式,负责加载和保存配置""" + + _instance = None + _config = None + _config_path = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if self._config is None: + self._load_config() + + def _get_default_config_path(self) -> str: + env_config = os.environ.get("AI_CONFIG_PATH") + if env_config: + return env_config + return os.path.expanduser("~/.aibox/config.json") + + def _get_default_config(self) -> Dict: + return { + + # 系统参数 + "system": { + "save_dir": "~/.aibox", # 储存目录 + "datetime_format": "%Y-%m-%d %H:%M:%S", # 时间格式 + "date_format": "%Y%m%d_%H%M%S", # 另一个时间格式 + "debug_level": 1, # 调试等级 + "load_history_by_default": True, # 默认加载历史记录 + "log_file": "~/.aibox/ai.log", # 日志文件 + "log_max_size": 10485760, # 日志最大大小 + "log_backup_count": 5, # 日志备份数量 + "skills_dir": "~/.aibox/skills", # 技能目录 + "prompts_dir": "~/.aibox/prompts", # 提示词目录 + "workspace_dir": "~/.aibox/workspace", # 工作空间目录 + "websocket_host": "localhost", # websocket监听地址 + "websocket_port": 8765, # websocket监听端口 + }, + + # 调度程序 + "scheduler": { + "enabled": True, # 开启调度 + "check_interval": 1.0, # 检查间隔 + "max_tasks_per_run": 10, # 每次运行最大任务量 + "broadcast_enabled": True # 开启广播 + }, + + + # 大模型调用接口 + "api": { + "deepseek_api_url": "https://api.deepseek.com/chat/completions", # deepseek API URL + "deepseek_model": "deepseek-v4-flash", # 模型名字 + "reasoning_enabled": False, # 开启思维链 + "api_key_env": "DEEPSEEK_API_KEY", # API-KEY所在的环境变量 + "timeout": 120, # 调用大模型请求超时时间 + "temperature": 0.7 # 大模型对话温度(类似思维发散程度) + }, + + # 大模型上下文 + "context": { + "max_history_messages": 500, # 最大历史消息数量 + "max_context_messages": 120, # 最大内容消息数量(?) + "max_tokens": 8192, # 最大输出token量 + "max_history_conversations": 5, # 最大历史对话 + "preserve_system_prompts": True, # 开启前置系统提示词 + # 对话压缩配置 + "compress_enabled": True, # 开启压缩 + "compress_message_threshold": 200, # 压缩消息条数阈值 + "compress_token_threshold": 120000, # 压缩消息词元阈值 + "compress_ratio": 0.2, # 压缩率 + "compress_keep_recent": 16 # 压缩保留最近的几条 + }, + + + # 定时任务参数 + "memo": { + "default_reminder_minutes": 60, + "max_memos_per_check": 10, # 每次检查定时任务数量 + "allow_user_tasks": True # 允许用户任务 + }, + + # 文件 + "files": { + "max_file_size": 10485710 # 最大文件大小 + }, + + # 指令 + "commands": { + "enabled": True # 开启指令 + }, + + # 网页 + "web": { + "timeout": 10, # 超时时间 + "delay": 0, # 延时 + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + "respect_robots": False, # + "max_download_size": 10485760 # 最大下载大小 + }, + + # 记忆 + "memory": { + "db_name": "memories.db", # 数据库名 + "max_search_results": 100, # 最大搜索结果 + "default_importance": 2 # ? + }, + + # 对话 + "conversation": { + "current_file": "current_conversation.json", # 当前对话文件 + "history_file": "conversation_history.json", # 历史对话文件 + "archive_dir": "archive" # ? + }, + + # 工具 + "tools": { + "max_result_length": 10000, # 结果最大长度 + "save_result_to_file": False, # 保存结果到文件 + "tool_results_dir": "tool_results" # 工具结果存储目录 + }, + + # 压缩器 + "compressor": { + "min_messages_to_compress": 5, # 最小压缩数量 + "weight_decay_factor": 0.01, # ? + "max_input_chars": 6000, # 最大输入字符 + "llm_max_tokens": 1000 # 大模型最大输出词元 + } + } + + def _load_config(self): + config_path = self._get_default_config_path() + self._config_path = config_path + + if os.path.exists(config_path): + try: + with open(config_path, 'r', encoding='utf-8') as f: + user_config = json.load(f) + default_config = self._get_default_config() + self._config = self._merge_configs(default_config, user_config) + print(i18n.get('config_loaded', config_path)) + except Exception as e: + print(f"⚠️ {i18n.get('error_prefix')}{e},使用默认配置") + self._config = self._get_default_config() + else: + print(i18n.get('config_not_found', config_path)) + self._config = self._get_default_config() + self._save_default_config(config_path) + + def _merge_configs(self, default: Dict, user: Dict) -> Dict: + result = default.copy() + for key, value in user.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + result[key] = self._merge_configs(result[key], value) + else: + result[key] = value + return result + + def _save_default_config(self, config_path: str): + try: + os.makedirs(os.path.dirname(config_path), exist_ok=True) + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(self._get_default_config(), f, ensure_ascii=False, indent=2) + print(i18n.get('config_created', config_path)) + except: + pass + + def get(self, key_path: str, default=None): + keys = key_path.split('.') + value = self._config + try: + for key in keys: + value = value[key] + return value + except (KeyError, TypeError): + return default + + def reload(self): + self._load_config() + + + def get_proxy(self) -> Optional[Dict[str, str]]: + """获取当前代理配置(返回 requests 可用的 proxies 字典)""" + proxy_conf = self.get("proxy.socks5") + if not proxy_conf or not proxy_conf.get("enabled", False): + return None + host = proxy_conf.get("host", "127.0.0.1") + port = proxy_conf.get("port", 1080) + username = proxy_conf.get("username") + password = proxy_conf.get("password") + if username and password: + proxy_url = f"socks5://{username}:{password}@{host}:{port}" + else: + proxy_url = f"socks5://{host}:{port}" + return {"http": proxy_url, "https": proxy_url} + + def set_proxy(self, host: str, port: int, username: str = None, password: str = None, enabled: bool = True, save: bool = True): + """动态设置代理配置(内存中更新,并可选择写入配置文件)""" + # 确保 proxy 节点存在 + if "proxy" not in self._config: + self._config["proxy"] = {} + self._config["proxy"]["socks5"] = { + "enabled": enabled, + "host": host, + "port": port, + "username": username, + "password": password + } + if save: + self._save_config() + return True + + def _save_config(self): + """将当前配置保存到配置文件(覆盖)""" + config_path = self._config_path + if config_path: + try: + os.makedirs(os.path.dirname(config_path), exist_ok=True) + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(self._config, f, ensure_ascii=False, indent=2) + print(f"✅ 配置已保存到 {config_path}") + except Exception as e: + print(f"⚠️ 保存配置失败: {e}") + + def get_proxy_info(self) -> Dict: + """获取代理配置的详细信息(用于显示)""" + proxy_conf = self.get("proxy.socks5", {}) + return { + "enabled": proxy_conf.get("enabled", False), + "host": proxy_conf.get("host", ""), + "port": proxy_conf.get("port", 0), + "username": proxy_conf.get("username"), + "password": proxy_conf.get("password") is not None # 只显示是否存在 + } diff --git a/aibox/common/constants.py b/aibox/common/constants.py new file mode 100644 index 0000000..351b701 --- /dev/null +++ b/aibox/common/constants.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +""" +common.constants - aibox 系统常量 + +抽离自 aibox.py 的 Constants 类与 constants 实例。仅依赖 common.config +(类体内 _config = ConfigManager(),运行期实例化时读取 i18n),不反向依赖 +aibox 任何符号,可安全独立导入。 +""" + +import os +import json + +from .config import ConfigManager +# ==================== 常量定义 ==================== + + +class Constants: + """系统常量 - 从配置管理器获取""" + + _config = ConfigManager() + + @classmethod + def _get_config(cls): + return cls._config + + @property + def DATETIME_FORMAT(cls): + return cls._get_config().get("system.datetime_format", "%Y-%m-%d %H:%M:%S") + + @property + def DATE_FORMAT(cls): + return cls._get_config().get("system.date_format", "%Y%m%d_%H%M%S") + + @property + def DEEPSEEK_API_URL(cls): + return cls._get_config().get("api.deepseek_api_url") + + @property + def DEEPSEEK_MODEL(cls): + return cls._get_config().get("api.deepseek_model", "deepseek-v4-flash") + + @property + def REASONING_ENABLED(cls): + return cls._get_config().get("api.reasoning_enabled", False) + + @property + def DEFAULT_TIMEOUT(cls): + return cls._get_config().get("api.timeout", 30) + + @property + def DEFAULT_MAX_HISTORY(cls): + return cls._get_config().get("context.max_history_messages", 500) + + @property + def DEFAULT_TEMPERATURE(cls): + return cls._get_config().get("api.temperature", 0.7) + + @property + def DEFAULT_MAX_TOKENS(cls): + return cls._get_config().get("context.max_tokens", 8000) + + @property + def MAX_FILE_SIZE(cls): + return cls._get_config().get("files.max_file_size", 10485710) + + @property + def SAVE_DIR(cls): + return os.path.expanduser(cls._get_config().get("system.save_dir", "~/.aibox")) + + @property + def SKILLS_DIR(cls): + return os.path.expanduser(cls._get_config().get("system.skills_dir", "~/.aibox/skills")) + + @property + def WORKSPACE_DIR(cls): + return os.path.expanduser(cls._get_config().get("system.workspace_dir", "~/.aibox/workspace")) + + @property + def PROMPTS_DIR(cls): + return os.path.expanduser(cls._get_config().get("system.prompts_dir", "~/.aibox/prompts")) + + @property + def MEMORY_DB(cls): + return cls._get_config().get("memory.db_name", "memories.db") + + @property + def CURRENT_CONVERSATION_FILE(cls): + return cls._get_config().get("conversation.current_file", "current_conversation.json") + + @property + def HISTORY_FILE(cls): + return cls._get_config().get("conversation.history_file", "conversation_history.json") + + @property + def ARCHIVE_DIR(cls): + return cls._get_config().get("conversation.archive_dir", "archive") + + @property + def USER_PREFIX(cls): + return "[用户]" + + @property + def SYSTEM_PREFIX(cls): + return "[系统]" + + @property + def ASSISTANT_PREFIX(cls): + return "[助手]" + + @property + def MAX_HISTORY_CONTEXT(cls): + return cls._get_config().get("context.max_context_messages", 50) + + @property + def MAX_HISTORY_CONVERSATIONS(cls): + return cls._get_config().get("context.max_history_conversations", 5) + + @property + def PRESERVE_SYSTEM_PROMPTS(cls): + return cls._get_config().get("context.preserve_system_prompts", True) + + @property + def WEB_TIMEOUT(cls): + return cls._get_config().get("web.timeout", 10) + + @property + def WEB_DELAY(cls): + return cls._get_config().get("web.delay", 0) + + @property + def WEB_USER_AGENT(cls): + return cls._get_config().get("web.user_agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + + @property + def WEB_MAX_DOWNLOAD_SIZE(cls): + return cls._get_config().get("web.max_download_size", 104857600) + + @property + def MEMORY_MAX_SEARCH_RESULTS(cls): + return cls._get_config().get("memory.max_search_results", 100) + + @property + def MEMORY_DEFAULT_IMPORTANCE(cls): + return cls._get_config().get("memory.default_importance", 2) + + @property + def MEMO_DEFAULT_REMINDER_MINUTES(cls): + return cls._get_config().get("memo.default_reminder_minutes", 60) + + @property + def MEMO_MAX_PER_CHECK(cls): + return cls._get_config().get("memo.max_memos_per_check", 10) + + @property + def LOG_FILE(cls): + return os.path.expanduser(cls._get_config().get("system.log_file", "~/.aibox/ai.log")) + + @property + def LOG_MAX_SIZE(cls): + return cls._get_config().get("system.log_max_size", 10485760) + + @property + def LOG_BACKUP_COUNT(cls): + return cls._get_config().get("system.log_backup_count", 5) + + @property + def COMMANDS_ENABLED(cls): + return cls._get_config().get("commands.enabled", True) + + @property + def WEBSOCKET_HOST(cls): + return cls._get_config().get("system.websocket_host", "localhost") + + @property + def WEBSOCKET_PORT(cls): + return cls._get_config().get("system.websocket_port", 8765) + + @property + def SCHEDULER_ENABLED(cls): + return cls._get_config().get("scheduler.enabled", True) + + @property + def SCHEDULER_CHECK_INTERVAL(cls): + return cls._get_config().get("scheduler.check_interval", 1.0) + + @property + def SCHEDULER_MAX_TASKS(cls): + return cls._get_config().get("scheduler.max_tasks_per_run", 10) + + @property + def SCHEDULER_BROADCAST(cls): + return cls._get_config().get("scheduler.broadcast_enabled", True) + + @property + def MEMO_ALLOW_USER_TASKS(cls): + return cls._get_config().get("memo.allow_user_tasks", True) + + @property + def COMPRESS_ENABLED(cls): + return cls._get_config().get("context.compress_enabled", True) + + @property + def COMPRESS_MESSAGE_THRESHOLD(cls): + return cls._get_config().get("context.compress_message_threshold", 100) + + @property + def COMPRESS_TOKEN_THRESHOLD(cls): + return cls._get_config().get("context.compress_token_threshold", 4000) + + @property + def COMPRESS_RATIO(cls): + return cls._get_config().get("context.compress_ratio", 0.4) + + @property + def COMPRESS_KEEP_RECENT(cls): + return cls._get_config().get("context.compress_keep_recent", 20) + + @property + def TOOL_MAX_RESULT_LENGTH(cls): + return cls._get_config().get("tools.max_result_length", 100000) # 0.1MB 字符限制 + + @property + def COMPRESSOR_MIN_MESSAGES(cls): + return cls._get_config().get("compressor.min_messages_to_compress", 5) + + @property + def COMPRESSOR_WEIGHT_DECAY_FACTOR(cls): + return cls._get_config().get("compressor.weight_decay_factor", 0.01) + + @property + def COMPRESSOR_MAX_INPUT_CHARS(cls): + return cls._get_config().get("compressor.max_input_chars", 6000) + + @property + def COMPRESSOR_LLM_MAX_TOKENS(cls): + return cls._get_config().get("compressor.llm_max_tokens", 1000) + + +# 创建常量实例 +constants = Constants() diff --git a/aibox/common/i18n.py b/aibox/common/i18n.py new file mode 100644 index 0000000..0f3db9a --- /dev/null +++ b/aibox/common/i18n.py @@ -0,0 +1,397 @@ +""" +i18n - 多语言支持子模块 + +仅依赖标准库 (os / sys / locale),是 common 包的最底层,不依赖包内其他模块。 +""" + +import os +import sys +import locale + +class I18n: + """多语言支持类 - 单例模式""" + + _instance = None + _lang = None + _strings = {} + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if not self._strings: + self._detect_language() + self._load_strings() + + def _detect_language(self): + # 首先尝试从环境变量获取(Linux/Mac) + lang = os.environ.get('LANG') or os.environ.get('LC_ALL') + if lang: + if lang.startswith('en_'): + self._lang = 'en' + else: + self._lang = 'zh' + else: + # Windows: 使用 locale.getlocale()(无需 setlocale) + try: + sys_locale = locale.getlocale()[0] + if sys_locale and sys_locale.startswith('en_'): + self._lang = 'en' + else: + self._lang = 'zh' + except: + self._lang = 'zh' + # 强制覆盖 + force_lang = os.environ.get("AI_LANGUAGE") + if force_lang in ['en', 'zh']: + self._lang = force_lang + + def _load_strings(self): + """加载多语言字符串""" + self._strings = { + 'zh': { + # 欢迎信息 + 'welcome_title': "🤖 单AI智能体系统 - WebSocket服务器已启动", + 'data_dir': "📁 数据存储目录: {}", + 'skills_dir': "📁 技能目录: {}", + 'prompts_dir': "📁 提示词目录: {}", + 'config_file': "📋 配置文件: {}", + 'memo_stats': "📋 任务统计:", + 'total_memos': " 📝 总任务: {} 个", + 'pending_memos': " ⏰ 待执行: {} 个", + 'overdue_memos': " ⚠️ 已过期: {} 个", + 'completed_memos': " ✅ 已完成: {} 个", + 'loaded_skills': "🔧 已加载扩展技能 ({} 个):", + 'skill_item': " • {}", + 'more_skills': " ... 还有 {} 个", + 'loaded_prompts': "📝 已加载提示词 ({} 个):", + 'prompt_item': " • {}", + 'websocket_started': "🌐 WebSocket服务器已启动在 ws://{}:{}", + 'client_connected': "🔌 客户端已连接: {}", + 'client_disconnected': "🔌 客户端断开连接: {}", + + # 任务调度器相关 + 'scheduler_started': "⏰ 任务调度器已启动", + 'scheduler_stopped': "⏰ 任务调度器已停止", + 'task_added': "📋 已添加任务: {} (ID: {})", + 'task_removed': "📋 已移除任务: {}", + 'task_executing': "⚙️ 正在执行任务: {} (ID: {})", + 'task_executed': "✅ 任务执行成功: {}", + 'task_failed': "❌ 任务执行失败: {} - {}", + 'task_stats': "📊 任务统计: 总数={}, 待执行={}, 已过期={}, 已完成={}", + 'check_memos_task': "检查过期任务", + 'broadcast_tasks': "📢 广播: {} 个任务已到期", + 'next_execution': "⏱️ 下次执行: {}", + 'task_completed': "✅ 任务已完成: {}", + + # 命令处理 + 'goodbye': "👋 再见!", + 'new_conversation': "🔄 开始新对话(历史已保存)", + 'new_conversation_reset': "🔄 开始新对话(历史已保存,对话记忆已重置)", + 'saved_conversations': "📚 已保存的对话:", + 'conv_status_current': "当前", + 'conv_status_archived': "归档", + 'recent_memories': "📚 最近的记忆:", + 'pending_memos_title': "📋 待执行的任务:", + 'memo_overdue': "⚠️", + 'memo_pending': "⏰", + 'search_results': "🔍 搜索 '{}' 找到 {} 条记忆:", + 'current_config': "⚙️ 当前配置:", + 'config_debug': " 调试级别: {}", + 'config_data_dir': " 数据目录: {}", + 'config_skills_dir': " 技能目录: {}", + 'config_prompts_dir': " 提示词目录: {}", + 'config_commands': " 命令执行: {}", + 'config_history': " 加载历史: {}", + 'config_executor': " 命令执行器: {}", + 'config_scheduler': " 任务调度器: {}", + 'enabled': "启用", + 'disabled': "禁用", + 'config_reloaded': "✅ 配置已重新加载", + 'prompts_reloaded': "✅ 提示词已重新加载 ({} 个)", + 'checking_memos': "🔍 检查任务...", + 'overdue_found': "发现 {} 个到期任务:", + 'no_overdue': "没有到期的任务", + 'memo_tool_usage': "📝 任务工具使用:", + 'memo_usage_in_dialogue': " 在对话中可以直接使用 memo 工具:", + 'memo_add_example': " - memo add title=\"任务标题\" content=\"任务内容\" reminder_time=\"+1h\" repeat_type=\"none\"", + 'memo_list_example': " - memo list", + 'memo_complete_example': " - memo complete memo_id=1", + 'memory_habit': "🧠 记忆习惯:", + 'memory_habit_desc': " AI会自动检查相关记忆,保持对话连贯性", + 'memory_save_desc': " 对话结束时,AI会自动保存重要记忆", + 'command_stream': "⚙️ 命令执行实时输出:", + 'command_stream_desc': " run_command 工具会自动实时显示命令输出", + 'command_stream_disable': " 可以通过 stream=false 参数关闭实时输出", + 'skills_extension': "🔧 扩展技能:", + 'skills_desc': " 将Python脚本放入 ~/.aibox/skills/ 目录", + 'skills_interface_desc': " 可执行文件需支持 --help 选项,第一行为简介,其余为完整使用说明", + 'skills_reloaded': "✅ 技能已重新加载 ({} 个)", + 'skills_reload_failed': "❌ 技能重载失败: {}", + 'skills_reload_success': "✅ 技能热重载成功,当前共有 {} 个扩展技能", + + # 对话相关 + 'input_prompt': "💬 请输入问题: ", + 'thinking': "🤔 思考中... [{}]", + 'assistant_prefix': " 💬 助手: ", + 'calling_tools': "\n 🔧 需要调用工具...", + 'analyzing_tool_results': "\n 🤔 分析工具结果...", + 'conversation_start': "🎭 对话开始", + 'conversation_ended': "✅ AI保存记忆并结束了对话", + 'conversation_stats': "\n📊 对话结束,总轮数: {}", + 'user_interrupted': "\n\n👋 用户中断", + 'error_prefix': "❌ 错误: ", + + # 工具相关 + 'command_output_title': "\n" + "=" * 50 + "\n📟 命令实时输出:\n" + "=" * 50, + 'command_output_end': "\n" + "=" * 50, + + # 备忘录检查 + 'memo_check_title': "⏰ 检查到期任务...", + 'memo_check_found': "发现 {} 个到期任务:", + 'memo_check_none': "没有到期的任务", + 'memo_list_title': "📋 待执行任务 (共 {} 个):", + 'memo_added': "✅ 任务已添加,ID: {}", + 'memo_add_failed': "❌ 添加失败: {}", + 'memo_completed': "✅ 任务 #{} 已完成", + 'memo_complete_failed': "❌ 无法完成任务 #{}", + + # 即时任务相关 + 'immediate_memo_added': "⚡ 已添加为即时任务并立即检查", + 'immediate_memo_executing': "⚡ 正在执行即时任务: {}", + + # 新对话 + 'new_dialogue_start': "🎭 开始新对话: {}", + + # 记忆相关 + 'memory_check': "习惯性检查相关记忆...", + 'memory_found': "找到 {} 条相关记忆", + + # 配置文件 + 'config_loaded': "📋 已加载配置文件: {}", + 'config_not_found': "📋 配置文件不存在: {},使用默认配置", + 'config_created': "📋 已创建默认配置文件: {}", + + # WebSocket消息类型 + 'ws_connected': "✅ 已连接到AI服务器", + 'ws_disconnected': "❌ 与服务器断开连接", + 'ws_error': "⚠️ WebSocket错误: {}", + 'ws_unknown_command': "❌ 未知命令: {}", + 'ws_command_processed': "✅ 命令已处理", + + # 消息类型 + 'msg_type_chunk': "chunk", + 'msg_type_complete': "complete", + 'msg_type_error': "error", + 'msg_type_command_result': "command_result", + 'msg_type_connection_ack': "connection_ack", + 'msg_type_notification': "notification", + 'msg_type_task_status': "task_status", + 'msg_type_task_executing': "task_executing", + 'msg_type_task_complete': "task_complete", + + # 新增提示词 + 'memory_search_prompt': "【重要习惯】在每次回答用户问题前,请先使用 memory search 工具搜索记忆库中与当前话题相关的记忆,确保回答的连贯性和准确性。", + 'identity_updated': "✅ 用户身份已更新,ID: {}", + 'status_updated': "✅ 智能体状态已更新", + + # 对话结束相关 + 'dialogue_ended': "🔚 对话已结束,请开始新对话", + 'dialogue_force_reset': "🔄 对话状态已重置", + 'dialogue_reset_complete': "✅ 对话状态已完全重置,系统提示词已重新加载", + + # 对话压缩相关(静默执行,不输出信息) + 'conversation_compress': "📦 对话内容压缩", + 'conversation_compressed': "✅ 对话已压缩: 从 {} 条消息压缩为 {} 条", + 'compress_threshold_reached': "⚠️ 对话已达到压缩阈值 ({} 条消息),正在压缩...", + 'compress_token_threshold_reached': "⚠️ 对话token数已达到 {},正在压缩...", + 'compress_failed': "❌ 对话压缩失败: {}", + 'compressed_summary': "【对话摘要】{}", + 'last_n_messages': "【最近消息】{}" + }, + 'en': { + # Welcome messages + 'welcome_title': "🤖 Single AI Agent System - WebSocket Server Started", + 'data_dir': "📁 Data directory: {}", + 'skills_dir': "📁 Skills directory: {}", + 'prompts_dir': "📁 Prompts directory: {}", + 'config_file': "📋 Configuration file: {}", + 'memo_stats': "📋 Task statistics:", + 'total_memos': " 📝 Total tasks: {}", + 'pending_memos': " ⏰ Pending: {}", + 'overdue_memos': " ⚠️ Overdue: {}", + 'completed_memos': " ✅ Completed: {}", + 'loaded_skills': "🔧 Loaded extension skills ({}):", + 'skill_item': " • {}", + 'more_skills': " ... {} more", + 'loaded_prompts': "📝 Loaded prompts ({}):", + 'prompt_item': " • {}", + 'websocket_started': "🌐 WebSocket server started at ws://{}:{}", + 'client_connected': "🔌 Client connected: {}", + 'client_disconnected': "🔌 Client disconnected: {}", + + # Scheduler related + 'scheduler_started': "⏰ Task scheduler started", + 'scheduler_stopped': "⏰ Task scheduler stopped", + 'task_added': "📋 Task added: {} (ID: {})", + 'task_removed': "📋 Task removed: {}", + 'task_executing': "⚙️ Executing task: {} (ID: {})", + 'task_executed': "✅ Task executed successfully: {}", + 'task_failed': "❌ Task execution failed: {} - {}", + 'task_stats': "📊 Task stats: total={}, pending={}, overdue={}, completed={}", + 'check_memos_task': "Check overdue tasks", + 'broadcast_tasks': "📢 Broadcast: {} tasks overdue", + 'next_execution': "⏱️ Next execution: {}", + 'task_completed': "✅ Task completed: {}", + + # Command processing + 'goodbye': "👋 Goodbye!", + 'new_conversation': "🔄 Starting new conversation (history saved)", + 'new_conversation_reset': "🔄 Starting new conversation (history saved, conversation memory reset)", + 'saved_conversations': "📚 Saved conversations:", + 'conv_status_current': "current", + 'conv_status_archived': "archived", + 'recent_memories': "📚 Recent memories:", + 'pending_memos_title': "📋 Pending tasks:", + 'memo_overdue': "⚠️", + 'memo_pending': "⏰", + 'search_results': "🔍 Searching '{}' found {} memories:", + 'current_config': "⚙️ Current configuration:", + 'config_debug': " Debug level: {}", + 'config_data_dir': " Data directory: {}", + 'config_skills_dir': " Skills directory: {}", + 'config_prompts_dir': " Prompts directory: {}", + 'config_commands': " Command execution: {}", + 'config_history': " Load history: {}", + 'config_executor': " Command executor: {}", + 'config_scheduler': " Task scheduler: {}", + 'enabled': "enabled", + 'disabled': "disabled", + 'config_reloaded': "✅ Configuration reloaded", + 'prompts_reloaded': "✅ Prompts reloaded ({})", + 'checking_memos': "🔍 Checking tasks...", + 'overdue_found': "Found {} overdue tasks:", + 'no_overdue': "No overdue tasks", + 'memo_tool_usage': "📝 Task tool usage:", + 'memo_usage_in_dialogue': " You can use memo tool directly in dialogue:", + 'memo_add_example': " - memo add title=\"Task\" content=\"Content\" reminder_time=\"+1h\" repeat_type=\"none\"", + 'memo_list_example': " - memo list", + 'memo_complete_example': " - memo complete memo_id=1", + 'memory_habit': "🧠 Memory habits:", + 'memory_habit_desc': " AI automatically checks relevant memories for conversation continuity", + 'memory_save_desc': " AI automatically saves important memories when conversation ends", + 'command_stream': "⚙️ Command execution real-time output:", + 'command_stream_desc': " run_command tool automatically displays command output in real-time", + 'command_stream_disable': " You can disable real-time output with stream=false parameter", + 'skills_extension': "🔧 Extension skills:", + 'skills_desc': " Put Python scripts in ~/.aibox/skills/ directory", + 'skills_interface_desc': " Executables must support --help; first line is the summary, rest is the full manual", + 'skills_reloaded': "✅ Skills reloaded ({})", + 'skills_reload_failed': "❌ Skills reload failed: {}", + 'skills_reload_success': "✅ Skills hot-reloaded successfully, now {} extension skills available", + + # Dialogue related + 'input_prompt': "💬 Please enter your question: ", + 'thinking': "🤔 Thinking... [{}]", + 'assistant_prefix': " 💬 Assistant: ", + 'calling_tools': "\n 🔧 Need to call tools...", + 'analyzing_tool_results': "\n 🤔 Analyzing tool results...", + 'conversation_start': "🎭 Conversation started", + 'conversation_ended': "✅ AI saved memory and ended conversation", + 'conversation_stats': "\n📊 Conversation ended, total turns: {}", + 'user_interrupted': "\n\n👋 User interrupted", + 'error_prefix': "❌ Error: ", + + # Tool related + 'command_output_title': "\n" + "=" * 50 + "\n📟 Command real-time output:\n" + "=" * 50, + 'command_output_end': "\n" + "=" * 50, + + # Memo check + 'memo_check_title': "⏰ Checking overdue tasks...", + 'memo_check_found': "Found {} overdue tasks:", + 'memo_check_none': "No overdue tasks", + 'memo_list_title': "📋 Pending tasks (total {}):", + 'memo_added': "✅ Task added, ID: {}", + 'memo_add_failed': "❌ Failed to add: {}", + 'memo_completed': "✅ Task #{} completed", + 'memo_complete_failed': "❌ Cannot complete task #{}", + + # Immediate task related + 'immediate_memo_added': "⚡ Added as immediate task and checked now", + 'immediate_memo_executing': "⚡ Executing immediate task: {}", + + # New dialogue + 'new_dialogue_start': "🎭 Starting new dialogue: {}", + + # Memory related + 'memory_check': "Habitually checking relevant memories...", + 'memory_found': "Found {} relevant memories", + + # Configuration file + 'config_loaded': "📋 Configuration file loaded: {}", + 'config_not_found': "📋 Configuration file not found: {}, using default configuration", + 'config_created': "📋 Default configuration file created: {}", + + # WebSocket message types + 'ws_connected': "✅ Connected to AI server", + 'ws_disconnected': "❌ Disconnected from server", + 'ws_error': "⚠️ WebSocket error: {}", + 'ws_unknown_command': "❌ Unknown command: {}", + 'ws_command_processed': "✅ Command processed", + + # Message types + 'msg_type_chunk': "chunk", + 'msg_type_complete': "complete", + 'msg_type_error': "error", + 'msg_type_command_result': "command_result", + 'msg_type_connection_ack': "connection_ack", + 'msg_type_notification': "notification", + 'msg_type_task_status': "task_status", + 'msg_type_task_executing': "task_executing", + 'msg_type_task_complete': "task_complete", + + # New prompts + 'memory_search_prompt': "【Important Habit】Before answering each user question, please use the memory search tool to search the memory database for information related to the current topic, ensuring coherence and accuracy of responses.", + 'identity_updated': "✅ User identity updated, ID: {}", + 'status_updated': "✅ Agent status updated", + + # Dialogue end related + 'dialogue_ended': "🔚 Dialogue ended, please start a new one", + 'dialogue_force_reset': "🔄 Dialogue state reset", + 'dialogue_reset_complete': "✅ Dialogue state fully reset, system prompts reloaded", + + # Conversation compression related (silent execution) + 'conversation_compress': "📦 Conversation compression", + 'conversation_compressed': "✅ Conversation compressed: from {} messages to {}", + 'compress_threshold_reached': "⚠️ Conversation reached compression threshold ({} messages), compressing...", + 'compress_token_threshold_reached': "⚠️ Conversation token count reached {}, compressing...", + 'compress_failed': "❌ Conversation compression failed: {}", + 'compressed_summary': "【Conversation Summary】{}", + 'last_n_messages': "【Recent Messages】{}" + } + } + + def get(self, key: str, *args) -> str: + """获取指定语言的字符串,支持格式化参数""" + if key in self._strings[self._lang]: + text = self._strings[self._lang][key] + if args: + return text.format(*args) + return text + if key in self._strings['zh']: + text = self._strings['zh'][key] + if args: + return text.format(*args) + return text + return key + + @property + def lang(self) -> str: + return self._lang + + + +# 创建全局多语言实例 +i18n = I18n() diff --git a/aibox/common/logger.py b/aibox/common/logger.py new file mode 100644 index 0000000..5eabd46 --- /dev/null +++ b/aibox/common/logger.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +""" +common.logger - aibox 统一日志系统 + +抽离自 aibox.py 的 Logger 类。依赖 common.constants(日志文件路径与大小限制) +与 common.config,不反向依赖 aibox 任何符号,可安全独立导入。 +""" + +import os +import logging +from logging.handlers import RotatingFileHandler +from datetime import datetime + +from .constants import constants +from .config import ConfigManager +# ==================== 日志系统 ==================== + +class Logger: + """统一日志系统 - 支持文件和终端输出""" + + LEVELS = { + "DEBUG": 3, + "INFO": 2, + "WARNING": 1, + "ERROR": 0 + } + + _file_handler = None + _log_initialized = False + + @classmethod + def init_file_logging(cls, log_file: str = None, max_size: int = None, backup_count: int = None): + """初始化文件日志""" + if cls._log_initialized: + return + + if log_file is None: + log_file = constants.LOG_FILE + if max_size is None: + max_size = constants.LOG_MAX_SIZE + if backup_count is None: + backup_count = constants.LOG_BACKUP_COUNT + + log_dir = os.path.dirname(log_file) + if log_dir: + os.makedirs(log_dir, exist_ok=True) + + cls._file_handler = RotatingFileHandler( + log_file, + maxBytes=max_size, + backupCount=backup_count, + encoding='utf-8' + ) + + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + cls._file_handler.setFormatter(formatter) + + cls._log_initialized = True + + def __init__(self, name: str = "System", level: int = None): + self.name = name + config = ConfigManager() + if level is None: + level = config.get("system.debug_level", 1) + self.level = level + + self.py_logger = logging.getLogger(f"AI.{name}") + self.py_logger.setLevel(logging.DEBUG) + + if Logger._file_handler and not self.py_logger.handlers: + self.py_logger.addHandler(Logger._file_handler) + + def debug(self, msg: str): + if self.level >= 3: + self._log("🐛", "DEBUG", msg) + self.py_logger.debug(msg) + + def info(self, msg: str): + if self.level >= 2: + self._log("📋", "INFO", msg) + self.py_logger.info(msg) + + def warning(self, msg: str): + if self.level >= 1: + self._log("⚠️", "WARNING", msg) + self.py_logger.warning(msg) + + def error(self, msg: str): + if self.level >= 0: + self._log("❌", "ERROR", msg) + self.py_logger.error(msg) + + def _log(self, emoji: str, level: str, msg: str): + """输出到终端""" + timestamp = datetime.now().strftime("%H:%M:%S") + print(f"{emoji} [{timestamp}] [{self.name}] {msg}", flush=True) + diff --git a/aibox/requirements.txt b/aibox/requirements.txt index 47836fe..e8d40cf 100644 --- a/aibox/requirements.txt +++ b/aibox/requirements.txt @@ -1,6 +1,7 @@ # backend websockets requests +PySocks # frontend websockets>=11.0