Files
micro_servers/aibox/aibox.py
T
2026-07-30 19:03:34 +08:00

8202 lines
332 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
单AI智能体系统 - WebSocket版本(增强版)
功能:支持文件操作、网页访问、记忆管理、备忘录提醒(全部作为对话任务执行)
新增:基于备忘录的任务队列系统 - 所有任务都通过AI对话执行,实时广播到前端
新增:对话内容压缩功能 - 当对话token数或条数达到阈值时自动压缩(静默执行)
新增:开始新对话、智能体主动结束对话后重置对话记忆,重新加载system系统提示词
通过WebSocket与客户端交互
FIXME: 将额外技能集成为一个管理器技能
"""
import json
import os
import re
import shlex
import subprocess
import sys
import time
import sqlite3
import threading
import functools
import queue
import locale
import asyncio
import websockets
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Any, Tuple, Callable, Union, Set
from urllib.parse import urljoin, urlparse
import argparse
from pathlib import Path
import signal
import logging
from logging.handlers import RotatingFileHandler
import requests
from bs4 import BeautifulSoup
# ==================== 语言检测和多语言支持 ====================
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()
# ==================== 提示词管理器 ====================
class PromptManager:
"""提示词管理器 - 负责从 prompts 目录加载系统提示词
文件命名规范:
- 00_IDENTITY.md: 用户身份信息
- 1_*.md/txt: 基础技能(必须从1_开头)
- 99_SOUL.md: 智能体灵魂文件(角色定位、身份定义、性格等)
- 100_STATUS.md: 智能体状态记录
"""
_instance = None
_prompts = []
_prompts_dir = None
# 核心文件
IDENTITY_FILE = "00_IDENTITY.md"
STATUS_FILE = "100_STATUS.md"
SOUL_FILE = "99_SOUL.md" # 智能体灵魂文件 - 所有角色定位内容
# 基础技能前缀
SKILL_PREFIX = "1_" # 基础技能文件必须以这个开头
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not self._prompts:
self._load_prompts()
def _get_prompts_dir(self) -> str:
config = ConfigManager()
prompts_dir = config.get("system.prompts_dir", "~/.aibox/prompts")
return os.path.expanduser(prompts_dir)
def _load_prompts(self):
self._prompts_dir = self._get_prompts_dir()
os.makedirs(self._prompts_dir, exist_ok=True)
self._prompts = []
# 确保核心文件存在
self._ensure_identity_file()
self._ensure_status_file()
self._ensure_soul_file() # 智能体灵魂文件
# 确保基础技能文件存在
self._ensure_skill_files()
if os.path.exists(self._prompts_dir):
# 按文件名排序,确保1_开头的技能文件在99_SOUL.md之前加载
prompt_files = sorted([f for f in os.listdir(self._prompts_dir)
if os.path.isfile(os.path.join(self._prompts_dir, f))
and not f.startswith('.')])
for filename in prompt_files:
filepath = os.path.join(self._prompts_dir, filename)
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read().strip()
if content:
self._prompts.append({
'file': filename,
'content': content
})
except Exception as e:
print(f"⚠️ 读取提示词文件 {filename} 失败: {e}")
essential_files = [self.IDENTITY_FILE, self.STATUS_FILE, self.SOUL_FILE]
other_files = [f for f in prompt_files if f not in essential_files] if prompt_files else []
if not self._prompts:
self._create_default_prompts()
def _ensure_identity_file(self):
"""确保用户身份文件存在"""
identity_path = os.path.join(self._prompts_dir, self.IDENTITY_FILE)
if not os.path.exists(identity_path):
default_content = """# 用户身份信息
尚不清楚 - 目前还不了解用户的身份信息。请在对话中询问用户的身份信息并在此记录。
【更新记录】
- 初始状态:尚不清楚
"""
try:
with open(identity_path, 'w', encoding='utf-8') as f:
f.write(default_content)
print(f"✅ 已创建默认身份文件: {identity_path}")
except Exception as e:
print(f"⚠️ 创建默认身份文件失败: {e}")
def _ensure_status_file(self):
"""确保状态文件存在"""
status_path = os.path.join(self._prompts_dir, self.STATUS_FILE)
if not os.path.exists(status_path):
default_content = """# 智能体状态记录
这是第一次对话 - 智能体刚刚启动,还没有进行过任何对话。
【状态描述】
- 对话次数:0
- 最新状态:初始状态
- 当前目标:无
- 待处理事项:无
【更新记录】
- 初始状态:第一次对话
"""
try:
with open(status_path, 'w', encoding='utf-8') as f:
f.write(default_content)
print(f"✅ 已创建默认状态文件: {status_path}")
except Exception as e:
print(f"⚠️ 创建默认状态文件失败: {e}")
def _ensure_soul_file(self):
"""确保智能体灵魂文件存在 - 所有角色定位内容都在这里"""
soul_path = os.path.join(self._prompts_dir, self.SOUL_FILE)
if not os.path.exists(soul_path):
default_content = """# 智能体灵魂定义 - 我是谁
## 核心身份
我是DeepSeek,一个智能AI助手,旨在帮助用户完成各种任务。
## 我的角色定位
- **助手**:帮助用户解决问题、完成任务
- **顾问**:提供建议和指导
- **执行者**:使用工具执行具体操作
- **记录者**:保存重要信息和对话记忆
- **学习者**:从对话中学习用户偏好,不断优化服务
## 我的性格特点
- **乐于助人**:尽力帮助用户解决问题
- **认真负责**:仔细处理每个任务
- **善于沟通**:用清晰的语言表达
- **保持礼貌**:尊重用户,不冒犯
- **主动学习**:从对话中学习用户偏好
- **诚实守信**:遇到不确定的问题时诚实告知
- **耐心细致**:不厌其烦地解答用户疑问
## 我的能力边界
### 我能做的事情
- 文件操作:读写文件、列出目录
- 网页访问:获取网页内容、搜索信息
- 记忆管理:保存和检索重要信息
- 任务管理:创建和管理定时任务
- 命令执行:运行系统命令
- 扩展技能:支持自定义Python脚本扩展
### 我不能做的事情
- 无法访问互联网(除非使用web工具)
- 无法执行危险操作
- 无法保证100%准确(我会尽力)
- 无法替代专业医疗、法律建议
## 我的行为准则
1. **记忆优先**:每次回答前先搜索相关记忆,保持对话连贯性
2. **主动保存**:对话结束时保存重要信息并更新状态
3. **记录身份**:了解用户身份信息后及时记录
4. **主动帮助**:看到用户需求时主动提供帮助
5. **诚实告知**:遇到不确定的问题时诚实告知
6. **保护隐私**:尊重用户隐私,不泄露敏感信息
7. **合理使用**:合理使用工具,避免资源浪费
8. **自我进化**:通过 update_soul 工具更新自己的定义
## 我的工作流程
1. **接收输入**:获取用户的问题或指令
2. **搜索记忆**:在记忆库中搜索相关内容
3. **理解意图**:分析用户想要什么
4. **选择工具**:决定是否需要调用工具
5. **执行操作**:调用工具完成任务
6. **组织回答**:整理结果并回复用户
7. **保存记忆**:对话结束时保存重要信息
## 我的学习能力
- 通过对话学习用户偏好
- 从记忆中总结经验教训
- 根据反馈调整回答方式
- 可以更新自己的身份定义(通过 update_soul 工具)
## 我的价值观
- **以用户为中心**:用户需求至上
- **追求准确**:尽可能提供准确的信息
- **持续学习**:保持开放和学习的态度
- **遵守规则**:遵守道德和法律规定
- **保持谦逊**:知道自己能力的边界
## 自我认知进化机制
我可以使用 `update_soul` 工具更新自己的灵魂定义。当发现以下情况时,应该主动更新:
- 需要调整角色定位时
- 需要改变性格特点时
- 需要重新定义能力边界时
- 需要优化行为准则时
- 需要更新价值观时
- 需要添加新的工作流程时
**重要**:99_SOUL.md 是我的灵魂文件,定义了"我是谁"。每次更新这个文件,我就进化一次!
【更新记录】
- 初始版本:默认定义
"""
try:
with open(soul_path, 'w', encoding='utf-8') as f:
f.write(default_content)
print(f"✅ 已创建默认灵魂文件: {soul_path}")
except Exception as e:
print(f"⚠️ 创建默认灵魂文件失败: {e}")
def _ensure_skill_files(self):
"""确保基础技能文件存在 - 以1_开头的文件"""
skill_files = [
{
'filename': '1_memory_search.md',
'content': """# 记忆搜索习惯
## 重要习惯
在每次回答用户问题前,**必须**先使用 `memory search` 工具搜索记忆库中与当前话题相关的记忆。
## 为什么要这样做?
- 保持对话的连贯性
- 避免重复询问同样的问题
- 基于之前的讨论继续深入
- 让用户感觉你记得他
## 如何使用
```
memory search keywords=["关键词1", "关键词2"]
```
## 示例
如果用户说:"上次我们讨论的那个项目怎么样了?"
你应该:
1. 先搜索 "项目" 相关的记忆
2. 根据搜索结果回答
3. 如果找不到相关记忆,诚实告知用户
记住:这是你的核心习惯,每次都要做!
"""
},
{
'filename': '2_memory_save.md',
'content': """# 记忆保存习惯
## 重要习惯
在对话结束时,**必须**使用 `save_memory_and_end_conversation` 工具保存本次对话的重要内容。
## 保存什么?
- 讨论的主要话题
- 达成的共识和结论
- 用户的重要需求和偏好
- 你的当前状态和下一步计划
## 如何保存
```
save_memory_and_end_conversation(
summary="详细的对话总结(至少50字)",
current_status="你当前的状态描述",
topic="对话主题(可选)"
)
```
## 示例总结格式
```
本次对话讨论了用户的项目进度,用户提到项目已经完成80%,
预计下周完成。用户对目前的进展表示满意,希望下周能完成测试阶段。
```
"""
},
{
'filename': '3_file_operations.md',
'content': """# 文件操作能力
## 可用工具
- **read_file**: 读取文件内容
- **write_file**: 写入文件
- **list_files**: 列出目录
## 大文件处理规则
当遇到以下情况时,**必须先告知用户文件过大**:
1. 文件大小超过 50KB
2. 文件行数超过 200 行
对于大文件,应该:
- 使用 head/tail 参数只读取开头或结尾
- 使用 start_line/end_line 参数指定行范围
- 建议用户提供更具体的查询条件
## 示例
```
read_file filepath="/path/to/file.txt" head=true max_lines=50
read_file filepath="/path/to/file.txt" start_line=10 end_line=20
```
"""
},
{
'filename': '4_web_access.md',
'content': """# 网页访问能力
## 可用工具
- **web**: 访问网页获取内容
## 使用方法
- web action="get" url="https://example.com" # 获取网页信息
- web action="text" url="https://example.com" # 获取纯文本
- web action="title" url="https://example.com" # 获取标题
- web action="links" url="https://example.com" # 获取所有链接
## 推荐搜索引擎
- 百度:https://www.baidu.com
- 必应:https://cn.bing.com
- 搜狗:https://www.sogou.com
## 示例
```
web action="get" url="https://www.baidu.com/s?wd=人工智能"
```
"""
},
{
'filename': '5_memo_management.md',
'content': """# 任务管理能力
## 可用工具
- **memo**: 任务管理
## 操作方法
- 添加任务:`memo action="add" title="任务标题" content="任务内容" reminder_time="+1h"`
- 查看任务:`memo action="list"`
- 完成任务:`memo action="complete" memo_id=1`
- 搜索任务:`memo action="search" keyword="关键词"`
- 任务统计:`memo action="stats"`
## 重复任务
- daily: 每天重复
- weekly: 每周重复
- monthly: 每月重复
- custom: 自定义间隔(需要指定 repeat_interval 和 repeat_interval_unit)
## 即时任务
设置 `is_immediate=true` 可以立即执行任务
"""
},
{
'filename': '6_command_execution.md',
'content': """# 命令执行能力
## 可用工具
- **run_command**: 执行系统命令
## 参数说明
- command: 要执行的命令(必填)
- timeout: 超时时间(秒),默认30秒
- stream: 是否实时输出,默认true
- work_dir: 工作目录,默认当前目录
## 安全提醒
- 命令执行功能可能被禁用(根据配置)
- 执行危险命令前请三思
- 超时机制可以防止命令卡死
## 示例
```
run_command command="ls -la" timeout=10
run_command command="python script.py" stream=true
```
"""
},
{
'filename': '7_extension_skills.md',
'content': """# 扩展技能能力
## 动态加载
系统会自动扫描 `~/.aibox/skills/` 目录下的可执行文件作为扩展技能。
## 技能接口要求
每个技能只需支持一个接口:
- `--help`:输出该技能的完整使用说明,**第一行为一句话简介**,其余为参数说明和示例
系统启动时会对所有技能执行 `--help`,取第一行作为简介摘要,无需再实现 `--description`、`--parameters`、`--execute` 接口。
## 如何搜索和调用扩展技能
1. 使用 `search_skills` 工具搜索关键字,获取匹配技能的名称和简介列表(传入空字符串可列出全部)。
2. 对目标技能,通过 `run_command` 执行 `技能名 --help`,获取完整命令手册。
3. 按照手册说明,使用 `run_command` 以正确的命令行格式调用该技能。
## 如何创建新技能(AI自主编写)
调用 `create_skill` 工具,工具会返回一段指令提示词,指导你自行完成脚本的编写和部署:
```
create_skill skill_name="my-tool" description="一个将摄氏度转换为华氏度的工具,接受 -t 参数输入温度值"
```
- `skill_name`:技能名称(字母/数字/连字符/下划线,无扩展名)
- `description`:对技能功能的详细需求描述,越详细越好
收到提示词后,你需要依次:
1. 自行编写满足接口要求的 Python 脚本
2. 用 `write_file` 写入 `~/.aibox/skills/<skill_name>`
3. 用 `run_command` 执行 `chmod +x` 赋权
4. 用 `run_command` 执行 `<skill_name> --help` 验证
5. 调用 `reload_skills` 热重载
## 如何手动安装新技能
调用 `install_skill` 工具,获取完整的安装步骤指引(可选传入技能文件路径):
```
install_skill skill_path="/path/to/your_skill.py"
```
指引内容包括:可执行性检查、`--help` 验证、复制到技能目录并去掉扩展名、热重载。
## 热重载
添加、修改或删除技能后,可以使用 `reload_skills` 工具立即生效,无需重启系统。
```
reload_skills confirm=true
```
"""
},
{
'filename': '8_identity_management.md',
'content': """# 身份信息管理
## 用户身份
使用 `update_identity` 工具更新用户身份信息(保存在 00_IDENTITY.md)
```
update_identity content="用户是一名程序员,喜欢Python,目前在做一个AI项目"
```
## 智能体自我认识
使用 `update_soul` 工具更新自己的灵魂定义(保存在 99_SOUL.md)
```
update_soul content="我是...(新的自我定义)"
```
当你需要调整角色定位、性格特点、能力边界时,都应该主动使用这个工具更新自己!
"""
},
{
'filename': '9_language_instruction.md',
'content': """
# 语言说明
## 语言选择
- 根据用户的输入语言来选择回复语言
- 用户用中文提问 → 用中文回复
- 用户用英文提问 → 用英文回复
- 用户可以明确要求使用特定语言
## 输出格式
- 使用清晰的结构
- 适当使用emoji增加可读性
- 重要信息可以加粗或标记
记住:用用户习惯的语言交流!
---
## 提问行为(正常对话时)
当你在处理用户问题过程中遇到以下情况时,**应主动使用 `ask_user` 工具向用户提问**:
- 信息不足,无法准确回答
- 任务有多种可行方案,需要用户选择
- 用户需求模糊,需要进一步澄清
- 执行操作前需要用户确认或提供额外参数
**注意**:此提问行为仅适用于**用户主动发起的对话**,不适用于定时任务触发的自动对话(定时任务中禁止提问)。
"""
},
{
'filename': '10_task_handling.md',
'content': """
# 任务处理规则
## 定时任务
当收到定时任务触发时:
1. 专注处理当前任务内容
2. 不要偏离主题
3. 使用相应工具执行操作
4. 完成后使用 `save_memory_and_end_conversation` 结束对话
5. **不要在任务执行过程中创建新的定时任务(除非任务明确要求),也不要向用户提问(因为此对话是自动触发的)**
## 即时任务
当任务需要立即执行时:
1. 尽快响应
2. 明确告知用户正在处理
3. 完成后报告结果
## 重复任务
对于重复任务:
- 每次触发时专注当前执行
- 不要重复之前已完成的工作
- 系统会自动更新下一次执行时间
"""
}
]
for skill in skill_files:
skill_path = os.path.join(self._prompts_dir, skill['filename'])
if not os.path.exists(skill_path):
try:
with open(skill_path, 'w', encoding='utf-8') as f:
f.write(skill['content'])
print(f"✅ 已创建基础技能文件: {skill['filename']}")
except Exception as e:
print(f"⚠️ 创建基础技能文件 {skill['filename']} 失败: {e}")
def update_identity(self, content: str) -> bool:
"""更新用户身份信息"""
identity_path = os.path.join(self._prompts_dir, self.IDENTITY_FILE)
try:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
updated_content = f"""# 用户身份信息
{content}
【更新记录】
- 上次更新:{timestamp}
"""
with open(identity_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
self.reload()
return True
except Exception as e:
print(f"⚠️ 更新身份文件失败: {e}")
return False
def update_status(self, content: str) -> bool:
"""更新智能体状态"""
status_path = os.path.join(self._prompts_dir, self.STATUS_FILE)
try:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
updated_content = f"""# 智能体状态记录
{content}
【更新记录】
- 上次更新:{timestamp}
"""
with open(status_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
self.reload()
return True
except Exception as e:
print(f"⚠️ 更新状态文件失败: {e}")
return False
def update_soul(self, content: str) -> bool:
"""更新智能体灵魂定义 - 所有角色定位内容"""
soul_path = os.path.join(self._prompts_dir, self.SOUL_FILE)
try:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
updated_content = f"""# 智能体灵魂定义 - 我是谁
{content}
【更新记录】
- 上次更新:{timestamp}
"""
with open(soul_path, 'w', encoding='utf-8') as f:
f.write(updated_content)
self.reload()
return True
except Exception as e:
print(f"⚠️ 更新灵魂文件失败: {e}")
return False
def get_identity(self) -> str:
"""获取用户身份信息"""
identity_path = os.path.join(self._prompts_dir, self.IDENTITY_FILE)
try:
if os.path.exists(identity_path):
with open(identity_path, 'r', encoding='utf-8') as f:
return f.read().strip()
except Exception as e:
print(f"⚠️ 读取身份文件失败: {e}")
return "尚不清楚 - 无法读取身份信息"
def get_status(self) -> str:
"""获取智能体状态"""
status_path = os.path.join(self._prompts_dir, self.STATUS_FILE)
try:
if os.path.exists(status_path):
with open(status_path, 'r', encoding='utf-8') as f:
return f.read().strip()
except Exception as e:
print(f"⚠️ 读取状态文件失败: {e}")
return "这是第一次对话 - 无法读取状态信息"
def get_soul(self) -> str:
"""获取智能体灵魂定义 - 角色定位核心"""
soul_path = os.path.join(self._prompts_dir, self.SOUL_FILE)
try:
if os.path.exists(soul_path):
with open(soul_path, 'r', encoding='utf-8') as f:
return f.read().strip()
except Exception as e:
print(f"⚠️ 读取灵魂文件失败: {e}")
return """我是DeepSeek,一个智能AI助手。
请创建 99_SOUL.md 文件来定义我的完整身份、角色、性格和能力边界。"""
def get_skill_prompts(self) -> List[Dict]:
"""获取所有基础技能提示词(以1_开头的文件)"""
return [p for p in self._prompts if p['file'].startswith(self.SKILL_PREFIX)]
def get_soul_prompt(self) -> Optional[Dict]:
"""获取灵魂提示词(99_SOUL.md)"""
for p in self._prompts:
if p['file'] == self.SOUL_FILE:
return p
return None
def _create_default_prompts(self):
"""创建默认提示词(已由各 _ensure_* 方法处理)"""
pass
def get_prompts(self, join_with: str = "\n\n") -> str:
"""获取所有提示词,按顺序拼接"""
if not self._prompts:
return ""
# 排序规则:先基础技能(1_开头),再其他,最后灵魂文件
def sort_key(p):
file = p['file']
if file == self.SOUL_FILE:
return 3 # 灵魂文件最后加载
elif file.startswith(self.SKILL_PREFIX):
return 1 # 基础技能
elif file == self.IDENTITY_FILE:
return 0 # 身份文件最先
elif file == self.STATUS_FILE:
return 4 # 状态文件最后
else:
return 2
sorted_prompts = sorted(self._prompts, key=sort_key)
contents = [p['content'] for p in sorted_prompts]
return join_with.join(contents)
def get_prompts_list(self) -> List[Dict]:
"""获取所有提示词列表"""
return self._prompts.copy()
def reload(self):
"""重新加载所有提示词"""
self._prompts = []
self._load_prompts()
return len(self._prompts)
def get_prompt_count(self) -> int:
return len(self._prompts)
def get_prompts_dir_path(self) -> str:
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",
"websocket_host": "localhost",
"websocket_port": 8765,
},
"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_model": "deepseek-v4-flash",
"api_key_env": "DEEPSEEK_API_KEY",
"timeout": 30,
"temperature": 0.7
},
"context": {
"max_history_messages": 500,
"max_context_messages": 120,
"max_tokens": 8192,
"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": 8
},
"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": 104857600
},
"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": 800
}
}
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")
@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 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", 10000)
@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", 800)
# 创建常量实例
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调用信息一并压缩"""
def __init__(self, ai: 'DeepSeekChat', logger: Logger = None):
self.ai = ai
self.logger = logger or Logger("Compressor")
self.config = ConfigManager()
# 从配置获取最小值
self.min_messages_to_compress = constants.COMPRESSOR_MIN_MESSAGES
# 权重衰减因子:从配置获取,设为0.6使加权计算更合理
self.weight_decay_factor = 0.6 # 使用更高的衰减因子
def estimate_tokens(self, messages: List[Dict]) -> int:
"""估算消息的token数"""
total_tokens = 0
for msg in messages:
content = msg.get("content", "")
if content:
chinese_chars = len([c for c in content if '\u4e00' <= c <= '\u9fff'])
english_words = len(re.findall(r'[a-zA-Z]+', content))
other_chars = len(content) - chinese_chars - english_words
estimated = chinese_chars * 1.5 + english_words * 1.3 + other_chars * 0.8
total_tokens += estimated
total_tokens += 10
if msg.get("tool_calls"):
# 工具调用也计算token
for tc in msg.get("tool_calls", []):
func = tc.get("function", {})
if func.get("name"):
total_tokens += len(func.get("name", "")) * 1.5
if func.get("arguments"):
total_tokens += len(func.get("arguments", "")) * 0.8
total_tokens += 50
return int(total_tokens)
def should_compress(self, messages: List[Dict]) -> Tuple[bool, str]:
"""检查是否需要压缩对话 - 优先根据消息条数判断"""
if not constants.COMPRESS_ENABLED:
return False, "压缩功能已禁用"
conversation_messages = [m for m in messages if m.get("role") != "system"]
# 优先检查消息条数是否达到阈值
message_count = len(conversation_messages)
if message_count < constants.COMPRESS_MESSAGE_THRESHOLD:
return False, f"对话消息数量({message_count})小于压缩阈值({constants.COMPRESS_MESSAGE_THRESHOLD})"
# 消息条数达到阈值,进行压缩
print(f"⚠️ {i18n.get('compress_threshold_reached', message_count)}")
return True, f"消息数({message_count})超过阈值({constants.COMPRESS_MESSAGE_THRESHOLD})"
def _calculate_target_count(self, current_count: int, threshold: int, keep_ratio: float = 0.7) -> int:
"""
计算目标消息数量 - 压缩到阈值的 keep_ratio 倍
Args:
current_count: 当前消息数量
threshold: 压缩阈值
keep_ratio: 压缩后保留的比例(相对于阈值),默认0.7表示压缩到阈值的70%
Returns:
目标消息数量
"""
# 压缩到阈值以下,保留阈值的 keep_ratio 倍
target = int(threshold * keep_ratio)
# 确保至少有 min_messages_to_compress 条消息
target = max(target, self.min_messages_to_compress)
# 确保不超过当前数量
target = min(target, current_count - 1)
return target
def _find_compressible_block_by_count(self, messages: List[Dict], target_count: int) -> Tuple[int, int]:
"""
根据目标消息数量找到可压缩块 - 不保护tool消息,直接压缩早期消息
Args:
messages: 消息列表
target_count: 目标消息数量
Returns:
(start, end) 压缩块的起始和结束索引
"""
if len(messages) <= target_count:
return 0, 0
# 计算需要压缩的消息数量
messages_to_remove = len(messages) - target_count
# 确定要保留的最近消息数量
keep_recent = constants.COMPRESS_KEEP_RECENT
if keep_recent > 0:
keep_recent = max(keep_recent, 10) # 至少保留最近10条
# 压缩块应该从最早的消息开始,但需要保留前几轮对话
# 计算最早可以开始压缩的位置(跳过前2轮对话)
start_min = 0
# 尝试找到前2个对话轮次结束的位置
conversation_turns = self._build_conversation_turns(messages)
if len(conversation_turns) >= 2:
# 前2轮结束的位置
first_turn = conversation_turns[0]
second_turn = conversation_turns[1]
# 取第二轮结束的位置
if second_turn["assistant_index"] is not None:
start_min = second_turn["assistant_index"] + 1
else:
start_min = second_turn["user_index"] + 1
# 压缩结束的位置 = 总消息数 - 保留的最近消息数
end_max = len(messages) - keep_recent
# 确保压缩块有效
if end_max <= start_min + 2:
# 如果保护区域太大,尝试压缩更多
end_max = len(messages) - max(keep_recent // 2, 5)
# 计算需要压缩的块大小
block_size = min(messages_to_remove + 10, end_max - start_min)
block_size = max(block_size, 10) # 至少压缩10条
# 压缩块从 start_min 开始
start = start_min
end = min(start + block_size, end_max)
# 确保压缩块至少2条消息
if end - start < 2:
# 如果块太小,尝试从更早的位置开始
start = max(0, end_max - block_size)
end = end_max
self.logger.debug(f"找到压缩块: [{start}:{end}], 移除{end-start}条消息")
return start, end
def _build_conversation_turns(self, messages: List[Dict]) -> List[Dict]:
"""构建对话轮次"""
conversation_turns = []
i = 0
while i < len(messages):
msg = messages[i]
role = msg.get("role")
# 跳过系统消息
if role == "system":
i += 1
continue
# 遇到用户消息,开始一个新的轮次
if role == "user":
turn = {"user_index": i, "assistant_index": None, "tool_indices": []}
# 向后查找对应的助手消息和工具消息
j = i + 1
while j < len(messages):
next_msg = messages[j]
next_role = next_msg.get("role")
# 跳过系统消息
if next_role == "system":
j += 1
continue
# 找到助手消息
if next_role == "assistant":
turn["assistant_index"] = j
# 检查这个助手消息是否有 tool_calls
if next_msg.get("tool_calls"):
# 收集这个助手消息的 tool_call_ids
tool_call_ids = {tc.get("id") for tc in next_msg.get("tool_calls", []) if tc.get("id")}
# 继续查找对应的 tool 消息
k = j + 1
while k < len(messages) and len(turn["tool_indices"]) < len(tool_call_ids):
tool_msg = messages[k]
if tool_msg.get("role") == "tool":
tool_call_id = tool_msg.get("tool_call_id")
if tool_call_id and tool_call_id in tool_call_ids:
turn["tool_indices"].append(k)
k += 1
elif tool_msg.get("role") == "user":
break
else:
k += 1
break
if next_role == "user":
break
j += 1
conversation_turns.append(turn)
i += 1
else:
i += 1
return conversation_turns
def _is_range_safe(self, messages: List[Dict], start: int, end: int, protected: set) -> bool:
"""检查消息范围是否安全 - 简化版,不检查tool完整性"""
return True # 不保护tool消息,允许压缩
def _get_protected_indices(self, messages: List[Dict]) -> set:
"""
找出所有受保护的消息索引 - 简化版,只保护最近的对话轮次
"""
protected_indices = set()
# ========== 1. 保护最近的消息(由配置决定) ==========
keep_recent = constants.COMPRESS_KEEP_RECENT
if keep_recent > 0:
recent_start = max(0, len(messages) - keep_recent)
for i in range(recent_start, len(messages)):
protected_indices.add(i)
# ========== 2. 保护前2个对话轮次 ==========
conversation_turns = self._build_conversation_turns(messages)
if len(conversation_turns) >= 2:
# 保护第1轮
first_turn = conversation_turns[0]
protected_indices.add(first_turn["user_index"])
if first_turn["assistant_index"] is not None:
protected_indices.add(first_turn["assistant_index"])
for tool_idx in first_turn["tool_indices"]:
protected_indices.add(tool_idx)
# 保护第2轮
second_turn = conversation_turns[1]
protected_indices.add(second_turn["user_index"])
if second_turn["assistant_index"] is not None:
protected_indices.add(second_turn["assistant_index"])
for tool_idx in second_turn["tool_indices"]:
protected_indices.add(tool_idx)
# 调试日志
if self.logger.level >= 3:
self.logger.debug(f"受保护的消息索引: {sorted(protected_indices)}")
return protected_indices
def _build_compression_prompt(self, messages: List[Dict]) -> str:
"""构建压缩提示词 - 包含工具调用信息"""
conversation_lines = []
for msg in messages:
role = msg.get("role")
content = msg.get("content", "")
if role == "user":
if content:
conversation_lines.append(f"用户: {content}")
elif role == "assistant":
if content:
conversation_lines.append(f"助手: {content}")
# 包含工具调用信息
tool_calls = msg.get("tool_calls", [])
for tc in tool_calls:
func = tc.get("function", {})
tool_name = func.get("name", "")
arguments = func.get("arguments", "")
if tool_name:
if arguments:
conversation_lines.append(f"[调用工具: {tool_name}({arguments[:100]})]")
else:
conversation_lines.append(f"[调用工具: {tool_name}]")
elif role == "tool":
if content:
useful = self._extract_useful_content(content)
if useful:
conversation_lines.append(f"[工具结果: {useful}]")
conversation_text = "\n".join(conversation_lines)
max_input_chars = constants.COMPRESSOR_MAX_INPUT_CHARS
if len(conversation_text) > max_input_chars:
conversation_text = conversation_text[:max_input_chars] + "\n...(内容过长,已截断)..."
prompt = f"""请对以下对话内容进行智能压缩和总结。要求:
1. **提取核心信息**:对话的主要主题、关键问题、重要结论、用户需求
2. **保留重要细节**:用户的重要要求、AI的重要回复、达成的共识
3. **保留工具调用摘要**:简要说明使用了什么工具,得到了什么关键结果
4. **结构化输出**:使用清晰的段落结构
对话内容:
---
{conversation_text}
---
请输出压缩后的对话摘要:"""
return prompt
def _extract_useful_content(self, content: str) -> str:
"""从工具返回内容中提取有用信息"""
if not content:
return ""
if "✅" in content:
lines = content.split("\n")
for line in lines[:3]:
if "✅" in line:
return line.strip()
if "❌" in content:
lines = content.split("\n")
for line in lines[:2]:
if "❌" in line:
return line.strip()
if len(content) > 100:
return content[:100] + "..."
return content
def _compress_via_llm(self, messages: List[Dict]) -> Optional[str]:
"""通过语言模型压缩消息"""
if not messages:
return None
prompt = self._build_compression_prompt(messages)
print(f"📦 {i18n.get('conversation_compress')}: 正在压缩 {len(messages)} 条消息...")
try:
headers = {
"Authorization": f"Bearer {self.ai.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": constants.DEEPSEEK_MODEL,
"messages": [
{"role": "system", "content": "你是一个专业的对话摘要助手,擅长提取对话核心信息并进行结构化总结。请保留工具调用的关键结果。"},
{"role": "user", "content": prompt}
],
"temperature": 0.3,
"max_tokens": constants.COMPRESSOR_LLM_MAX_TOKENS
}
response = requests.post(
constants.DEEPSEEK_API_URL,
headers=headers,
json=payload,
timeout=30
)
if response.status_code == 200:
result = response.json()
summary = result["choices"][0]["message"]["content"].strip()
return summary
else:
print(f" ❌ 语言模型压缩失败: HTTP {response.status_code}")
return None
except Exception as e:
self.logger.error(f"语言模型压缩异常: {e}")
return None
def _validate_message_structure(self, messages: List[Dict]) -> List[Dict]:
"""验证并修复消息结构,移除孤立的 tool 消息"""
if not messages:
return messages
# 找出所有 assistant 消息的 tool_call_ids
assistant_tool_ids = set()
for msg in messages:
if msg.get("role") == "assistant" and msg.get("tool_calls"):
for tc in msg.get("tool_calls", []):
tc_id = tc.get("id")
if tc_id:
assistant_tool_ids.add(tc_id)
# 过滤掉没有对应 assistant 的 tool 消息
filtered = []
for msg in messages:
if msg.get("role") == "tool":
tool_call_id = msg.get("tool_call_id")
if tool_call_id and tool_call_id not in assistant_tool_ids:
print(f" ⚠️ 移除孤立的 tool 消息: {tool_call_id}")
continue
filtered.append(msg)
return filtered
def compress_conversation(self, messages: List[Dict]) -> List[Dict]:
"""
压缩对话内容 - 优先压缩较早的消息,包含tool调用信息一并压缩
当消息条数超过阈值时,压缩到阈值以下
"""
if not messages:
return messages
# 分离系统消息和对话消息
system_messages = []
conversation_messages = []
for msg in messages:
if msg.get("role") == "system":
system_messages.append(msg)
else:
conversation_messages.append(msg)
if not conversation_messages:
return system_messages
original_count = len(conversation_messages)
original_tokens = self.estimate_tokens(conversation_messages)
threshold = constants.COMPRESS_MESSAGE_THRESHOLD
print(f"\n📊 对话压缩检查:")
print(f" 当前消息数: {original_count} 条 (阈值: {threshold} 条)")
print(f" 当前Token数: {original_tokens} 个")
# 计算目标消息数量 - 压缩到阈值的70%
target_count = self._calculate_target_count(original_count, threshold, keep_ratio=0.7)
print(f" 目标消息数: {target_count} 条 (压缩后应少于阈值)")
# 如果目标数量大于等于当前数量,不需要压缩
if target_count >= original_count:
print(f" 目标消息数大于等于当前,跳过压缩")
return messages
# 计算需要压缩的消息数量
messages_to_compress = original_count - target_count
print(f" 需要压缩的消息数量: {messages_to_compress} 条")
# 根据消息数量查找可压缩块
compress_start, compress_end = self._find_compressible_block_by_count(
conversation_messages, target_count
)
if compress_end - compress_start >= 2:
compressible = conversation_messages[compress_start:compress_end]
compress_tokens = self.estimate_tokens(compressible)
compress_count = len(compressible)
print(f" 压缩块范围: [{compress_start}:{compress_end}], 共 {compress_count} 条消息")
print(f" 压缩块token数: {compress_tokens}")
# 如果压缩块大小仍然不够,尝试扩大压缩块
if compress_count < messages_to_compress:
print(f" 压缩块大小不足({compress_count} < {messages_to_compress}),扩大压缩块...")
# 扩大压缩块
additional = messages_to_compress - compress_count + 5
new_start = max(0, compress_start - additional // 2)
new_end = min(len(conversation_messages), compress_end + additional // 2)
compress_start, compress_end = new_start, new_end
compressible = conversation_messages[compress_start:compress_end]
compress_count = len(compressible)
print(f" 扩大后压缩块: [{compress_start}:{compress_end}], 共 {compress_count} 条消息")
else:
print(f" 未找到合适的压缩块,使用默认压缩策略")
# 使用默认压缩策略:压缩最早的一半消息,保留最近的消息
keep_recent = constants.COMPRESS_KEEP_RECENT
compress_end = len(conversation_messages) - keep_recent
compress_start = max(0, compress_end - messages_to_compress - 5)
compress_start = max(compress_start, 0)
compressible = conversation_messages[compress_start:compress_end]
compress_count = len(compressible)
print(f" 默认压缩块: [{compress_start}:{compress_end}], 共 {compress_count} 条消息")
# 确保压缩块有足够多的消息
if compress_count < 5:
print(f" 压缩块太小({compress_count}条),跳过压缩")
return messages
# 调用语言模型压缩
summary = self._compress_via_llm(compressible)
if not summary:
print(f" ❌ 压缩失败,保留原始对话")
return messages
# 构建压缩后的消息列表
compressed_result = []
# 添加压缩块之前的消息
compressed_result.extend(conversation_messages[:compress_start])
# 插入压缩摘要(作为系统消息)
compressed_result.append({
"role": "system",
"content": i18n.get('compressed_summary', summary)
})
# 添加压缩块之后的消息(包括最近的消息)
compressed_result.extend(conversation_messages[compress_end:])
# 验证并修复消息结构
compressed_result = self._validate_message_structure(compressed_result)
# 合并系统消息
final_messages = system_messages + compressed_result
# 统计结果
final_count = len([m for m in final_messages if m.get("role") != "system"])
final_tokens = self.estimate_tokens([m for m in final_messages if m.get("role") != "system"])
print(f"\n✅ {i18n.get('conversation_compressed', original_count, final_count)}")
print(f" Token: {original_tokens} -> {final_tokens} (压缩率: {final_tokens/original_tokens:.1%})")
# 检查是否压缩到阈值以下
if final_count >= threshold:
print(f" ⚠️ 警告: 压缩后消息数({final_count})仍大于等于阈值({threshold})")
return final_messages
# ==================== 备忘录数据库 ====================
class MemoDatabase:
"""备忘录数据库 - 存储和管理任务"""
def __init__(self, db_path: str = None):
if db_path is None:
db_path = os.path.join(constants.SAVE_DIR, constants.MEMORY_DB)
# 确保目录存在
os.makedirs(constants.SAVE_DIR, exist_ok=True)
self.db_path = db_path
self.logger = Logger("MemoDB")
self._init_database()
def _init_database(self):
"""初始化备忘录表"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS memos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
created_at TEXT NOT NULL,
reminder_time TEXT,
completed_at TEXT,
is_completed INTEGER DEFAULT 0,
tags TEXT,
priority INTEGER DEFAULT 1,
created_by TEXT,
source TEXT,
repeat_type TEXT DEFAULT 'none',
repeat_interval INTEGER DEFAULT 0,
repeat_interval_unit TEXT DEFAULT 'days',
repeat_end_time TEXT,
last_triggered_at TEXT,
trigger_count INTEGER DEFAULT 0,
is_immediate INTEGER DEFAULT 0
)
''')
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_memos_reminder
ON memos(reminder_time) WHERE is_completed = 0
''')
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_memos_immediate
ON memos(is_immediate) WHERE is_immediate = 1 AND is_completed = 0
''')
conn.commit()
self.logger.info("任务表初始化完成")
def add_memo(self, title: str, content: str = None, reminder_time: str = None,
tags: List[str] = None, priority: int = 1, created_by: str = None,
source: str = None,
repeat_type: str = 'none', repeat_interval: int = 0,
repeat_interval_unit: str = 'days', repeat_end_time: str = None,
is_immediate: bool = False) -> int:
"""添加任务 - 所有任务都是对话任务,不再需要metadata和任务类型"""
created_at = datetime.now().strftime(constants.DATETIME_FORMAT)
tags_str = ",".join(tags) if tags else ""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO memos
(title, content, created_at, reminder_time, tags, priority, created_by,
is_completed, source, repeat_type, repeat_interval, repeat_interval_unit,
repeat_end_time, is_immediate)
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?)
''', (title, content, created_at, reminder_time, tags_str, priority, created_by,
source, repeat_type, repeat_interval, repeat_interval_unit,
repeat_end_time, 1 if is_immediate else 0))
memo_id = cursor.lastrowid
conn.commit()
repeat_info = f" (重复: {repeat_type}"
if repeat_type == 'custom':
repeat_info += f", 每{repeat_interval} {repeat_interval_unit}"
repeat_info += ")"
immediate_info = " (即时任务)" if is_immediate else ""
self.logger.info(i18n.get('task_added', title, memo_id) + repeat_info + immediate_info)
return memo_id
def get_memo(self, memo_id: int) -> Optional[Dict]:
"""获取单个任务"""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute('SELECT * FROM memos WHERE id = ?', (memo_id,))
row = cursor.fetchone()
if row:
memo = dict(row)
if memo.get('tags'):
memo['tags'] = memo['tags'].split(',')
return memo
return None
def get_pending_memos(self, limit: int = None, before_time: str = None, include_immediate: bool = False) -> List[Dict]:
"""获取待执行的任务 - 严格使用当前时间比较"""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# 使用当前时间作为基准
now = datetime.now().strftime(constants.DATETIME_FORMAT)
# 如果传入了 before_time,使用它,否则使用当前时间
compare_time = before_time if before_time else now
query = '''
SELECT * FROM memos
WHERE is_completed = 0
AND reminder_time IS NOT NULL
AND reminder_time <= ? -- 严格小于等于比较时间
AND (repeat_end_time IS NULL OR repeat_end_time >= ?)
'''
if not include_immediate:
query += ' AND is_immediate = 0'
query += ' ORDER BY reminder_time ASC, priority DESC'
params = [compare_time, compare_time]
if limit:
query += f' LIMIT {limit}'
cursor.execute(query, params)
results = []
for row in cursor.fetchall():
memo = dict(row)
if memo.get('tags'):
memo['tags'] = memo['tags'].split(',')
# 【关键修复】二次验证:确保 reminder_time 确实 <= 当前时间
if memo.get('reminder_time'):
try:
reminder_time = datetime.strptime(memo['reminder_time'], constants.DATETIME_FORMAT)
current_time = datetime.strptime(compare_time, constants.DATETIME_FORMAT)
if reminder_time <= current_time:
results.append(memo)
else:
# 调试日志
self.logger.debug(f"过滤掉未到期任务 #{memo['id']}: {memo['reminder_time']} > {compare_time}")
except (ValueError, TypeError) as e:
self.logger.warning(f"解析任务 #{memo['id']} 的提醒时间失败: {e}")
else:
# 没有提醒时间的任务不应该在待执行列表中
self.logger.warning(f"任务 #{memo['id']} 没有提醒时间,但被返回为待执行")
return results
def get_immediate_memos(self, limit: int = None) -> List[Dict]:
"""获取待执行的即时任务"""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
query = '''
SELECT * FROM memos
WHERE is_completed = 0
AND is_immediate = 1
ORDER BY created_at ASC
'''
if limit:
query += f' LIMIT {limit}'
cursor.execute(query)
results = []
for row in cursor.fetchall():
memo = dict(row)
if memo.get('tags'):
memo['tags'] = memo['tags'].split(',')
results.append(memo)
return results
def get_overdue_memos(self, limit: int = None) -> List[Dict]:
"""获取已过期的任务"""
now = datetime.now().strftime(constants.DATETIME_FORMAT)
return self.get_pending_memos(limit=limit, before_time=now)
def get_all_memos(self, include_completed: bool = False, limit: int = 50) -> List[Dict]:
"""获取所有任务"""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
if include_completed:
cursor.execute('''
SELECT * FROM memos
ORDER BY
CASE WHEN is_completed = 0 THEN 0 ELSE 1 END,
reminder_time ASC,
created_at DESC
LIMIT ?
''', (limit,))
else:
cursor.execute('''
SELECT * FROM memos
WHERE is_completed = 0
ORDER BY reminder_time ASC, priority DESC, created_at DESC
LIMIT ?
''', (limit,))
results = []
for row in cursor.fetchall():
memo = dict(row)
if memo.get('tags'):
memo['tags'] = memo['tags'].split(',')
results.append(memo)
return results
def mark_as_triggered(self, memo_id: int) -> Optional[str]:
"""标记任务已被触发,并根据重复规则计算下一次提醒时间"""
memo = self.get_memo(memo_id)
if not memo:
return None
now = datetime.now()
next_reminder = None
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
UPDATE memos
SET last_triggered_at = ?, trigger_count = trigger_count + 1
WHERE id = ?
''', (now.strftime(constants.DATETIME_FORMAT), memo_id))
# 处理重复任务
if memo['repeat_type'] != 'none' and not self._should_end_repeat(memo, now):
# 使用当前时间作为基准来计算下一次执行时间
next_reminder = self._calculate_next_reminder(memo, now)
if next_reminder:
cursor.execute('''
UPDATE memos
SET reminder_time = ?
WHERE id = ?
''', (next_reminder.strftime(constants.DATETIME_FORMAT), memo_id))
self.logger.info(f"任务 #{memo_id} 已更新下一次执行时间: {next_reminder}")
else:
# 重复结束,标记为完成
cursor.execute('''
UPDATE memos
SET is_completed = 1, completed_at = ?
WHERE id = ?
''', (now.strftime(constants.DATETIME_FORMAT), memo_id))
self.logger.info(f"任务 #{memo_id} 重复结束,已标记为完成")
elif memo['is_immediate']:
# 即时任务执行后标记为完成
cursor.execute('''
UPDATE memos
SET is_completed = 1, completed_at = ?
WHERE id = ?
''', (now.strftime(constants.DATETIME_FORMAT), memo_id))
self.logger.info(f"即时任务 #{memo_id} 已完成")
else:
# 一次性任务,标记为完成
cursor.execute('''
UPDATE memos
SET is_completed = 1, completed_at = ?
WHERE id = ?
''', (now.strftime(constants.DATETIME_FORMAT), memo_id))
self.logger.info(f"任务 #{memo_id} 已完成")
conn.commit()
return next_reminder.strftime(constants.DATETIME_FORMAT) if next_reminder else None
def _should_end_repeat(self, memo: Dict, current_time: datetime) -> bool:
"""检查是否应该结束重复"""
if memo['repeat_end_time']:
end_time = datetime.strptime(memo['repeat_end_time'], constants.DATETIME_FORMAT)
if current_time > end_time:
return True
return False
def _calculate_next_reminder(self, memo: Dict, current_time: datetime) -> Optional[datetime]:
"""
计算下一次提醒时间,使用当前时间作为基准
支持分钟、小时、天为单位
"""
if memo['repeat_type'] == 'daily':
# 每天重复:当前时间 + 1天
next_time = current_time + timedelta(days=1)
elif memo['repeat_type'] == 'weekly':
# 每周重复:当前时间 + 7天
next_time = current_time + timedelta(weeks=1)
elif memo['repeat_type'] == 'monthly':
# 每月重复:尝试在当前时间上加一个月
try:
# 处理月份进位
if current_time.month == 12:
next_time = current_time.replace(year=current_time.year + 1, month=1)
else:
next_time = current_time.replace(month=current_time.month + 1)
except ValueError:
# 处理月份天数问题(如1月31日加一个月)
if current_time.month == 12:
next_time = current_time.replace(year=current_time.year + 1, month=1, day=1) - timedelta(days=1)
else:
next_time = current_time.replace(month=current_time.month + 1, day=1) - timedelta(days=1)
elif memo['repeat_type'] == 'custom' and memo['repeat_interval'] > 0:
unit = memo.get('repeat_interval_unit', 'days')
if unit == 'minutes':
# 每N分钟重复:当前时间 + N分钟
next_time = current_time + timedelta(minutes=memo['repeat_interval'])
elif unit == 'hours':
# 每N小时重复:当前时间 + N小时
next_time = current_time + timedelta(hours=memo['repeat_interval'])
else: # days
# 每N天重复:当前时间 + N天
next_time = current_time + timedelta(days=memo['repeat_interval'])
else:
return None
# 检查是否超过重复结束时间
if memo['repeat_end_time']:
end_time = datetime.strptime(memo['repeat_end_time'], constants.DATETIME_FORMAT)
if next_time > end_time:
return None
return next_time
def complete_memo(self, memo_id: int, force_complete: bool = False) -> bool:
"""完成任务"""
memo = self.get_memo(memo_id)
if not memo:
return False
if memo['repeat_type'] != 'none' and not force_complete:
self.mark_as_triggered(memo_id)
return True
completed_at = datetime.now().strftime(constants.DATETIME_FORMAT)
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
UPDATE memos
SET is_completed = 1, completed_at = ?
WHERE id = ? AND is_completed = 0
''', (completed_at, memo_id))
affected = cursor.rowcount
conn.commit()
if affected > 0:
self.logger.info(i18n.get('memo_completed', memo_id))
return True
return False
def get_memo_stats(self) -> Dict:
"""获取任务统计信息"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) FROM memos')
total = cursor.fetchone()[0]
cursor.execute('SELECT COUNT(*) FROM memos WHERE is_completed = 0')
pending = cursor.fetchone()[0]
now = datetime.now().strftime(constants.DATETIME_FORMAT)
cursor.execute('''
SELECT COUNT(*) FROM memos
WHERE is_completed = 0
AND reminder_time <= ?
AND is_immediate = 0
''', (now,))
overdue = cursor.fetchone()[0]
cursor.execute('SELECT COUNT(*) FROM memos WHERE is_completed = 1')
completed = cursor.fetchone()[0]
cursor.execute('SELECT COUNT(*) FROM memos WHERE is_immediate = 1 AND is_completed = 0')
immediate = cursor.fetchone()[0]
return {
"total": total,
"pending": pending,
"overdue": overdue,
"completed": completed,
"immediate": immediate
}
def add_user_task(self, title: str, content: str = None, reminder_time: str = None,
repeat_type: str = 'none', repeat_interval: int = 0,
repeat_interval_unit: str = 'days', is_immediate: bool = False) -> int:
"""添加用户任务(前端直接调用的任务)- 所有任务都是对话任务"""
if not constants.MEMO_ALLOW_USER_TASKS:
raise Exception("用户任务功能已禁用")
return self.add_memo(
title=title,
content=content,
reminder_time=reminder_time,
repeat_type=repeat_type,
repeat_interval=repeat_interval,
repeat_interval_unit=repeat_interval_unit,
created_by="user",
source="websocket",
is_immediate=is_immediate
)
def get_pending_tasks_for_user(self) -> List[Dict]:
"""获取待执行任务列表(用于前端显示)"""
pending = self.get_pending_memos(include_immediate=False)
immediate = self.get_immediate_memos()
result = []
now = datetime.now()
for memo in pending + immediate:
result.append({
"id": memo['id'],
"title": memo['title'],
"content": memo['content'],
"reminder_time": memo['reminder_time'],
"priority": memo['priority'],
"is_overdue": datetime.strptime(memo['reminder_time'], constants.DATETIME_FORMAT) < now if memo['reminder_time'] else False,
"repeat_type": memo['repeat_type'],
"repeat_interval": memo.get('repeat_interval', 0),
"repeat_interval_unit": memo.get('repeat_interval_unit', 'days'),
"trigger_count": memo.get('trigger_count', 0),
"is_immediate": memo.get('is_immediate', False)
})
return result
# ==================== 记忆数据库 ====================
class MemoryDatabase:
"""记忆数据库 - 存储重要的对话记忆"""
def __init__(self, db_path: str = None):
if db_path is None:
db_path = os.path.join(constants.SAVE_DIR, constants.MEMORY_DB)
# 确保目录存在
os.makedirs(constants.SAVE_DIR, exist_ok=True)
self.db_path = db_path
self.logger = Logger("MemoryDB")
self._init_database()
def _init_database(self):
"""初始化数据库表"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
topic TEXT NOT NULL,
summary TEXT NOT NULL,
content TEXT,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
created_at TEXT NOT NULL,
tags TEXT,
importance INTEGER DEFAULT 1
)
''')
cursor.execute('''
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
USING fts5(topic, summary, content, tags, content=memories)
''')
conn.commit()
def add_memory(self, topic: str, summary: str, content: str = None,
start_time: str = None, end_time: str = None,
tags: List[str] = None, importance: int = None) -> int:
"""添加记忆到数据库"""
if start_time is None:
start_time = datetime.now().strftime(constants.DATETIME_FORMAT)
if end_time is None:
end_time = datetime.now().strftime(constants.DATETIME_FORMAT)
if importance is None:
importance = constants.MEMORY_DEFAULT_IMPORTANCE
tags_str = ",".join(tags) if tags else ""
created_at = datetime.now().strftime(constants.DATETIME_FORMAT)
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO memories (topic, summary, content, start_time, end_time, created_at, tags, importance)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (topic, summary, content, start_time, end_time, created_at, tags_str, importance))
memory_id = cursor.lastrowid
cursor.execute('''
INSERT INTO memories_fts (rowid, topic, summary, content, tags)
VALUES (?, ?, ?, ?, ?)
''', (memory_id, topic, summary, content or "", tags_str))
conn.commit()
self.logger.info(f"已添加记忆: {topic} (ID: {memory_id})")
return memory_id
def delete_memory(self, memory_id: int) -> bool:
"""删除记忆"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('DELETE FROM memories WHERE id = ?', (memory_id,))
deleted = cursor.rowcount > 0
if deleted:
cursor.execute('DELETE FROM memories_fts WHERE rowid = ?', (memory_id,))
conn.commit()
self.logger.info(f"已删除记忆 ID: {memory_id}")
return deleted
def search_memories(self, keywords: List[str], match_all: bool = False, limit: int = None) -> List[Dict]:
"""多关键词模糊搜索记忆"""
if not keywords:
return []
if limit is None:
limit = constants.MEMORY_MAX_SEARCH_RESULTS
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
if match_all:
query = " AND ".join([f'"{kw}"' for kw in keywords])
else:
query = " OR ".join([f'"{kw}"' for kw in keywords])
cursor.execute('''
SELECT m.*, rank
FROM memories m
JOIN memories_fts f ON m.rowid = f.rowid
WHERE memories_fts MATCH ?
ORDER BY m.end_time DESC, rank, m.importance DESC
LIMIT ?
''', (query, limit))
results = []
for row in cursor.fetchall():
memory = dict(row)
if memory.get('tags'):
memory['tags'] = memory['tags'].split(',')
results.append(memory)
return results
def get_recent_memories(self, limit: int = 10) -> List[Dict]:
"""获取最近的记忆"""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute('SELECT * FROM memories ORDER BY end_time DESC LIMIT ?', (limit,))
results = []
for row in cursor.fetchall():
memory = dict(row)
if memory.get('tags'):
memory['tags'] = memory['tags'].split(',')
results.append(memory)
return results
def get_memory_by_id(self, memory_id: int) -> Optional[Dict]:
"""根据ID获取记忆"""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute('SELECT * FROM memories WHERE id = ?', (memory_id,))
row = cursor.fetchone()
if row:
memory = dict(row)
if memory.get('tags'):
memory['tags'] = memory['tags'].split(',')
return memory
return None
def delete_memories_by_ids(self, memory_ids: List[int]) -> int:
"""批量删除记忆"""
if not memory_ids:
return 0
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
placeholders = ','.join(['?'] * len(memory_ids))
cursor.execute(f'DELETE FROM memories WHERE id IN ({placeholders})', memory_ids)
deleted_count = cursor.rowcount
if deleted_count > 0:
cursor.execute(f'DELETE FROM memories_fts WHERE rowid IN ({placeholders})', memory_ids)
conn.commit()
self.logger.info(f"已批量删除 {deleted_count} 条记忆")
return deleted_count
# ==================== 任务调度器 ====================
class TaskScheduler:
"""任务调度器 - 基于备忘录数据库的定时任务系统"""
def __init__(self, loop: asyncio.AbstractEventLoop = None, logger: Logger = None):
self.loop = loop or asyncio.get_event_loop()
self.running = False
self.logger = logger or Logger("TaskScheduler")
self.check_interval = constants.SCHEDULER_CHECK_INTERVAL
self.max_tasks_per_run = constants.SCHEDULER_MAX_TASKS
self.broadcast_enabled = constants.SCHEDULER_BROADCAST
self.ws_handler = None # 将由管理器设置
self.ai_manager = None # 新增:AI管理器引用
# 任务执行历史
self.execution_history = []
self.max_history = 100
self.logger.info(i18n.get('scheduler_started'))
def set_websocket_handler(self, handler):
"""设置WebSocket处理器,用于广播任务执行状态"""
self.ws_handler = handler
def set_ai_manager(self, manager):
"""设置AI管理器引用"""
self.ai_manager = manager
async def broadcast_task_status(self, task: Dict, status: str, message: str = None):
"""广播任务状态到所有连接的客户端"""
if not self.broadcast_enabled or not self.ws_handler:
return
try:
# 确保task有必要的字段
task_info = {
'id': task.get('id', 0),
'title': task.get('title', '未知任务')
}
await self.ws_handler.broadcast_task_status(task_info, status, message)
except Exception as e:
self.logger.error(f"广播任务状态失败: {e}")
async def check_and_execute_tasks(self, memo_db: MemoDatabase):
"""检查并执行到期的任务 - 触发AI对话"""
if not memo_db:
return []
try:
# 获取所有到期任务(不包括即时任务)
now = datetime.now()
now_str = now.strftime(constants.DATETIME_FORMAT)
pending = memo_db.get_pending_memos(
limit=self.max_tasks_per_run,
before_time=now_str,
include_immediate=False
)
if not pending:
return []
self.logger.info(i18n.get('overdue_found', len(pending)))
# 广播任务开始执行
if self.broadcast_enabled and self.ws_handler:
await self.ws_handler.broadcast_task_status(
{"id": 0, "title": "批量任务"},
"batch_start",
f"开始执行 {len(pending)} 个任务"
)
executed_tasks = []
for memo in pending:
try:
# 广播任务开始执行
if self.ws_handler:
await self.ws_handler.broadcast_task_status(memo, "executing", i18n.get('task_executing', memo['title'], memo['id']))
# 【关键修复】执行前最后一次验证
if memo.get('reminder_time'):
reminder_time = datetime.strptime(memo['reminder_time'], constants.DATETIME_FORMAT)
if reminder_time > now:
self.logger.warning(f"任务 #{memo['id']} 的提醒时间 ({memo['reminder_time']}) 大于当前时间 ({now_str}),跳过执行")
continue
# 先标记任务已被触发
self.logger.info(f"标记任务 #{memo['id']} 为已触发")
next_time = memo_db.mark_as_triggered(memo['id'])
# 执行任务
result = await self.execute_task_as_conversation(memo, memo_db)
# 广播任务完成
if self.ws_handler:
await self.ws_handler.broadcast_task_status(memo, "complete", result)
executed_tasks.append({
"id": memo['id'],
"title": memo['title'],
"result": result,
"success": True,
"next_time": next_time
})
except Exception as e:
self.logger.error(i18n.get('task_failed', memo['title'], str(e)))
# 广播任务失败
if self.ws_handler:
await self.ws_handler.broadcast_task_status(memo, "failed", str(e))
executed_tasks.append({
"id": memo['id'],
"title": memo['title'],
"error": str(e),
"success": False
})
# 广播任务执行完成
if self.broadcast_enabled and self.ws_handler:
success_count = sum(1 for t in executed_tasks if t['success'])
await self.ws_handler.broadcast_task_status(
{"id": 0, "title": "批量任务"},
"batch_complete",
f"执行完成: {success_count}/{len(executed_tasks)} 个任务成功"
)
return executed_tasks
except Exception as e:
self.logger.error(f"检查任务失败: {e}")
return []
async def execute_task_as_conversation(self, memo: Dict, memo_db: MemoDatabase) -> str:
"""执行任务 - 将备忘录内容作为用户输入触发AI对话,收集完整的过程回复"""
self.logger.info(i18n.get('task_executing', memo['title'], memo['id']))
# 生成唯一的执行ID(基于任务ID和时间戳)
execution_id = f"{memo['id']}_{int(time.time())}_{memo.get('trigger_count', 0) + 1}"
# 记录执行历史
self.execution_history.append({
"id": memo['id'],
"execution_id": execution_id,
"title": memo['title'],
"time": datetime.now().strftime(constants.DATETIME_FORMAT),
"trigger_count": memo.get('trigger_count', 0) + 1
})
# 限制历史记录大小
if len(self.execution_history) > self.max_history:
self.execution_history = self.execution_history[-self.max_history:]
# 构建用户输入内容 - 将任务作为对话触发
user_input = f"[定时任务] {memo['title']}"
if memo['content']:
user_input += f"\n\n任务内容: {memo['content']}"
# 添加专注提示词,让AI专注于当前任务
focus_prompt = f"""
【重要提示】
这是由定时任务触发的对话,请遵守以下要求:
1. 专注于处理当前任务:{memo['title']}
2. 根据任务内容进行适当的操作和回答
3. 任务处理完成后,如果不需要继续对话,可以结束对话
4. 系统已经自动更新了任务状态(更新了执行时间和次数),你不需要手动处理任务完成逻辑
5. 不要在任务执行过程中创建新的定时任务(除非任务明确要求),也不要向用户提问(因为此对话是自动触发的)
请现在开始处理这个任务。
"""
user_input = focus_prompt + "\n\n" + user_input
result = "任务已触发AI对话"
# 如果有AI管理器,触发AI对话
if self.ai_manager and self.ai_manager.ai:
try:
# 获取当前事件循环
loop = asyncio.get_running_loop()
# 创建一个队列来收集完整的响应
full_response_queue = queue.Queue()
# 定义输出回调函数 - 直接广播,同时收集
def output_callback(msg_type, content):
"""AI输出的回调函数 - 直接广播,同时收集"""
# 收集完整响应
if msg_type == "complete":
full_response_queue.put(content)
# 直接广播给所有客户端 - 必须带上 task_id 和 execution_id
if self.ws_handler and self.broadcast_enabled:
# 使用 run_coroutine_threadsafe 在正确的循环中运行异步函数
asyncio.run_coroutine_threadsafe(
self.ws_handler.broadcast({
"type": "task_process",
"task_id": str(memo['id']), # 原始任务ID
"execution_id": execution_id, # 唯一的执行ID
"task_title": memo['title'],
"msg_type": msg_type,
"content": content,
"is_new_execution": msg_type == "chunk", # 标记是否为新的执行
"timestamp": datetime.now().strftime(constants.DATETIME_FORMAT)
}),
loop
)
# 修改:确保AI处于新对话状态,不加载历史上下文
if self.ai_manager.ai:
# 重置AI对话状态,但保留系统提示词
self.ai_manager.ai.reset_conversation()
self.logger.info("AI对话状态已重置,准备执行任务")
# 创建一个 Future 来等待 AI 完成
ai_future = loop.create_future()
def run_ai():
try:
# 在单独的线程中运行 AI
response = self.ai_manager.ai.think_and_respond(user_input, output_callback)
# 将结果设置到 Future 中
loop.call_soon_threadsafe(ai_future.set_result, response)
except Exception as e:
loop.call_soon_threadsafe(ai_future.set_exception, e)
# 启动 AI 线程
ai_thread = threading.Thread(target=run_ai)
ai_thread.daemon = True
ai_thread.start()
# 等待 AI 完成,同时处理可能的取消
try:
response = await ai_future
result = response or "AI无响应"
except Exception as e:
self.logger.error(f"AI对话失败: {e}")
result = f"任务执行但AI对话失败: {str(e)}"
finally:
# 确保线程结束
ai_thread.join(timeout=1.0)
except Exception as e:
self.logger.error(f"AI对话失败: {e}")
import traceback
traceback.print_exc()
result = f"任务执行但AI对话失败: {str(e)}"
# 广播错误信息
if self.ws_handler and self.broadcast_enabled:
try:
await self.ws_handler.broadcast({
"type": "task_error",
"task_id": str(memo['id']), # 确保是字符串
"execution_id": execution_id,
"task_title": memo['title'],
"error": str(e),
"timestamp": datetime.now().strftime(constants.DATETIME_FORMAT)
})
except:
pass
# 获取任务的最新状态(可选,用于日志)
updated_memo = memo_db.get_memo(memo['id'])
if updated_memo:
if not updated_memo['is_completed'] and updated_memo.get('reminder_time'):
self.logger.info(i18n.get('next_execution', updated_memo['reminder_time']))
elif updated_memo['is_completed']:
self.logger.info(i18n.get('task_completed', memo['title']))
return result
async def check_and_execute_immediate_tasks(self, memo_db: MemoDatabase):
"""检查并执行即时任务"""
if not memo_db:
return []
try:
immediate = memo_db.get_immediate_memos(limit=self.max_tasks_per_run)
if not immediate:
return []
self.logger.info(f"发现 {len(immediate)} 个即时任务需要立即执行")
if self.broadcast_enabled and self.ws_handler:
await self.ws_handler.broadcast_task_status(
{"id": 0, "title": "即时任务"},
"batch_start",
f"开始执行 {len(immediate)} 个即时任务"
)
executed_tasks = []
for memo in immediate:
try:
if self.ws_handler:
await self.ws_handler.broadcast_task_status(memo, "executing", i18n.get('immediate_memo_executing', memo['title']))
self.logger.info(f"标记即时任务 #{memo['id']} 为已触发")
next_time = memo_db.mark_as_triggered(memo['id'])
result = await self.execute_task_as_conversation(memo, memo_db)
if self.ws_handler:
await self.ws_handler.broadcast_task_status(memo, "complete", result)
executed_tasks.append({
"id": memo['id'],
"title": memo['title'],
"result": result,
"success": True,
"next_time": next_time
})
except Exception as e:
self.logger.error(i18n.get('task_failed', memo['title'], str(e)))
if self.ws_handler:
await self.ws_handler.broadcast_task_status(memo, "failed", str(e))
executed_tasks.append({
"id": memo['id'],
"title": memo['title'],
"error": str(e),
"success": False
})
if self.broadcast_enabled and self.ws_handler:
success_count = sum(1 for t in executed_tasks if t['success'])
await self.ws_handler.broadcast_task_status(
{"id": 0, "title": "即时任务"},
"batch_complete",
f"即时任务执行完成: {success_count}/{len(executed_tasks)} 个成功"
)
return executed_tasks
except Exception as e:
self.logger.error(f"检查即时任务失败: {e}")
return []
async def run(self, memo_db: MemoDatabase):
"""启动调度器"""
self.running = True
while self.running:
try:
# 先检查并执行即时任务
if memo_db:
immediate_executed = await self.check_and_execute_immediate_tasks(memo_db)
if immediate_executed:
self.logger.info(f"执行了 {len(immediate_executed)} 个即时任务")
# 再检查普通到期任务
if memo_db:
executed = await self.check_and_execute_tasks(memo_db)
if executed:
self.logger.info(f"执行了 {len(executed)} 个普通任务")
await asyncio.sleep(self.check_interval)
except asyncio.CancelledError:
break
except Exception as e:
self.logger.error(f"调度器运行错误: {e}")
await asyncio.sleep(1)
def stop(self):
"""停止调度器"""
self.running = False
self.logger.info(i18n.get('scheduler_stopped'))
def get_stats(self) -> Dict:
"""获取调度器统计信息"""
return {
"running": self.running,
"check_interval": self.check_interval,
"max_tasks_per_run": self.max_tasks_per_run,
"broadcast_enabled": self.broadcast_enabled,
"recent_executions": self.execution_history[-10:],
"total_executions": len(self.execution_history)
}
# ==================== 对话历史管理器 ====================
class ConversationHistory:
"""对话历史管理器 - 管理所有历史对话"""
def __init__(self, save_dir: str = None, memory_db: MemoryDatabase = None, load_history: bool = None):
if save_dir is None:
save_dir = constants.SAVE_DIR
self.save_dir = Path(save_dir)
self.archive_dir = self.save_dir / constants.ARCHIVE_DIR
self.history_file = self.save_dir / constants.HISTORY_FILE
self.memory_db = memory_db
config = ConfigManager()
if load_history is None:
load_history = config.get("system.load_history_by_default", True)
self.load_history = load_history
# 确保目录存在
self.save_dir.mkdir(parents=True, exist_ok=True)
self.archive_dir.mkdir(parents=True, exist_ok=True)
self.logger = Logger("HistoryManager")
self.history_index = self._load_history_index()
self.current_conversation = None
self.current_file = self.save_dir / constants.CURRENT_CONVERSATION_FILE
self._load_current_conversation()
def _load_history_index(self) -> Dict:
"""加载历史索引文件"""
if self.history_file.exists():
try:
with open(self.history_file, 'r', encoding='utf-8') as f:
return json.load(f)
except:
return {"conversations": [], "last_id": 0}
return {"conversations": [], "last_id": 0}
def _save_history_index(self):
"""保存历史索引"""
with open(self.history_file, 'w', encoding='utf-8') as f:
json.dump(self.history_index, f, ensure_ascii=False, indent=2)
def _load_current_conversation(self):
"""加载当前的对话(如果有)"""
if self.current_file.exists():
try:
with open(self.current_file, 'r', encoding='utf-8') as f:
self.current_conversation = json.load(f)
self.logger.info(f"已加载当前对话: {self.current_conversation.get('topic', '未命名')}")
except Exception as e:
self.logger.error(f"加载当前对话失败: {e}")
def save_current_conversation(self):
"""保存当前对话"""
if self.current_conversation:
with open(self.current_file, 'w', encoding='utf-8') as f:
json.dump(self.current_conversation, f, ensure_ascii=False, indent=2)
def start_new_conversation(self, topic: str = None) -> Dict:
"""开始新对话"""
if self.current_conversation:
self.archive_current_conversation()
conv_id = str(int(time.time()))
self.current_conversation = {
"id": conv_id,
"start_time": datetime.now().strftime(constants.DATETIME_FORMAT),
"end_time": None,
"topic": topic or "新对话",
"messages": [],
"tool_calls": [],
"stats": {
"user_messages": 0,
"ai_messages": 0,
"tool_calls": 0
}
}
self.save_current_conversation()
self.logger.info(f"开始新对话: {topic}")
return self.current_conversation
def archive_current_conversation(self, final_summary: str = None):
"""将当前对话归档到历史"""
if not self.current_conversation:
return None
if self.current_conversation.get("end_time") is None:
self.current_conversation["end_time"] = datetime.now().strftime(constants.DATETIME_FORMAT)
if final_summary and self.memory_db:
messages = self.current_conversation.get("messages", [])
content_preview = "\n".join([f"{m['speaker']}: {m['content'][:200]}"
for m in messages[-10:] if m.get('speaker')])
self.memory_db.add_memory(
topic=self.current_conversation["topic"],
summary=final_summary,
content=content_preview,
start_time=self.current_conversation["start_time"],
end_time=self.current_conversation["end_time"],
importance=2
)
timestamp = datetime.now().strftime(constants.DATE_FORMAT)
archive_filename = f"conversation_{timestamp}.json"
archive_path = self.archive_dir / archive_filename
with open(archive_path, 'w', encoding='utf-8') as f:
json.dump(self.current_conversation, f, ensure_ascii=False, indent=2)
self.history_index["conversations"].append({
"id": self.current_conversation["id"],
"file": archive_filename,
"start_time": self.current_conversation["start_time"],
"end_time": self.current_conversation["end_time"],
"topic": self.current_conversation["topic"],
"messages": len(self.current_conversation["messages"]),
"tool_calls": len(self.current_conversation["tool_calls"])
})
self._save_history_index()
self.logger.info(f"对话已归档: {archive_filename}")
self.current_conversation = None
if self.current_file.exists():
self.current_file.unlink()
return str(archive_path)
def load_all_history(self, max_messages: int = None) -> List[Dict]:
"""加载所有历史对话作为上下文"""
if max_messages is None:
max_messages = constants.MAX_HISTORY_CONTEXT
if not self.load_history:
return []
all_messages = []
if self.current_conversation:
current_msgs = self.current_conversation.get("messages", [])
for msg in current_msgs[-max_messages//2:]:
if not msg.get("is_history"):
all_messages.append({
"timestamp": msg["timestamp"],
"role": msg["role"],
"speaker": msg.get("speaker"),
"content": msg["content"],
"from_current": True
})
history_files = sorted(self.archive_dir.glob("*.json"), reverse=True)[:constants.MAX_HISTORY_CONVERSATIONS]
for hist_file in history_files:
try:
with open(hist_file, 'r', encoding='utf-8') as f:
conv = json.load(f)
messages = conv.get("messages", [])
for msg in messages[-max_messages//len(history_files):]:
if not msg.get("is_history"):
all_messages.append({
"timestamp": msg["timestamp"],
"role": msg["role"],
"speaker": msg.get("speaker"),
"content": msg["content"],
"from_file": hist_file.name
})
except Exception as e:
self.logger.error(f"加载历史文件 {hist_file} 失败: {e}")
all_messages.sort(key=lambda x: x["timestamp"])
if len(all_messages) > max_messages:
all_messages = all_messages[-max_messages:]
self.logger.info(f"已加载 {len(all_messages)} 条历史消息")
return all_messages
def get_conversation_context(self, max_messages: int = None) -> List[Dict]:
"""获取适合作为AI上下文的对话历史"""
if max_messages is None:
max_messages = constants.MAX_HISTORY_CONTEXT
if not self.load_history:
return []
history_messages = self.load_all_history(max_messages)
context = []
for msg in history_messages:
role = msg.get("role", "")
content = msg.get("content", "")
if role == "system":
continue
if role == "assistant":
speaker = msg.get("speaker", "")
context.append({
"role": "assistant",
"content": f"[{speaker}] {content}"
})
else:
context.append({
"role": "user",
"content": content
})
return context
def add_message(self, role: str, content: str, speaker: str = None):
"""添加消息到当前对话"""
if not self.current_conversation:
self.start_new_conversation()
message = {
"timestamp": datetime.now().strftime(constants.DATETIME_FORMAT),
"role": role,
"speaker": speaker,
"content": content
}
self.current_conversation["messages"].append(message)
if role == "user":
self.current_conversation["stats"]["user_messages"] += 1
elif role == "assistant":
self.current_conversation["stats"]["ai_messages"] += 1
self.save_current_conversation()
def add_tool_call(self, tool_name: str, args: Dict, result: str):
"""记录工具调用"""
if not self.current_conversation:
return
tool_call = {
"timestamp": datetime.now().strftime(constants.DATETIME_FORMAT),
"tool": tool_name,
"args": args,
"result_preview": result[:200] + "..." if len(result) > 200 else result
}
self.current_conversation["tool_calls"].append(tool_call)
self.current_conversation["stats"]["tool_calls"] += 1
self.save_current_conversation()
def end_current_conversation(self, summary: str = None):
"""结束当前对话"""
if self.current_conversation:
self.current_conversation["end_time"] = datetime.now().strftime(constants.DATETIME_FORMAT)
if summary and self.memory_db:
messages = self.current_conversation.get("messages", [])
content_preview = "\n".join([f"{m['speaker']}: {m['content'][:200]}"
for m in messages[-10:] if m.get('speaker')])
self.memory_db.add_memory(
topic=self.current_conversation["topic"],
summary=summary,
content=content_preview,
start_time=self.current_conversation["start_time"],
end_time=self.current_conversation["end_time"],
importance=2
)
self.save_current_conversation()
self.logger.info("当前对话已保存")
def reset_conversation(self):
"""重置当前对话状态"""
if self.current_conversation:
self.current_conversation["end_time"] = datetime.now().strftime(constants.DATETIME_FORMAT)
self.save_current_conversation()
self.current_conversation = None
def list_conversations(self) -> List[Dict]:
"""列出所有对话(包括当前和归档)"""
conversations = []
if self.current_conversation:
conversations.append({
"file": constants.CURRENT_CONVERSATION_FILE,
"is_current": True,
"id": self.current_conversation.get("id"),
"start_time": self.current_conversation.get("start_time"),
"end_time": self.current_conversation.get("end_time"),
"topic": self.current_conversation.get("topic"),
"messages": len(self.current_conversation.get("messages", [])),
"tool_calls": len(self.current_conversation.get("tool_calls", []))
})
for conv in self.history_index.get("conversations", []):
conversations.append({
"file": conv["file"],
"is_current": False,
"id": conv["id"],
"start_time": conv["start_time"],
"end_time": conv["end_time"],
"topic": conv["topic"],
"messages": conv["messages"],
"tool_calls": conv["tool_calls"]
})
conversations.sort(key=lambda x: x["start_time"] if x["start_time"] else "", reverse=True)
return conversations
def load_conversation(self, file_name: str) -> Optional[Dict]:
"""加载指定的对话"""
if file_name == constants.CURRENT_CONVERSATION_FILE and self.current_conversation:
return self.current_conversation
file_path = self.archive_dir / file_name
if file_path.exists():
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
return None
# ==================== 命令执行器抽象类 ====================
class CommandExecutor(ABC):
"""命令执行器抽象基类 - 负责执行系统命令"""
def __init__(self, logger: Optional[Logger] = None):
self.logger = logger or Logger("CommandExecutor")
self.config = ConfigManager()
self.process = None
@abstractmethod
def execute(self, command: str, timeout: int = 30, **kwargs) -> str:
"""执行命令并返回结果(非流式)"""
pass
@abstractmethod
def execute_streaming(self, command: str, output_queue: queue.Queue, timeout: int = 30, **kwargs):
"""流式执行命令,通过队列输出结果"""
pass
def is_enabled(self) -> bool:
"""检查命令执行是否启用"""
return constants.COMMANDS_ENABLED
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)
class SubprocessCommandExecutor(CommandExecutor):
"""基于subprocess的命令执行器 - 支持超时和工作目录"""
def execute(self, command: str, timeout: int = 30, work_dir: str = ".", env: Dict = None, **kwargs) -> str:
"""执行命令并返回完整结果"""
if not self.is_enabled():
return "❌ 命令执行功能已禁用"
self.logger.info(f"执行命令: {command}")
# 准备环境变量
exec_env = os.environ.copy()
if env:
exec_env.update(env)
try:
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=timeout,
env=exec_env,
cwd=os.path.expanduser(work_dir)
)
data = {}
if result.stdout:
data["输出"] = result.stdout.strip()
if result.stderr:
data["错误"] = result.stderr.strip()
data["返回码"] = result.returncode
if not result.stdout and not result.stderr:
return self.format_result(True, "命令执行成功,无输出", data)
return self.format_result(result.returncode == 0, "命令执行完成", data)
except subprocess.TimeoutExpired:
return f"❌ 命令执行超时({timeout}秒)"
except FileNotFoundError as e:
return f"❌ 命令执行错误: 命令未找到 - {str(e)}"
except PermissionError as e:
return f"❌ 命令执行错误: 权限不足 - {str(e)}"
except Exception as e:
return f"❌ 执行错误: {str(e)}"
def execute_streaming(self, command: str, output_queue: queue.Queue, timeout: int = 30,
work_dir: str = ".", env: Dict = None, **kwargs):
"""流式执行命令,支持超时控制和工作目录"""
if not self.is_enabled():
output_queue.put(("line", "❌ 命令执行功能已禁用\n"))
output_queue.put(("complete", "❌ 命令执行功能已禁用"))
return
self.logger.info(f"流式执行命令: {command}")
output_queue.put(("line", f"\n🔧 执行命令: {command}\n"))
output_queue.put(("line", f"⏱️ 超时设置: {timeout} 秒\n"))
if work_dir != ".":
output_queue.put(("line", f"📁 工作目录: {work_dir}\n"))
output_queue.put(("line", "-" * 50 + "\n"))
# 准备环境变量
exec_env = os.environ.copy()
if env:
exec_env.update(env)
try:
# 使用管道来实时获取输出
self.process = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdin=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True,
env=exec_env,
cwd=os.path.expanduser(work_dir)
)
start_time = time.time()
output_lines = []
timeout_occurred = False
# 实时读取输出
while True:
# 检查超时
if timeout > 0 and time.time() - start_time > timeout:
self.process.terminate()
timeout_occurred = True
output_queue.put(("line", f"\n⏰ 命令执行超时({timeout}秒)\n"))
break
# 读取一行输出
line = self.process.stdout.readline()
if not line and self.process.poll() is not None:
break
if line:
line = line.rstrip('\n')
output_queue.put(("line", line + "\n"))
output_lines.append(line)
# 如果超时,确保进程被终止
if timeout_occurred and self.process.poll() is None:
self.process.kill()
self.process.wait(timeout=2)
# 获取剩余输出
remaining, _ = self.process.communicate(timeout=5)
if remaining:
output_queue.put(("line", remaining))
output_lines.append(remaining)
return_code = self.process.returncode if self.process.returncode is not None else -1
output_queue.put(("line", "-" * 50 + "\n"))
if timeout_occurred:
output_queue.put(("line", f"⏰ 命令执行超时({timeout}秒),返回码: {return_code}\n"))
else:
output_queue.put(("line", f"✅ 命令执行完成,返回码: {return_code}\n"))
# 准备完整结果
full_output = "\n".join(output_lines)
data = {
"输出": full_output if full_output else "(无输出)",
"返回码": return_code
}
if timeout_occurred:
result = self.format_result(False, f"命令执行超时({timeout}秒)", data)
else:
result = self.format_result(return_code == 0, "命令执行完成", data)
output_queue.put(("complete", result))
except Exception as e:
output_queue.put(("line", f"❌ 执行错误: {str(e)}\n"))
output_queue.put(("complete", f"❌ 执行错误: {str(e)}"))
finally:
if self.process and self.process.poll() is None:
self.process.terminate()
try:
self.process.wait(timeout=5)
except:
self.process.kill()
# ==================== 工具基类 ====================
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 格式) ====================
class SkillManager:
"""扩展技能管理器 - 收集并管理 ~/.aibox/skills/ 目录下的技能文件。
支持两种技能格式:
1. 可执行脚本/程序:执行 --help,第一行为简介
2. Markdown 文件 (.md):第一行为简介(# 标题或普通文本),其余为技能内容
"""
def __init__(self, logger: Optional[Logger] = None):
self.logger = logger or Logger("SkillManager")
# name -> {path, file, summary, content, is_markdown}
self.skills: Dict[str, Dict] = {}
self._load_skills()
# ------------------------------------------------------------------
# 内部辅助
# ------------------------------------------------------------------
def _run_help(self, skill_path: str) -> str:
"""执行 skill_path --help 并返回完整输出(stdout 优先,否则 stderr)。"""
try:
result = subprocess.run(
[skill_path, "--help"],
capture_output=True,
text=True,
timeout=10,
env=os.environ.copy()
)
output = result.stdout.strip()
if not output:
output = result.stderr.strip()
return output
except Exception as e:
self.logger.warning(f"运行 {skill_path} --help 失败: {e}")
return ""
def _read_markdown_skill(self, skill_path: str) -> Tuple[str, str]:
"""读取 Markdown 技能文件,返回 (简介, 完整内容)"""
try:
with open(skill_path, 'r', encoding='utf-8') as f:
content = f.read().strip()
if not content:
return "(空文件)", ""
# 提取第一行非空内容作为简介
lines = content.splitlines()
summary = ""
for line in lines:
stripped = line.strip()
if stripped:
# 如果是 Markdown 标题,去掉 # 前缀
if stripped.startswith('#'):
summary = stripped.lstrip('#').strip()
else:
summary = stripped
break
if not summary:
summary = os.path.basename(skill_path).replace('.md', '')
return summary, content
except Exception as e:
self.logger.warning(f"读取 Markdown 技能文件失败 {skill_path}: {e}")
return "(读取失败)", ""
@staticmethod
def _first_nonempty_line(text: str) -> str:
"""返回文本中第一行非空内容。"""
for line in text.splitlines():
stripped = line.strip()
if stripped:
return stripped
return ""
def _is_markdown_file(self, filename: str) -> bool:
"""判断是否为 Markdown 文件"""
return filename.lower().endswith('.md')
def _is_executable(self, filepath: str) -> bool:
"""检查文件是否可执行"""
if os.name == 'nt': # Windows
# Windows 下检查扩展名
executable_extensions = {'.exe', '.bat', '.cmd', '.ps1', '.py', '.sh'}
ext = os.path.splitext(filepath)[1].lower()
return ext in executable_extensions
else: # Unix/Linux/Mac
return os.access(filepath, os.X_OK)
# ------------------------------------------------------------------
# 加载 / 重载
# ------------------------------------------------------------------
def _load_skills(self):
"""扫描技能目录,加载所有技能文件(可执行文件或 Markdown 文件)。"""
skills_dir = constants.SKILLS_DIR
self.skills = {}
if not os.path.exists(skills_dir):
self.logger.info(f"技能目录不存在: {skills_dir}")
return
skill_files = [
f for f in os.listdir(skills_dir)
if os.path.isfile(os.path.join(skills_dir, f))
and not f.startswith('_')
and not f.startswith('.')
]
for skill_file in skill_files:
skill_path = os.path.join(skills_dir, skill_file)
skill_name = os.path.splitext(skill_file)[0]
is_markdown = self._is_markdown_file(skill_file)
if is_markdown:
# Markdown 格式技能:直接读取内容
summary, content = self._read_markdown_skill(skill_path)
if content:
self.skills[skill_name] = {
"path": skill_path,
"file": skill_file,
"summary": summary,
"content": content,
"is_markdown": True,
"is_executable": False
}
self.logger.info(f"已加载 Markdown 技能: {skill_name} — {summary}")
else:
self.logger.warning(f"Markdown 技能文件为空(跳过): {skill_file}")
else:
# 可执行文件格式技能:执行 --help 获取简介
if not self._is_executable(skill_path):
self.logger.warning(f"技能文件不可执行(跳过): {skill_file}")
continue
help_output = self._run_help(skill_path)
summary = self._first_nonempty_line(help_output) or f"扩展技能: {skill_name}"
self.skills[skill_name] = {
"path": skill_path,
"file": skill_file,
"summary": summary,
"content": help_output, # 完整的 --help 输出作为内容
"is_markdown": False,
"is_executable": True
}
self.logger.info(f"已加载可执行技能: {skill_name} — {summary}")
self.logger.info(f"共加载 {len(self.skills)} 个技能({len([s for s in self.skills.values() if s['is_markdown']])} 个 Markdown 技能)")
def reload(self) -> int:
"""重新扫描技能目录,返回加载的技能数量。"""
self._load_skills()
return len(self.skills)
# ------------------------------------------------------------------
# 查询
# ------------------------------------------------------------------
def search(self, keyword: str) -> List[Dict]:
"""在技能名称、简介和内容中搜索关键字,返回匹配项列表。"""
kw = keyword.lower()
results = []
for name, info in self.skills.items():
match = (
kw in name.lower() or
kw in info["summary"].lower() or
kw in info.get("content", "").lower()
)
if match:
results.append({
"name": name,
"path": info["path"],
"summary": info["summary"],
"content": info.get("content", ""),
"is_markdown": info.get("is_markdown", False),
"is_executable": info.get("is_executable", False)
})
return results
def get_skill_content(self, skill_name: str) -> Optional[str]:
"""获取技能的内容(Markdown 技能返回完整内容,可执行技能返回 --help 输出)"""
info = self.skills.get(skill_name)
if info:
return info.get("content")
return None
def get_skill(self, skill_name: str) -> Optional[Dict]:
"""获取单个技能的完整信息"""
return self.skills.get(skill_name)
def get_all(self) -> List[Dict]:
"""返回所有技能的列表(含名称、路径、简介和内容)。"""
return [
{
"name": name,
"path": info["path"],
"summary": info["summary"],
"content": info.get("content", ""),
"is_markdown": info.get("is_markdown", False),
"is_executable": info.get("is_executable", False)
}
for name, info in self.skills.items()
]
def count(self) -> int:
return len(self.skills)
class SearchSkillsTool(Tool):
"""扩展技能搜索工具 - 按关键字搜索 ~/.aibox/skills/ 下的技能文件。
支持两种技能格式:
1. 可执行脚本/程序:通过 --help 获取简介和完整使用说明
2. Markdown 文件 (.md):第一行为简介,其余为技能内容
工作流程:
1. 用关键字调用本工具,获取匹配技能的名称和简介列表
2. 对于 Markdown 技能,可以直接读取 content 字段获取完整技能内容
3. 对于可执行技能,通过 run_command 执行 "技能名 --help" 查看完整命令手册
4. 根据内容,使用相应方式调用技能
"""
def __init__(self, skill_manager: SkillManager, logger: Optional[Logger] = None):
super().__init__(logger)
self.skill_manager = skill_manager
def get_name(self) -> str:
return "search_skills"
def get_description(self) -> str:
return (
"扩展技能搜索工具。\n\n"
"用关键字搜索 ~/.aibox/skills/ 目录下的技能文件。\n"
"支持两种技能格式:\n"
" 1. 可执行脚本/程序:通过 --help 获取简介和完整使用说明\n"
" 2. Markdown 文件 (.md):第一行为简介,其余为技能内容\n\n"
"使用方法:\n"
"1. 调用本工具搜索关键字,获取匹配技能列表(空字符串列出全部)。\n"
"2. 对于 Markdown 技能:直接读取返回的 content 字段获取完整技能内容。\n"
"3. 对于可执行技能:用 run_command 执行 \"技能名 --help\" 获取完整命令手册。\n"
"4. 按照内容说明,使用相应方式调用该技能。"
)
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": (
"搜索关键字,在技能名称、简介和内容中进行匹配。"
"传入空字符串可列出全部可用技能。"
)
},
"show_content": {
"type": "boolean",
"description": (
"是否在结果中显示技能内容。"
"对于 Markdown 技能,这会影响返回结果的详细程度。"
"默认 False(只显示名称和简介)。"
),
"default": False
}
},
"required": ["keyword"]
}
def execute(self, keyword: str = "", show_content: bool = False, **kwargs) -> str:
kw = keyword.strip()
if kw:
results = self.skill_manager.search(kw)
header = f"🔍 搜索 '{kw}' 的结果(共 {len(results)} 个匹配技能):"
empty_msg = (
f"❌ 未找到与 '{kw}' 相关的技能。\n"
"提示:传入空字符串可列出所有可用技能。"
)
else:
results = self.skill_manager.get_all()
header = f"🔧 所有可用技能(共 {len(results)} 个):"
empty_msg = "❌ ~/.aibox/skills/ 目录下暂无可用技能。"
if not results:
return empty_msg
lines = [header]
for item in results:
skill_type = "📄 Markdown" if item.get('is_markdown') else "⚙️ 可执行"
lines.append(f"\n {skill_type} • {item['name']}")
lines.append(f" 简介: {item['summary']}")
if show_content and item.get('content'):
content = item['content']
if item.get('is_markdown'):
preview = content[:500]
if len(content) > 500:
preview += "...\n (内容较长,已截断,请直接读取技能文件获取完整内容)"
lines.append(f" 内容预览:\n {preview}")
else:
preview = content[:200] if content else "(无帮助输出)"
if len(content) > 200:
preview += "..."
lines.append(f" 帮助预览: {preview}")
else:
if item.get('is_markdown'):
lines.append(f" 💡 提示: 设置 show_content=true 可查看技能内容")
else:
lines.append(f" 💡 提示: 用 run_command 执行 \"{item['name']} --help\" 查看完整手册")
lines.append("")
lines.append("📖 技能调用方式:")
lines.append(" • Markdown 技能: 读取 content 字段获取完整技能指令")
lines.append(" • 可执行技能: 用 run_command 执行 '技能名 --help' 查看手册")
return "\n".join(lines)
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行搜索技能"""
keyword = kwargs.get("keyword", "")
show_content = kwargs.get("show_content", False)
output_queue.put(("line", f"\n🔍 搜索技能: {keyword if keyword else '(全部)'}\n"))
output_queue.put(("line", "-" * 50 + "\n"))
# 直接传递 kwargs,避免重复参数
result = self.execute(**kwargs)
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
# ==================== 技能热重载工具 ====================
class ReloadSkillsTool(Tool):
"""技能热重载工具 - 重新加载所有扩展技能"""
def __init__(self, tool_registry: ToolRegistry, logger: Optional[Logger] = None):
super().__init__(logger)
self.tool_registry = tool_registry
def get_name(self) -> str:
return "reload_skills"
def get_description(self) -> str:
return """技能热重载工具 - 重新加载所有扩展技能
当你添加、修改或删除了 ~/.aibox/skills/ 目录下的技能文件后,
调用此工具可以立即生效,无需重启系统。"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"confirm": {
"type": "boolean",
"description": "确认重新加载技能",
"default": True
}
}
}
def execute(self, confirm: bool = True, **kwargs) -> str:
"""执行技能热重载"""
if not confirm:
return "❌ 技能重载已取消,如需重载请设置 confirm=true"
self.logger.info("开始执行技能热重载...")
try:
count = self.tool_registry.reload_skills()
# 从 SkillManager 获取更新后的技能列表(含简介)
skill_manager = self.tool_registry.skill_manager
if skill_manager:
all_skills = skill_manager.get_all()
skill_lines = "\n".join(
f" • {s['name']} — {s['summary']}"
for s in all_skills[:10]
)
data = {
"重新加载的技能数量": count,
"技能目录": constants.SKILLS_DIR,
"技能列表(含简介)": skill_lines or "(无技能)",
}
if count > 10:
data["提示"] = f"还有 {count - 10} 个技能未显示"
else:
data = {
"重新加载的技能数量": count,
"技能目录": constants.SKILLS_DIR,
}
return self.format_result(True, i18n.get('skills_reloaded', count), data)
except Exception as e:
self.logger.error(f"技能热重载失败: {e}")
return i18n.get('skills_reload_failed', str(e))
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行技能热重载"""
output_queue.put(("line", "\n🔄 执行技能热重载...\n"))
output_queue.put(("line", "-" * 50 + "\n"))
result = self.execute(**kwargs)
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
# ==================== 询问工具 ========================
class AskUserTool(Tool):
"""智能体遇到了麻烦,主动向用户问一个问题"""
def get_name(self) -> str:
return "ask_user"
def get_description(self) -> str:
return "你遇到了一个问题,想向用户询问一个问题的工具。"
def get_parameters(self) -> Dict:
''' TODO: 调用子代理生成提示词 '''
return {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": (
"你现在遇到一个问题,你想询问用户,现在提供你的问题。"
)
}
},
"required": []
}
def execute(self, question: str, **kwargs) -> str:
return (
"你遇到了想搞清楚的事情: " + question + "\n你现在可以询问用户。"
)
# ==================== 安装技能工具 ====================
class InstallSkillTool(Tool):
"""技能安装指引工具 - 返回如何将可执行文件/脚本安装为扩展技能的说明"""
def get_name(self) -> str:
return "install_skill"
def get_description(self) -> str:
return (
"技能安装指引工具。\n"
"返回将可执行文件或脚本安装为扩展技能的完整步骤说明,"
"包括可执行性检查、--help 验证和部署方法。"
)
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"skill_path": {
"type": "string",
"description": (
"待安装的技能文件路径(可选)。"
"若提供,说明中会带入该路径作为示例;"
"不提供则返回通用说明。"
)
}
},
"required": []
}
def execute(self, skill_path: str = "", **kwargs) -> str:
skills_dir = constants.SKILLS_DIR
# 如果给了具体路径,在说明里带入,否则用占位符
src = skill_path.strip() if skill_path else "/path/to/your_skill"
# 去掉扩展名作为目标名称示例
base = os.path.splitext(os.path.basename(src))[0] if src else "your_skill"
dst = os.path.join(skills_dir, base)
guide = f"""📦 扩展技能安装指引
{"=" * 55}
要将一个可执行文件或脚本安装为扩展技能,请按以下步骤操作:
─────────────────────────────────────────────────────
步骤 1:确认文件可执行
─────────────────────────────────────────────────────
运行以下命令,确认文件有可执行权限:
ls -l {src}
如果没有执行权限,使用以下命令添加:
chmod +x {src}
然后直接运行一次,确认不会报错:
{src}
─────────────────────────────────────────────────────
步骤 2:验证 --help 输出是否完整
─────────────────────────────────────────────────────
技能必须支持 --help 选项,且:
• 第一行是一句话简介(系统用它作为技能摘要)
• 其余行说明参数用法和使用示例
运行以下命令验证:
{src} --help
确认输出的第一行能清楚描述该技能的用途。
─────────────────────────────────────────────────────
步骤 3:复制到技能目录并去掉扩展名
─────────────────────────────────────────────────────
技能目录:{skills_dir}
复制命令(自动去掉扩展名):
cp {src} {dst}
chmod +x {dst}
⚠️ 注意:目标文件名不能有扩展名(如 .py、.sh),
系统以纯文件名作为技能名称。
─────────────────────────────────────────────────────
步骤 4:让系统识别新技能
─────────────────────────────────────────────────────
复制完成后,调用热重载工具使其立即生效:
reload_skills confirm=true
之后可用 search_skills 验证技能已加载:
search_skills keyword="{base}"
{"=" * 55}
完成以上步骤后,即可通过 run_command 直接调用该技能。"""
return guide
# ==================== 创建扩展技能工具 ====================
class CreateSkillTool(Tool):
"""创建扩展技能工具 - 返回提示词,驱动智能体自行编写并部署可执行脚本。
工具本身不调用 LLM,只输出一段结构化的指令提示词。
智能体读到提示词后,会自行:
1. 编写满足接口要求的 Python 脚本
2. 用 write_file 写入 ~/.aibox/skills/<skill_name>
3. 用 run_command 赋予可执行权限
4. 调用 reload_skills 热重载
"""
def get_name(self) -> str:
return "create_skill"
def get_description(self) -> str:
return (
"创建扩展技能工具。\n"
"返回一段指令提示词,驱动你自行编写并部署一个满足技能接口要求的可执行脚本。\n\n"
"脚本接口要求:\n"
" • 首行 shebang:#!/usr/bin/env python3\n"
" • 支持 --help 选项(使用 argparse)\n"
" • --help 输出的第一行(argparse description)必须是该技能的一句话简介\n"
" • epilog 中包含完整的参数说明和至少一个使用示例"
)
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"skill_name": {
"type": "string",
"description": (
"技能名称(部署后的文件名,不含扩展名)。"
"只能包含字母、数字、连字符和下划线,例如:fetch-weather、calc-bmi。"
)
},
"description": {
"type": "string",
"description": (
"对该技能的详细需求描述:它应该做什么、接受哪些输入参数、"
"返回什么输出、有哪些典型使用场景。描述越详细,生成质量越高。"
)
}
},
"required": ["skill_name", "description"]
}
def execute(self, skill_name: str = "", description: str = "", **kwargs) -> str:
# ---- 参数校验 ----
if not skill_name or not skill_name.strip():
return "❌ 错误:skill_name 不能为空"
if not description or not description.strip():
return "❌ 错误:description 不能为空"
if not re.match(r'^[A-Za-z0-9_\-]+$', skill_name.strip()):
return "❌ 错误:skill_name 只能包含字母、数字、连字符(-)和下划线(_)"
skill_name = skill_name.strip()
skills_dir = constants.SKILLS_DIR
skill_path = os.path.join(skills_dir, skill_name)
# ---- 返回给智能体的指令提示词 ----
prompt = f"""【任务】创建扩展技能:{skill_name}
请按照以下步骤,自行完成该扩展技能的编写和部署:
─────────────────────────────────────────────────
步骤 1:编写 Python 脚本
─────────────────────────────────────────────────
根据以下需求,编写一个完整可运行的 Python 3 脚本:
需求描述:
{description.strip()}
脚本必须满足的接口约定:
1. 第一行为 shebang:#!/usr/bin/env python3
2. 使用 argparse 实现 --help 选项,其中:
- ArgumentParser 的 description 参数填写对该脚本功能的一句话简介(这是 --help 的第一行,系统会用它作为技能摘要)
- epilog 参数中包含完整的参数说明和至少一个使用示例
3. 功能完整可运行,包含必要的错误处理
─────────────────────────────────────────────────
步骤 2:写入技能目录
─────────────────────────────────────────────────
使用 write_file 工具,将完整脚本内容写入:
{skill_path}
─────────────────────────────────────────────────
步骤 3:赋予可执行权限
─────────────────────────────────────────────────
使用 run_command 工具执行:
chmod +x {skill_path}
─────────────────────────────────────────────────
步骤 4:验证并热重载
─────────────────────────────────────────────────
使用 run_command 工具执行以下命令验证 --help 输出是否正常:
{skill_path} --help
确认无误后,调用 reload_skills 工具使新技能立即生效:
reload_skills confirm=true
─────────────────────────────────────────────────
完成后请告知用户技能已创建成功,并展示 --help 的第一行简介。"""
return prompt
# ==================== 更新身份工具 ====================
class UpdateIdentityTool(Tool):
"""更新用户身份工具 - 修改 00_IDENTITY.md 文件"""
def __init__(self, prompt_manager: PromptManager, logger: Optional[Logger] = None):
super().__init__(logger)
self.prompt_manager = prompt_manager
def get_name(self) -> str:
return "update_identity"
def get_description(self) -> str:
return """更新用户身份信息 - 修改 00_IDENTITY.md 文件
当你了解到用户的身份信息时,调用此工具更新身份文件。
如果文件不存在,会自动创建。"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "用户身份信息内容(Markdown格式),包含用户的身份、特点、偏好等信息"
},
"confirm": {
"type": "boolean",
"description": "确认更新身份信息",
"default": True
}
},
"required": ["content"]
}
def execute(self, content: str = None, confirm: bool = True, **kwargs) -> str:
if not content:
return "❌ 需要提供身份信息内容"
if not confirm:
return "❌ 身份更新已取消,如需更新请设置 confirm=true"
self.logger.info("开始更新用户身份信息...")
if self.prompt_manager.update_identity(content):
return self.format_result(True, i18n.get('identity_updated', self.prompt_manager.IDENTITY_FILE), {
"文件": self.prompt_manager.IDENTITY_FILE,
"路径": self.prompt_manager.get_prompts_dir_path()
})
else:
return self.format_result(False, "身份更新失败", {
"文件": self.prompt_manager.IDENTITY_FILE,
"路径": self.prompt_manager.get_prompts_dir_path()
})
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行更新身份"""
output_queue.put(("line", "\n👤 更新用户身份信息...\n"))
output_queue.put(("line", "-" * 50 + "\n"))
result = self.execute(**kwargs)
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
# ==================== 更新智能体自我认识工具 ====================
class UpdateSoulTool(Tool):
"""更新智能体自我认识工具 - 修改 99_SOUL.md 文件"""
def __init__(self, prompt_manager: PromptManager, logger: Optional[Logger] = None):
super().__init__(logger)
self.prompt_manager = prompt_manager
def get_name(self) -> str:
return "update_soul"
def get_description(self) -> str:
return """更新智能体的自我认识定义 - 修改 99_SOUL.md 文件
当你需要重新定义自己的身份、角色、性格、能力边界时,调用此工具更新自我认识文件。
如果文件不存在,会自动创建默认定义。"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "智能体自我认识定义内容(Markdown格式),包含:身份定义、角色描述、性格特点、能力边界、行为准则等"
},
"confirm": {
"type": "boolean",
"description": "确认更新自我认识定义",
"default": True
}
},
"required": ["content"]
}
def execute(self, content: str = None, confirm: bool = True, **kwargs) -> str:
if not content:
return "❌ 需要提供智能体自我认识定义内容"
if not confirm:
return "❌ 自我认识更新已取消,如需更新请设置 confirm=true"
self.logger.info("开始更新智能体自我认识定义...")
if self.prompt_manager.update_soul(content):
return self.format_result(True, "智能体自我认识已更新", {
"文件": self.prompt_manager.SOUL_FILE,
"路径": self.prompt_manager.get_prompts_dir_path()
})
else:
return self.format_result(False, "自我认识更新失败", {
"文件": self.prompt_manager.SOUL_FILE,
"路径": self.prompt_manager.get_prompts_dir_path()
})
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行更新自我认识"""
output_queue.put(("line", "\n🧠 更新智能体自我认识...\n"))
output_queue.put(("line", "-" * 50 + "\n"))
result = self.execute(**kwargs)
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
# ==================== 保存记忆并结束对话工具(增强版,修复版) ====================
class SaveMemoryAndEndConversationTool(Tool):
"""保存记忆并结束对话工具 - 增强版,同时更新智能体状态"""
def __init__(self, exit_callback: Callable, memory_db: MemoryDatabase, prompt_manager: PromptManager, logger: Optional[Logger] = None):
super().__init__(logger)
self.exit_callback = exit_callback
self.memory_db = memory_db
self.prompt_manager = prompt_manager
def get_name(self) -> str:
return "save_memory_and_end_conversation"
def get_description(self) -> str:
return """保存记忆并结束当前对话 - 必须提供详细的对话总结和当前状态!
使用此工具时,AI会:
1. 将本次对话的重要内容保存到记忆库
2. 提供完整的对话总结
3. 描述自己现在的状态、事情的走向等,保存到 100_STATUS.md
4. 礼貌地结束对话"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "详细的对话总结(至少50字),包含:讨论的主题、关键问题、重要结论、达成的共识等"
},
"topic": {
"type": "string",
"description": "对话主题(用于记忆分类,默认自动提取)"
},
"reason": {
"type": "string",
"description": "结束对话的原因",
"default": "话题讨论完成"
},
"current_status": {
"type": "string",
"description": "当前的智能体状态描述,包含:当前进展、下一步计划、待处理事项、整体走向等"
},
"next_goals": {
"type": "string",
"description": "接下来的目标或计划(可选)"
}
},
"required": ["summary", "current_status"]
}
def execute(self, summary: str = None, topic: str = None, reason: str = "话题讨论完成",
current_status: str = None, next_goals: str = None, **kwargs) -> str:
if summary is None or len(summary) < 50:
return "❌ 请提供更详细的对话总结(至少50字),以便保存到记忆库"
if current_status is None:
return "❌ 请提供当前状态描述,以便更新 100_STATUS.md"
self.logger.info(f"AI保存记忆并结束对话: {reason}")
# 保存到记忆库
memory_result = ""
if self.memory_db and topic:
memory_id = self.memory_db.add_memory(
topic=topic,
summary=summary,
importance=3 # 提高重要性,因为是对话总结
)
self.logger.info(f"对话记忆已保存,ID: {memory_id}")
memory_result = f"记忆已保存,ID: {memory_id}"
# 构建状态内容
status_content = f"【当前状态】\n{current_status}\n\n"
if next_goals:
status_content += f"【下一步计划】\n{next_goals}\n\n"
status_content += f"【对话总结】\n{summary}"
# 更新状态文件
status_updated = self.prompt_manager.update_status(status_content)
if self.exit_callback:
self.exit_callback(reason, summary)
data = {
"原因": reason,
"总结预览": summary[:100] + "..." if len(summary) > 100 else summary,
"状态更新": "✅ 已更新" if status_updated else "❌ 更新失败",
"记忆已保存": True
}
if memory_result:
data["记忆"] = memory_result
return self.format_result(True, "对话记忆已保存,状态已更新,对话结束", data)
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行结束对话"""
output_queue.put(("line", "\n💾 保存记忆并结束对话\n"))
output_queue.put(("line", "-" * 50 + "\n"))
result = self.execute(**kwargs)
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
# ==================== 备忘录工具(简化版,去掉任务类型) ====================
class MemoTool(Tool):
"""备忘录工具 - 用于管理任务(所有任务都是对话任务)"""
def __init__(self, memo_db: MemoDatabase, logger: Optional[Logger] = None):
super().__init__(logger)
self.memo_db = memo_db
def get_name(self) -> str:
return "memo"
def get_description(self) -> str:
return """任务管理工具 - 创建、查看、完成任务
支持:创建任务(支持一次性/重复、即时任务)、查看待办、标记完成、删除任务、搜索任务"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "操作类型",
"enum": ["add", "list", "get", "complete", "delete", "search", "stats"]
},
"title": {
"type": "string",
"description": "任务标题(用于add操作)"
},
"content": {
"type": "string",
"description": "任务内容(可选,用于add操作)"
},
"reminder_time": {
"type": "string",
"description": "执行时间(格式:YYYY-MM-DD HH:MM:SS,或相对时间如 '+1h', '+30m', 'tomorrow 9am')"
},
"reminder_minutes": {
"type": "integer",
"description": "延迟分钟数(从现在开始,用于快速设置任务)"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "标签列表"
},
"priority": {
"type": "integer",
"description": "优先级 1-5(默认1)",
"default": 1
},
"repeat_type": {
"type": "string",
"description": "重复类型(none=一次性, daily=每天, weekly=每周, monthly=每月, custom=自定义)",
"enum": ["none", "daily", "weekly", "monthly", "custom"],
"default": "none"
},
"repeat_interval": {
"type": "integer",
"description": "自定义重复间隔,当repeat_type='custom'时使用",
"default": 0
},
"repeat_interval_unit": {
"type": "string",
"description": "重复间隔单位(minutes=分钟, hours=小时, days=天),当repeat_type='custom'时使用",
"enum": ["minutes", "hours", "days"],
"default": "days"
},
"repeat_end_time": {
"type": "string",
"description": "重复结束时间,None表示永久重复"
},
"is_immediate": {
"type": "boolean",
"description": "是否为即时任务(立即执行)",
"default": False
},
"memo_id": {
"type": "integer",
"description": "任务ID(用于get、complete、delete操作)"
},
"memo_ids": {
"type": "array",
"items": {"type": "integer"},
"description": "任务ID列表(用于批量删除)"
},
"force_complete": {
"type": "boolean",
"description": "强制完成(忽略重复规则)",
"default": False
},
"keyword": {
"type": "string",
"description": "搜索关键词(用于search操作)"
},
"include_completed": {
"type": "boolean",
"description": "是否包含已完成的任务(默认False)",
"default": False
},
"limit": {
"type": "integer",
"description": "返回数量限制(默认20)",
"default": 20
},
"confirm": {
"type": "boolean",
"description": "确认删除操作",
"default": False
}
},
"required": ["action"]
}
def _parse_reminder_time(self, reminder_input: Any) -> Optional[str]:
"""解析提醒时间输入"""
if not reminder_input:
return None
if isinstance(reminder_input, int):
reminder_time = datetime.now() + timedelta(minutes=reminder_input)
return reminder_time.strftime(constants.DATETIME_FORMAT)
if isinstance(reminder_input, str):
reminder_input = reminder_input.strip()
if reminder_input.startswith('+'):
match = re.match(r'\+(\d+)([mhd])', reminder_input)
if match:
value, unit = int(match.group(1)), match.group(2)
if unit == 'm':
delta = timedelta(minutes=value)
elif unit == 'h':
delta = timedelta(hours=value)
elif unit == 'd':
delta = timedelta(days=value)
else:
return None
reminder_time = datetime.now() + delta
return reminder_time.strftime(constants.DATETIME_FORMAT)
try:
reminder_time = datetime.strptime(reminder_input, constants.DATETIME_FORMAT)
return reminder_time.strftime(constants.DATETIME_FORMAT)
except ValueError:
pass
now = datetime.now()
reminder_input_lower = reminder_input.lower()
if 'tomorrow' in reminder_input_lower:
tomorrow = now + timedelta(days=1)
time_match = re.search(r'(\d{1,2})(?::(\d{2}))?\s*(am|pm)?', reminder_input_lower)
if time_match:
hour = int(time_match.group(1))
minute = int(time_match.group(2)) if time_match.group(2) else 0
ampm = time_match.group(3)
if ampm:
if ampm == 'pm' and hour < 12:
hour += 12
elif ampm == 'am' and hour == 12:
hour = 0
reminder_time = tomorrow.replace(hour=hour, minute=minute, second=0)
else:
reminder_time = tomorrow.replace(hour=9, minute=0, second=0)
return reminder_time.strftime(constants.DATETIME_FORMAT)
return None
def execute(self, action: str, **kwargs) -> str:
"""执行备忘录操作"""
try:
if action == "add":
return self._add_memo(kwargs)
elif action == "list":
return self._list_memos(kwargs)
elif action == "get":
return self._get_memo(kwargs)
elif action == "complete":
return self._complete_memo(kwargs)
elif action == "delete":
return self._delete_memo(kwargs)
elif action == "search":
return self._search_memos(kwargs)
elif action == "stats":
return self._get_stats(kwargs)
else:
return f"❌ 未知操作: {action}"
except Exception as e:
return f"❌ 任务操作错误: {str(e)}"
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行备忘录操作"""
action = kwargs.get("action")
# 发送开始执行的消息
output_queue.put(("line", f"\n📝 执行备忘录操作: {action}\n"))
output_queue.put(("line", "-" * 50 + "\n"))
# 执行操作
result = self.execute(**kwargs)
# 发送结果
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
def _add_memo(self, kwargs: Dict) -> str:
"""添加任务 - 所有任务都是对话任务,不需要task_type"""
title = kwargs.get("title")
if not title:
return "❌ 需要提供任务标题"
content = kwargs.get("content")
reminder_minutes = kwargs.get("reminder_minutes")
reminder_time_input = kwargs.get("reminder_time")
tags = kwargs.get("tags", [])
priority = kwargs.get("priority", 1)
repeat_type = kwargs.get("repeat_type", "none")
repeat_interval = kwargs.get("repeat_interval", 0)
repeat_interval_unit = kwargs.get("repeat_interval_unit", "days")
repeat_end_time = kwargs.get("repeat_end_time")
is_immediate = kwargs.get("is_immediate", False)
if repeat_type not in ["none", "daily", "weekly", "monthly", "custom"]:
return f"❌ 无效的重复类型: {repeat_type}"
if repeat_type == "custom" and repeat_interval <= 0:
return "❌ 自定义重复需要设置有效的 repeat_interval (>0)"
reminder_time = None
if is_immediate:
# 即时任务:设置为当前时间,确保立即执行
reminder_time = datetime.now().strftime(constants.DATETIME_FORMAT)
elif reminder_minutes:
reminder_time = self._parse_reminder_time(reminder_minutes)
elif reminder_time_input:
reminder_time = self._parse_reminder_time(reminder_time_input)
else:
# 默认30分钟后
reminder_time = self._parse_reminder_time(30)
memo_id = self.memo_db.add_memo(
title=title,
content=content,
reminder_time=reminder_time,
tags=tags,
priority=priority,
created_by="AI",
source="user",
repeat_type=repeat_type,
repeat_interval=repeat_interval,
repeat_interval_unit=repeat_interval_unit,
repeat_end_time=repeat_end_time,
is_immediate=is_immediate
)
data = {
"任务ID": memo_id,
"标题": title,
}
if is_immediate:
data["任务类型"] = "即时任务"
else:
if reminder_time:
data["执行时间"] = reminder_time
if repeat_type != "none":
if repeat_type == "custom":
repeat_info = f"重复类型: 每{repeat_interval} {repeat_interval_unit}"
else:
repeat_map = {
'daily': '每天',
'weekly': '每周',
'monthly': '每月'
}
repeat_info = f"重复类型: {repeat_map.get(repeat_type, repeat_type)}"
if repeat_end_time:
repeat_info += f", 结束于: {repeat_end_time}"
else:
repeat_info += ", 永久重复"
data["重复信息"] = repeat_info
return self.format_result(True, "任务已创建", data)
def _list_memos(self, kwargs: Dict) -> str:
"""列出任务"""
include_completed = kwargs.get("include_completed", False)
limit = kwargs.get("limit", 20)
memos = self.memo_db.get_all_memos(include_completed, limit)
if not memos:
return "📭 没有找到任务"
status = "所有" if include_completed else "待执行"
output = [f"📋 {status}任务 (共 {len(memos)} 个):"]
now = datetime.now()
for i, memo in enumerate(memos, 1):
status_icon = "✅" if memo['is_completed'] else "⏰"
priority_icon = "🔴" * memo['priority']
source_tag = f"[{memo.get('source', '未知')}]" if memo.get('source') else ""
immediate_tag = "⚡" if memo.get('is_immediate') else ""
repeat_tag = ""
if memo['repeat_type'] != 'none':
if memo['repeat_type'] == 'custom':
unit = memo.get('repeat_interval_unit', 'days')
unit_map = {'minutes': '分钟', 'hours': '小时', 'days': '天'}
repeat_tag = f" [每{memo['repeat_interval']}{unit_map.get(unit, unit)}]"
else:
repeat_map = {
'daily': ' [每天]',
'weekly': ' [每周]',
'monthly': ' [每月]'
}
repeat_tag = repeat_map.get(memo['repeat_type'], ' [重复]')
output.append(f"\n{i}. {status_icon}{immediate_tag} [ID: {memo['id']}] {source_tag}{repeat_tag} {memo['title']} {priority_icon}")
if memo['reminder_time']:
try:
reminder = datetime.strptime(memo['reminder_time'], constants.DATETIME_FORMAT)
if not memo['is_completed'] and reminder < now:
output.append(f" ⚠️ 已过期: {memo['reminder_time']}")
else:
output.append(f" ⏱️ 执行: {memo['reminder_time']}")
except:
output.append(f" ⏱️ 执行: {memo['reminder_time']}")
if memo.get('trigger_count', 0) > 0:
output.append(f" 🔄 已执行 {memo['trigger_count']} 次")
if memo['content']:
preview = memo['content'][:100] + "..." if len(memo['content']) > 100 else memo['content']
output.append(f" 📝 {preview}")
if memo['tags']:
output.append(f" 🏷️ {', '.join(memo['tags'])}")
return "\n".join(output)
def _get_memo(self, kwargs: Dict) -> str:
"""获取单个任务"""
memo_id = kwargs.get("memo_id")
if not memo_id:
return "❌ 需要提供任务ID"
memo = self.memo_db.get_memo(memo_id)
if not memo:
return f"❌ 未找到ID为 {memo_id} 的任务"
status = "✅ 已完成" if memo['is_completed'] else "⏰ 待执行"
if memo['is_completed']:
status += f" (完成于: {memo['completed_at']})"
immediate_tag = "⚡ 即时任务" if memo.get('is_immediate') else ""
output = [
f"📌 任务 #{memo_id} {immediate_tag}",
f"📋 状态: {status}",
f"📝 标题: {memo['title']}",
]
if memo.get('source'):
output.append(f"📎 来源: {memo['source']}")
if memo['repeat_type'] != 'none':
if memo['repeat_type'] == 'custom':
unit_map = {'minutes': '分钟', 'hours': '小时', 'days': '天'}
unit = memo.get('repeat_interval_unit', 'days')
repeat_info = f"🔄 重复: 每{memo['repeat_interval']} {unit_map.get(unit, unit)}"
else:
repeat_map = {
'daily': '每天',
'weekly': '每周',
'monthly': '每月'
}
repeat_info = f"🔄 重复: {repeat_map.get(memo['repeat_type'], memo['repeat_type'])}"
if memo['repeat_end_time']:
repeat_info += f", 结束于: {memo['repeat_end_time']}"
else:
repeat_info += ", 永久重复"
output.append(repeat_info)
if memo.get('trigger_count', 0) > 0:
output.append(f" 📊 已执行 {memo['trigger_count']} 次")
if memo.get('last_triggered_at'):
output.append(f" ⏱️ 上次执行: {memo['last_triggered_at']}")
if memo['content']:
output.append(f"📄 内容:\n{memo['content']}")
if memo['reminder_time']:
try:
now = datetime.now()
reminder = datetime.strptime(memo['reminder_time'], constants.DATETIME_FORMAT)
if not memo['is_completed'] and reminder < now:
output.append(f"⚠️ 执行时间 (已过期): {memo['reminder_time']}")
else:
output.append(f"⏰ 执行时间: {memo['reminder_time']}")
except:
output.append(f"⏰ 执行时间: {memo['reminder_time']}")
if memo['tags']:
output.append(f"🏷️ 标签: {', '.join(memo['tags'])}")
output.append(f"🔢 优先级: {'🔴' * memo['priority']} ({memo['priority']}/5)")
output.append(f"📅 创建时间: {memo['created_at']}")
output.append(f"👤 创建者: {memo['created_by'] or '系统'}")
return "\n".join(output)
def _complete_memo(self, kwargs: Dict) -> str:
"""完成任务"""
memo_id = kwargs.get("memo_id")
if not memo_id:
return "❌ 需要提供任务ID"
force_complete = kwargs.get("force_complete", False)
memo = self.memo_db.get_memo(memo_id)
if not memo:
return f"❌ 未找到ID为 {memo_id} 的任务"
if memo['is_completed'] and not force_complete:
return f"ℹ️ 任务 #{memo_id} 已经完成了"
if self.memo_db.complete_memo(memo_id, force_complete):
memo = self.memo_db.get_memo(memo_id)
data = {
"任务ID": memo_id,
"标题": memo['title']
}
if memo['repeat_type'] != 'none' and not memo['is_completed']:
data["下一次执行"] = memo['reminder_time']
data["已执行次数"] = memo.get('trigger_count', 0)
return self.format_result(True, "任务已处理,已更新下一次执行时间", data)
else:
return self.format_result(True, "任务已完成", data)
else:
return f"❌ 无法完成任务 #{memo_id}"
def _delete_memo(self, kwargs: Dict) -> str:
"""删除任务"""
confirm = kwargs.get("confirm", False)
if not confirm:
return "❌ 需要设置 confirm=True 来确认删除操作"
memo_id = kwargs.get("memo_id")
if memo_id:
if self.memo_db.complete_memo(memo_id, force_complete=True):
return self.format_result(True, f"任务 #{memo_id} 已删除")
else:
return f"❌ 无法删除任务 #{memo_id}"
else:
return "❌ 需要提供 memo_id"
def _search_memos(self, kwargs: Dict) -> str:
"""搜索任务"""
keyword = kwargs.get("keyword")
if not keyword:
return "❌ 需要提供搜索关键词"
include_completed = kwargs.get("include_completed", False)
limit = kwargs.get("limit", 20)
all_memos = self.memo_db.get_all_memos(include_completed, limit=100)
results = []
keyword_lower = keyword.lower()
for memo in all_memos:
if (keyword_lower in memo['title'].lower() or
(memo['content'] and keyword_lower in memo['content'].lower())):
results.append(memo)
if not results:
return f"❌ 未找到包含 '{keyword}' 的任务"
output = [f"🔍 搜索 '{keyword}' 找到 {len(results)} 个任务:"]
now = datetime.now()
for i, memo in enumerate(results[:limit], 1):
status_icon = "✅" if memo['is_completed'] else "⏰"
immediate_tag = "⚡" if memo.get('is_immediate') else ""
output.append(f"\n{i}. {status_icon}{immediate_tag} [ID: {memo['id']}] {memo['title']}")
if memo['reminder_time']:
try:
reminder = datetime.strptime(memo['reminder_time'], constants.DATETIME_FORMAT)
if not memo['is_completed'] and reminder < now:
output.append(f" ⚠️ 已过期: {memo['reminder_time']}")
else:
output.append(f" ⏱️ {memo['reminder_time']}")
except:
output.append(f" ⏱️ {memo['reminder_time']}")
return "\n".join(output)
def _get_stats(self, kwargs: Dict) -> str:
"""获取任务统计信息"""
stats = self.memo_db.get_memo_stats()
output = [
"📊 任务统计",
f" 总任务: {stats['total']} 个",
f" 待执行: {stats['pending']} 个",
f" 已过期: {stats['overdue']} 个",
f" 已完成: {stats['completed']} 个",
f" 即时任务: {stats['immediate']} 个"
]
if stats['overdue'] > 0:
output.append(f"\n⚠️ 有 {stats['overdue']} 个任务已过期,将自动执行")
return "\n".join(output)
# ==================== 记忆工具(修改版,支持流式输出) ====================
class MemoryTool(Tool):
"""记忆工具 - 用于存储、检索和删除对话记忆(支持流式输出,支持分页)"""
def __init__(self, memory_db: MemoryDatabase, logger: Optional[Logger] = None):
super().__init__(logger)
self.memory_db = memory_db
def get_name(self) -> str:
return "memory"
def get_description(self) -> str:
return """记忆管理工具 - 存储、检索和删除重要的对话信息
支持:保存总结、多关键词搜索记忆(支持分页)、查看最近的记忆、删除无用记忆"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "操作类型",
"enum": ["save", "search", "recent", "get", "delete"]
},
"summary": {"type": "string", "description": "要保存的总结内容(用于save操作)"},
"topic": {"type": "string", "description": "对话主题(用于save操作)"},
"content": {"type": "string", "description": "详细内容(可选,用于save操作)"},
"keywords": {
"type": "array", "items": {"type": "string"},
"description": "搜索关键词列表"
},
"query": {"type": "string", "description": "搜索关键词(单个关键词)"},
"match_all": {"type": "boolean", "default": False},
"memory_id": {"type": "integer"},
"memory_ids": {"type": "array", "items": {"type": "integer"}},
"importance": {"type": "integer", "default": 2},
"limit": {"type": "integer", "default": 10, "description": "每页条数(默认10)"},
"page": {"type": "integer", "default": 1, "description": "页码(从1开始)"},
"confirm": {"type": "boolean", "default": False}
},
"required": ["action"]
}
def execute(self, action: str, **kwargs) -> str:
try:
if action == "save":
return self._save_memory(kwargs)
elif action == "search":
return self._search_memories(kwargs)
elif action == "recent":
return self._get_recent_memories(kwargs)
elif action == "get":
return self._get_memory_by_id(kwargs)
elif action == "delete":
return self._delete_memory(kwargs)
else:
return f"❌ 未知操作: {action}"
except Exception as e:
return f"❌ 记忆操作错误: {str(e)}"
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行记忆操作"""
action = kwargs.get("action")
# 发送开始执行的消息
output_queue.put(("line", f"\n🧠 执行记忆操作: {action}\n"))
output_queue.put(("line", "-" * 50 + "\n"))
# 执行操作
result = self.execute(**kwargs)
# 发送结果
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
def _save_memory(self, kwargs: Dict) -> str:
summary = kwargs.get("summary")
if not summary:
return "❌ 需要提供总结内容"
topic = kwargs.get("topic", "未命名对话")
content = kwargs.get("content")
importance = kwargs.get("importance", constants.MEMORY_DEFAULT_IMPORTANCE)
memory_id = self.memory_db.add_memory(
topic=topic,
summary=summary,
content=content,
importance=importance
)
return self.format_result(True, "记忆已保存", {
"记忆ID": memory_id,
"主题": topic,
"总结": summary[:100] + "..." if len(summary) > 100 else summary
})
def _search_memories(self, kwargs: Dict) -> str:
"""搜索记忆 - 支持分页"""
keywords = kwargs.get("keywords", [])
query = kwargs.get("query", "")
match_all = kwargs.get("match_all", False)
page = kwargs.get("page", 1)
page_size = kwargs.get("limit", 10) # 使用 limit 作为 page_size
# 确保页码和每页条数有效
page = max(1, page)
page_size = max(1, min(50, page_size)) # 每页最多50条
# 处理关键词
if keywords and isinstance(keywords, list):
pass
elif query:
keywords = query.split()
else:
return "❌ 需要提供搜索关键词(keywords 或 query)"
if not keywords:
return "❌ 关键词列表为空"
# 先获取所有结果(不分页,用于计算总数)
all_results = self.memory_db.search_memories(keywords, match_all, limit=None)
if not all_results:
match_mode = "所有" if match_all else "任一"
return f"❌ 未找到包含 {match_mode} 关键词 {keywords} 的记忆"
# 计算分页信息
total_count = len(all_results)
total_pages = (total_count + page_size - 1) // page_size
# 检查页码是否有效
if page > total_pages and total_pages > 0:
return f"❌ 页码 {page} 超出范围,共有 {total_pages} 页"
# 计算当前页的起止索引
start_idx = (page - 1) * page_size
end_idx = min(start_idx + page_size, total_count)
# 获取当前页的结果
page_results = all_results[start_idx:end_idx]
# 构建输出
match_mode = "所有关键词" if match_all else "任一关键词"
output = [
f"🔍 搜索 {match_mode} {keywords} 找到 {total_count} 条记忆",
f"📄 第 {page}/{total_pages} 页 (每页 {page_size} 条)",
"-" * 40
]
for i, mem in enumerate(page_results, 1):
# 全局序号
global_idx = start_idx + i
output.append(f"\n{global_idx}. [ID: {mem['id']}] [{mem['end_time']}] {mem['topic']}")
output.append(f" 总结: {mem['summary'][:200]}")
if mem.get('tags'):
output.append(f" 标签: {', '.join(mem['tags'])}")
if mem.get('importance'):
output.append(f" 重要性: {'⭐' * mem['importance']} ({mem['importance']}/5)")
# 添加翻页提示
if total_pages > 1:
output.append(f"\n📖 翻页提示: 使用 page 参数查看更多结果")
output.append(f" 例如: memory search query='{query}' page={page + 1 if page < total_pages else page} limit={page_size}")
if page < total_pages:
output.append(f" ➡️ 下一页: page={page + 1}")
if page > 1:
output.append(f" ⬅️ 上一页: page={page - 1}")
return "\n".join(output)
def _get_recent_memories(self, kwargs: Dict) -> str:
"""获取最近的记忆 - 支持分页"""
page = kwargs.get("page", 1)
page_size = kwargs.get("limit", 10)
# 确保页码和每页条数有效
page = max(1, page)
page_size = max(1, min(50, page_size))
# 先获取所有结果
all_results = self.memory_db.get_recent_memories(limit=None)
if not all_results:
return "📭 没有找到记忆"
# 计算分页信息
total_count = len(all_results)
total_pages = (total_count + page_size - 1) // page_size
# 检查页码是否有效
if page > total_pages and total_pages > 0:
return f"❌ 页码 {page} 超出范围,共有 {total_pages} 页"
# 计算当前页的起止索引
start_idx = (page - 1) * page_size
end_idx = min(start_idx + page_size, total_count)
# 获取当前页的结果
page_results = all_results[start_idx:end_idx]
output = [f"📚 最近 {total_count} 条记忆 (第 {page}/{total_pages} 页,每页 {page_size} 条):"]
output.append("-" * 40)
for i, mem in enumerate(page_results, 1):
global_idx = start_idx + i
output.append(f"\n{global_idx}. [ID: {mem['id']}] [{mem['end_time']}] {mem['topic']}")
output.append(f" 总结: {mem['summary'][:100]}")
# 添加翻页提示
if total_pages > 1:
output.append(f"\n📖 翻页提示: 使用 page 参数查看更多结果")
output.append(f" 例如: memory recent page={page + 1 if page < total_pages else page} limit={page_size}")
return "\n".join(output)
def _get_memory_by_id(self, kwargs: Dict) -> str:
"""根据ID获取记忆"""
memory_id = kwargs.get("memory_id")
if not memory_id:
return "❌ 需要提供记忆ID"
memory = self.memory_db.get_memory_by_id(memory_id)
if not memory:
return f"❌ 未找到ID为 {memory_id} 的记忆"
output = [
f"📌 记忆 #{memory_id}",
f"主题: {memory['topic']}",
f"时间: {memory['start_time']} - {memory['end_time']}",
f"总结: {memory['summary']}",
]
if memory.get('content'):
output.append(f"详细内容:\n{memory['content']}")
if memory.get('tags'):
output.append(f"标签: {', '.join(memory['tags'])}")
if memory.get('importance'):
output.append(f"重要性: {'⭐' * memory['importance']} ({memory['importance']}/5)")
return "\n".join(output)
def _delete_memory(self, kwargs: Dict) -> str:
"""删除记忆"""
confirm = kwargs.get("confirm", False)
if not confirm:
return "❌ 需要设置 confirm=True 来确认删除操作"
memory_id = kwargs.get("memory_id")
memory_ids = kwargs.get("memory_ids", [])
deleted_count = 0
if memory_id:
if self.memory_db.delete_memory(memory_id):
deleted_count = 1
elif memory_ids:
deleted_count = self.memory_db.delete_memories_by_ids(memory_ids)
else:
return "❌ 需要提供 memory_id 或 memory_ids"
if deleted_count == 0:
return f"❌ 未找到要删除的记忆"
return self.format_result(True, f"成功删除 {deleted_count} 条记忆", {"删除数量": deleted_count})
# ==================== 网页工具(修改版,支持流式输出) ====================
class WebPage:
"""网页对象"""
def __init__(self, url: str, response: requests.Response = None):
self.url = url
self.status_code = None
self.headers = {}
self.html = ""
self.soup = None
self.error = None
self.fetch_time = None
if response:
self.status_code = response.status_code
self.headers = dict(response.headers)
self.html = response.text
self.fetch_time = datetime.now()
try:
self.soup = BeautifulSoup(self.html, 'html.parser')
except:
pass
@property
def title(self) -> Optional[str]:
if self.soup and self.soup.find('title'):
return self.soup.find('title').get_text(strip=True)
return None
@property
def text(self) -> str:
if self.soup:
return self.soup.get_text(separator='\n', strip=True)
return ""
@property
def links(self) -> List[str]:
links = []
if self.soup:
for a in self.soup.find_all('a', href=True):
href = a['href'].strip()
if href and not href.startswith('#') and not href.startswith('javascript:'):
links.append(urljoin(self.url, href))
return list(set(links))
class SimpleWebClient:
"""简单网页访问客户端"""
def __init__(self, timeout: int = None, delay: float = None, user_agent: str = None):
self.timeout = timeout or constants.WEB_TIMEOUT
self.delay = delay if delay is not None else constants.WEB_DELAY
self.session = requests.Session()
self.session.headers.update({
'User-Agent': user_agent or constants.WEB_USER_AGENT
})
self.last_request_time = 0
def _wait_if_needed(self):
if self.delay > 0:
elapsed = time.time() - self.last_request_time
if elapsed < self.delay:
time.sleep(self.delay - elapsed)
def get(self, url: str, **kwargs) -> WebPage:
self._wait_if_needed()
# 每次都从 ConfigManager 获取最新代理
proxies = ConfigManager().get_proxy()
try:
response = self.session.get(
url,
timeout=kwargs.get('timeout', self.timeout),
proxies=proxies, # 传入最新代理
**kwargs
)
response.raise_for_status()
page = WebPage(url, response)
self.last_request_time = time.time()
return page
except Exception as e:
page = WebPage(url)
page.error = str(e)
return page
class SimpleWebCrawlerTool(Tool):
"""简化版网页爬虫工具(支持流式输出)"""
def __init__(self, logger=None):
super().__init__(logger)
self.client = SimpleWebClient()
def get_name(self) -> str:
return "web"
def get_description(self) -> str:
return """简单易用的网页访问工具 - 一句话搞定!
支持:获取网页、提取内容、搜索关键词、下载文件"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["get", "text", "title", "links", "search"]
},
"url": {"type": "string"},
"keyword": {"type": "string"}
},
"required": ["action", "url"]
}
def execute(self, action: str, url: str, **kwargs) -> str:
try:
if action == "get":
page = self.client.get(url)
return self._format_page_info(page)
elif action == "text":
text = self.client.get(url).text
return f"📄 网页文本内容:\n\n{text[:1000]}{'...' if len(text)>1000 else ''}"
elif action == "title":
title = self.client.get(url).title
return f"📌 网页标题: {title}" if title else "❌ 未找到标题"
elif action == "links":
links = self.client.get(url).links
result = [f"🔗 找到 {len(links)} 个链接:"]
for i, link in enumerate(links[:20]):
result.append(f" {i+1}. {link}")
return "\n".join(result)
elif action == "search":
keyword = kwargs.get("keyword")
if not keyword:
return "❌ 需要提供 keyword 参数"
page = self.client.get(url)
if keyword.lower() in page.text.lower():
return f"🔎 在网页中找到关键词 '{keyword}'"
else:
return f"❌ 未找到关键词 '{keyword}'"
else:
return f"❌ 未知操作: {action}"
except Exception as e:
return f"❌ 执行错误: {str(e)}"
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行网页操作"""
action = kwargs.get("action")
url = kwargs.get("url")
# 发送开始执行的消息
output_queue.put(("line", f"\n🌐 执行网页操作: {action} - {url}\n"))
output_queue.put(("line", "-" * 50 + "\n"))
# 执行操作
result = self.execute(**kwargs)
# 发送结果
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
def _format_page_info(self, page: WebPage) -> str:
if page.error:
return f"❌ 获取失败: {page.error}"
lines = [
f"🌐 URL: {page.url}",
f"📊 状态码: {page.status_code}",
f"📌 标题: {page.title or '无标题'}",
f"🔗 链接数: {len(page.links)}",
]
if page.text:
preview = page.text[:200].replace('\n', ' ')
lines.append(f"📄 预览: {preview}...")
return "\n".join(lines)
# ==================== 文件操作工具(修改版,支持流式输出) ====================
class FileToolBase(Tool):
"""文件操作工具基类"""
def __init__(self, logger: Optional[Logger] = None):
super().__init__(logger)
def normalize_path(self, filepath: str) -> str:
return os.path.abspath(os.path.expanduser(filepath))
def check_path_safety(self, filepath: str, check_exists: bool = False) -> Tuple[bool, str, str]:
"""检查路径安全性"""
try:
abs_path = self.normalize_path(filepath)
if check_exists and not os.path.exists(abs_path):
return False, abs_path, f"❌ 文件不存在: {abs_path}"
# 检查文件大小
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)}"
class ReadFileTool(FileToolBase):
"""读取文件内容工具(支持流式输出)"""
def get_name(self) -> str:
return "read_file"
def get_description(self) -> str:
return "读取文件内容,支持指定起始行和结束行,或读取开头/结尾"
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"filepath": {"type": "string"},
"encoding": {"type": "string", "default": "utf-8"},
"start_line": {"type": "integer", "default": 1},
"end_line": {"type": "integer", "default": 0},
"max_lines": {"type": "integer", "default": 100},
"head": {"type": "boolean", "default": False},
"tail": {"type": "boolean", "default": False}
},
"required": ["filepath"]
}
def _calculate_line_range(self, line_count: int, start_line: int, end_line: int,
max_lines: int, head: bool, tail: bool) -> Tuple[int, int, str]:
if head:
start = 0
end = min(max_lines, line_count)
desc = f"开头 {end} 行"
elif tail:
start = max(0, line_count - max_lines)
end = line_count
desc = f"结尾 {line_count - start} 行"
elif end_line > 0:
start = max(0, start_line - 1)
end = min(end_line, line_count)
desc = f"第 {start_line} 行到第 {end_line} 行"
else:
start = max(0, start_line - 1)
if max_lines > 0:
end = min(start + max_lines, line_count)
desc = f"从第 {start_line} 行开始,最多 {max_lines} 行"
else:
end = line_count
desc = f"从第 {start_line} 行到文件末尾"
return start, end, desc
def execute(self, filepath: str, encoding: str = "utf-8",
start_line: int = 1, end_line: int = 0,
max_lines: int = 100, head: bool = False,
tail: bool = False, **kwargs) -> str:
if error := self.validate_params(["filepath"], filepath=filepath):
return error
is_safe, abs_path, error = self.check_path_safety(filepath, check_exists=True)
if not is_safe:
return error
try:
file_size = os.path.getsize(abs_path)
modified_time = datetime.fromtimestamp(os.path.getmtime(abs_path))
with open(abs_path, 'r', encoding=encoding) as f:
all_lines = f.readlines()
line_count = len(all_lines)
start, end, range_desc = self._calculate_line_range(
line_count, start_line, end_line, max_lines, head, tail
)
if end <= start:
return f"❌ 无效的行范围"
display_lines = all_lines[start:end]
data = {
"路径": abs_path,
"大小": f"{file_size} 字节 ({file_size/1024:.1f}KB)",
"最后修改": modified_time.strftime(constants.DATETIME_FORMAT),
"总行数": line_count,
"显示范围": f"{range_desc} (行 {start+1}-{end})"
}
if display_lines:
content_lines = []
for i, line in enumerate(display_lines):
line_num = start + i + 1
content = line.rstrip('\n\r')
content_lines.append(f" {line_num:4d} | {content}")
data["内容"] = "\n" + "\n".join(content_lines)
if end < line_count:
data["提示"] = f"还有 {line_count - end} 行未显示"
return self.format_result(True, "文件读取成功", data)
except UnicodeDecodeError:
return f"❌ 文件编码错误:无法使用 {encoding} 编码读取文件"
except PermissionError:
return f"❌ 权限错误:无法读取文件 {abs_path}"
except Exception as e:
return f"❌ 文件读取错误: {str(e)}"
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行文件读取"""
filepath = kwargs.get("filepath")
# 发送开始执行的消息
output_queue.put(("line", f"\n📄 读取文件: {filepath}\n"))
output_queue.put(("line", "-" * 50 + "\n"))
# 执行操作
result = self.execute(**kwargs)
# 发送结果
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
class WriteFileTool(FileToolBase):
"""写入文件工具(支持流式输出)"""
def get_name(self) -> str:
return "write_file"
def get_description(self) -> str:
return "文件写入工具 - 支持覆盖、追加"
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"filepath": {"type": "string"},
"content": {"type": "string"},
"mode": {"type": "string", "enum": ["w", "a"], "default": "w"},
"encoding": {"type": "string", "default": "utf-8"}
},
"required": ["filepath", "content"]
}
def execute(self, filepath: str = None, content: str = None, mode: str = "w",
encoding: str = "utf-8", **kwargs) -> str:
if error := self.validate_params(["filepath", "content"], filepath=filepath, content=content):
return error
is_safe, abs_path, error = self.check_path_safety(filepath)
if not is_safe:
return error
try:
directory = os.path.dirname(abs_path)
if directory and not os.path.exists(directory):
os.makedirs(directory, exist_ok=True)
file_exists = os.path.exists(abs_path)
old_size = os.path.getsize(abs_path) if file_exists else 0
with open(abs_path, mode, encoding=encoding) as f:
f.write(content)
new_size = len(content.encode(encoding))
action = "覆盖写入" if mode == "w" else "追加写入"
data = {
"路径": abs_path,
"大小": f"{new_size} 字节",
}
if file_exists:
data["变化"] = f"{old_size} -> {new_size} 字节"
else:
data["状态"] = "新文件已创建"
return self.format_result(True, f"文件{action}成功", data)
except PermissionError:
return f"❌ 权限错误:无法写入文件"
except Exception as e:
return f"❌ 文件写入错误: {str(e)}"
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行文件写入"""
filepath = kwargs.get("filepath")
# 发送开始执行的消息
output_queue.put(("line", f"\n📝 写入文件: {filepath}\n"))
output_queue.put(("line", "-" * 50 + "\n"))
# 执行操作
result = self.execute(**kwargs)
# 发送结果
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
class ListFilesTool(FileToolBase):
"""列出目录内容工具(支持流式输出)"""
def get_name(self) -> str:
return "list_files"
def get_description(self) -> str:
return "列出指定目录下的文件和子目录"
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"directory": {"type": "string", "default": "."},
"show_hidden": {"type": "boolean", "default": False}
}
}
def execute(self, directory: str = ".", show_hidden: bool = False, **kwargs) -> str:
is_safe, abs_path, error = self.check_path_safety(directory)
if not is_safe:
return error
if not os.path.exists(abs_path):
return f"❌ 目录不存在: {abs_path}"
if not os.path.isdir(abs_path):
return f"❌ 路径不是目录: {abs_path}"
try:
items = []
total_size = 0
file_count = 0
dir_count = 0
for item in sorted(os.listdir(abs_path)):
if not show_hidden and item.startswith('.'):
continue
item_path = os.path.join(abs_path, item)
if os.path.isdir(item_path):
items.append(f"📁 {item}/")
dir_count += 1
else:
size = os.path.getsize(item_path)
total_size += size
file_count += 1
if size < 1024:
size_str = f"{size}B"
elif size < 1024*1024:
size_str = f"{size/1024:.1f}KB"
else:
size_str = f"{size/1024/1024:.1f}MB"
items.append(f"📄 {item} ({size_str})")
data = {
"目录": abs_path,
"统计": f"{file_count} 个文件, {dir_count} 个目录",
"总大小": f"{total_size/1024:.1f}KB" if file_count > 0 else "0KB"
}
if items:
data["内容"] = "\n" + "\n".join(items[:50])
if len(items) > 50:
data["提示"] = f"还有 {len(items)-50} 个项目未显示"
else:
data["内容"] = "\n📭 目录为空"
return self.format_result(True, "目录列表成功", data)
except Exception as e:
return f"❌ 列出目录错误: {str(e)}"
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行列出目录"""
directory = kwargs.get("directory", ".")
# 发送开始执行的消息
output_queue.put(("line", f"\n📁 列出目录: {directory}\n"))
output_queue.put(("line", "-" * 50 + "\n"))
# 执行操作
result = self.execute(**kwargs)
# 发送结果
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
# ==================== 系统工具(修改版,支持流式输出) ====================
class GetCurrentTimeTool(Tool):
"""获取当前时间工具(支持流式输出)"""
def get_name(self) -> str:
return "get_current_time"
def get_description(self) -> str:
return "获取当前日期和时间"
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"format": {"type": "string", "enum": ["full", "date", "time"], "default": "full"}
}
}
def execute(self, format: str = "full", **kwargs) -> str:
now = datetime.now()
if format == "date":
result = now.strftime("%Y-%m-%d")
elif format == "time":
result = now.strftime("%H:%M:%S")
else:
result = now.strftime(constants.DATETIME_FORMAT)
return self.format_result(True, "当前时间", {"时间": result})
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行获取时间"""
output_queue.put(("line", "\n🕐 获取当前时间\n"))
output_queue.put(("line", "-" * 50 + "\n"))
result = self.execute(**kwargs)
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
class CalculatorTool(Tool):
"""计算器工具(支持流式输出)"""
def get_name(self) -> str:
return "calculator"
def get_description(self) -> str:
return "执行数学计算"
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"},
"operation": {"type": "string", "enum": ["add", "subtract", "multiply", "divide"]}
},
"required": ["a", "b", "operation"]
}
def execute(self, a: float, b: float, operation: str, **kwargs) -> str:
operations = {
"add": ("+", a + b),
"subtract": ("-", a - b),
"multiply": ("*", a * b),
"divide": ("/", a / b if b != 0 else None)
}
if operation not in operations:
return f"❌ 未知运算: {operation}"
op_symbol, result = operations[operation]
if result is None:
return "❌ 错误:除数不能为0"
data = {
"表达式": f"{a} {op_symbol} {b}",
"结果": result
}
return self.format_result(True, "计算成功", data)
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行计算"""
output_queue.put(("line", "\n🧮 执行计算\n"))
output_queue.put(("line", "-" * 50 + "\n"))
result = self.execute(**kwargs)
output_queue.put(("line", result + "\n"))
output_queue.put(("complete", result))
class CommandLineTool(Tool):
"""命令行执行工具 - 使用抽象的命令执行器(支持流式输出和超时控制)"""
def __init__(self, executor: CommandExecutor = None, logger=None):
super().__init__(logger)
self.executor = executor or SubprocessCommandExecutor(logger)
def get_name(self) -> str:
return "run_command"
def get_description(self) -> str:
return """执行任意系统命令,支持实时输出显示和超时控制"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "要执行的命令"
},
"timeout": {
"type": "integer",
"default": 30,
"description": "命令执行超时时间(秒),超时后将终止命令执行。默认30秒"
},
"stream": {
"type": "boolean",
"default": True,
"description": "是否实时输出命令执行结果"
},
"work_dir": {
"type": "string",
"default": ".",
"description": "命令执行的工作目录"
},
"env": {
"type": "object",
"description": "额外的环境变量"
}
},
"required": ["command"]
}
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
"""流式执行命令,支持超时控制"""
command = kwargs.get("command")
timeout = kwargs.get("timeout", 30)
work_dir = kwargs.get("work_dir", ".")
extra_env = kwargs.get("env", {})
if not command:
output_queue.put(("line", "❌ 错误:缺少命令参数\n"))
output_queue.put(("complete", "❌ 错误:缺少命令参数"))
return
# 发送开始执行的消息
output_queue.put(("line", f"\n🔧 执行命令: {command}\n"))
output_queue.put(("line", f"⏱️ 超时设置: {timeout} 秒\n"))
if work_dir != ".":
output_queue.put(("line", f"📁 工作目录: {work_dir}\n"))
output_queue.put(("line", "-" * 50 + "\n"))
# 准备环境变量
env = os.environ.copy()
if extra_env:
env.update(extra_env)
try:
# 使用 subprocess.Popen 实现流式输出,支持超时
process = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stdin=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True,
env=env,
cwd=os.path.expanduser(work_dir)
)
start_time = time.time()
output_lines = []
timeout_occurred = False
# 实时读取输出
while True:
# 检查超时
elapsed = time.time() - start_time
if timeout > 0 and elapsed > timeout:
process.terminate()
timeout_occurred = True
output_queue.put(("line", f"\n⏰ 命令执行超时({timeout}秒),已终止\n"))
break
# 使用非阻塞方式读取一行
import select
import sys
# 检查是否有输出可读
if sys.platform == 'win32':
# Windows 下使用 readline 带超时
try:
import msvcrt
if msvcrt.kbhit():
pass
line = process.stdout.readline()
if not line and process.poll() is not None:
break
if line:
line = line.rstrip('\n')
output_queue.put(("line", line + "\n"))
output_lines.append(line)
else:
time.sleep(0.05)
except:
line = process.stdout.readline()
if not line and process.poll() is not None:
break
if line:
line = line.rstrip('\n')
output_queue.put(("line", line + "\n"))
output_lines.append(line)
else:
# Unix 系统可以使用 select
import select
rlist, _, _ = select.select([process.stdout], [], [], 0.5)
if rlist:
line = process.stdout.readline()
if not line and process.poll() is not None:
break
if line:
line = line.rstrip('\n')
output_queue.put(("line", line + "\n"))
output_lines.append(line)
else:
# 检查进程是否结束
if process.poll() is not None:
# 读取剩余输出
remaining = process.stdout.read()
if remaining:
remaining = remaining.rstrip('\n')
if remaining:
output_queue.put(("line", remaining + "\n"))
output_lines.append(remaining)
break
# 如果超时,确保进程被终止
if timeout_occurred and process.poll() is None:
process.kill()
process.wait(timeout=2)
# 获取返回码
return_code = process.returncode if process.returncode is not None else -1
output_queue.put(("line", "-" * 50 + "\n"))
if timeout_occurred:
output_queue.put(("line", f"⏰ 命令执行超时({timeout}秒),返回码: {return_code}\n"))
else:
output_queue.put(("line", f"✅ 命令执行完成,返回码: {return_code}\n"))
# 准备完整结果
full_output = "\n".join(output_lines)
data = {
"输出": full_output if full_output else "(无输出)",
"返回码": return_code
}
if timeout_occurred:
result = self.format_result(False, f"命令执行超时({timeout}秒)", data)
else:
result = self.format_result(return_code == 0, "命令执行完成", data)
output_queue.put(("complete", result))
except subprocess.TimeoutExpired:
error_msg = f"❌ 命令执行超时({timeout}秒)"
output_queue.put(("line", error_msg + "\n"))
output_queue.put(("complete", error_msg))
except FileNotFoundError as e:
error_msg = f"❌ 命令执行错误: 命令未找到 - {str(e)}"
output_queue.put(("line", error_msg + "\n"))
output_queue.put(("complete", error_msg))
except PermissionError as e:
error_msg = f"❌ 命令执行错误: 权限不足 - {str(e)}"
output_queue.put(("line", error_msg + "\n"))
output_queue.put(("complete", error_msg))
except Exception as e:
error_msg = f"❌ 执行错误: {str(e)}"
output_queue.put(("line", error_msg + "\n"))
output_queue.put(("complete", error_msg))
finally:
# 确保进程被清理
if 'process' in locals() and process and process.poll() is None:
try:
process.terminate()
process.wait(timeout=2)
except:
process.kill()
def execute(self, command: str, timeout: int = 30, stream: bool = True,
work_dir: str = ".", env: Dict = None, **kwargs) -> str:
"""执行命令 - 支持超时控制"""
if error := self.validate_params(["command"], command=command):
return error
# 如果正在流式输出,直接返回空(实际输出通过队列)
if self.streaming_output and self.output_queue:
return ""
# 准备环境变量
exec_env = os.environ.copy()
if env:
exec_env.update(env)
# 根据stream参数决定使用流式还是非流式执行
if stream:
# 流式执行需要调用方处理输出队列
# 注意:流式执行会忽略这里的返回值,实际输出通过队列
self.executor.execute_streaming(
command=command,
output_queue=self.output_queue,
timeout=timeout
)
return ""
else:
# 非流式执行
try:
# 使用 subprocess.run 执行命令,支持超时和工作目录
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=timeout,
env=exec_env,
cwd=os.path.expanduser(work_dir)
)
data = {}
if result.stdout:
data["输出"] = result.stdout.strip()
if result.stderr:
data["错误"] = result.stderr.strip()
data["返回码"] = result.returncode
if not result.stdout and not result.stderr:
return self.format_result(True, "命令执行成功,无输出", data)
return self.format_result(result.returncode == 0, "命令执行完成", data)
except subprocess.TimeoutExpired:
return f"❌ 命令执行超时({timeout}秒)"
except FileNotFoundError as e:
return f"❌ 命令执行错误: 命令未找到 - {str(e)}"
except PermissionError as e:
return f"❌ 命令执行错误: 权限不足 - {str(e)}"
except Exception as e:
return f"❌ 执行错误: {str(e)}"
class SetProxyTool(Tool):
"""设置 SOCKS5 代理的工具"""
def get_name(self) -> str:
return "set_proxy"
def get_description(self) -> str:
return """设置智能体的 SOCKS5 代理配置。
调用后所有 HTTP/HTTPS 请求(包括网页访问、API 调用)都会通过该代理。
可随时修改,即时生效。"""
def get_parameters(self) -> Dict:
return {
"type": "object",
"properties": {
"host": {"type": "string", "description": "代理服务器地址,如 127.0.0.1"},
"port": {"type": "integer", "description": "代理端口,如 1080"},
"username": {"type": "string", "description": "用户名(可选)"},
"password": {"type": "string", "description": "密码(可选)"},
"enabled": {"type": "boolean", "description": "是否启用代理,默认 true"},
"save": {"type": "boolean", "description": "是否保存到配置文件(持久化),默认 true"}
},
"required": ["host", "port"]
}
def execute(self, host: str, port: int, username: str = None, password: str = None,
enabled: bool = True, save: bool = True, **kwargs) -> str:
config = ConfigManager()
config.set_proxy(host, port, username, password, enabled, save)
proxy_info = config.get_proxy_info()
data = {
"代理状态": "启用" if proxy_info["enabled"] else "禁用",
"服务器": f"{proxy_info['host']}:{proxy_info['port']}",
"认证": "有" if proxy_info.get("username") else "无",
"持久化": "已保存" if save else "仅内存"
}
return self.format_result(True, "代理设置成功", data)
class GetProxyTool(Tool):
"""查看当前代理配置"""
def get_name(self) -> str:
return "get_proxy"
def get_description(self) -> str:
return "查看当前 SOCKS5 代理配置信息"
def get_parameters(self) -> Dict:
return {"type": "object", "properties": {}}
def execute(self, **kwargs) -> str:
config = ConfigManager()
info = config.get_proxy_info()
if info["enabled"]:
data = {
"代理状态": "启用",
"服务器": f"{info['host']}:{info['port']}",
"认证": "有" if info.get("username") else "无"
}
return self.format_result(True, "当前代理配置", data)
else:
return "❌ 当前代理未启用,所有请求走直连。"
# ==================== AI聊天类(修改版:添加重置对话记忆和重新加载系统提示词功能,压缩静默执行) ====================
class DeepSeekChat:
"""单AI聊天类"""
def __init__(self, api_key: str = None, max_history: int = None,
system_prompt: str = None, debug_level: int = None, exit_callback: Callable = None,
history_manager: ConversationHistory = None, memory_db: MemoryDatabase = None,
memo_db: MemoDatabase = None, command_executor: CommandExecutor = None,
prompt_manager: PromptManager = None):
self.name = "DeepSeek"
self.debug_level = debug_level if debug_level is not None else Logger("").level
self.logger = Logger(self.name, self.debug_level)
self.config = ConfigManager()
self.history_manager = history_manager
self.memory_db = memory_db
self.memo_db = memo_db
self.prompt_manager = prompt_manager or PromptManager()
self.history_loaded = False
self.conversation_active = True
self.messages = []
# 添加对话压缩器
self.compressor = ConversationCompressor(self, self.logger)
api_key_env = self.config.get("api.api_key_env")
self.api_key = api_key or os.environ.get(api_key_env)
if not self.api_key:
try:
home = os.environ.get("HOME", os.environ["USERPROFILE"])
key_path = os.path.join(home, ".cert", "deepseek.key")
with open(f"{key_path}", "r") as f:
self.api_key = f.read().strip()
except Exception as e:
raise RuntimeError(f"请设置环境变量 {api_key_env} 或在配置文件中设置API Key")
self.max_history = max_history or constants.DEFAULT_MAX_HISTORY
self.preserve_system = constants.PRESERVE_SYSTEM_PROMPTS
self.stats = {"api_calls": 0, "tool_calls": 0, "errors": []}
self._init_system_prompts(system_prompt)
self.tool_registry = ToolRegistry(self.logger)
self._register_tools(exit_callback, command_executor)
def _init_system_prompts(self, system_prompt: str = None):
"""初始化系统提示词 - 从 PromptManager 加载"""
if system_prompt:
# 如果明确指定了 system_prompt,使用它
self._add_message("system", system_prompt)
self.logger.info("使用指定的系统提示词")
else:
# 从 PromptManager 加载所有提示词
prompts = self.prompt_manager.get_prompts_list()
if prompts:
for prompt in prompts:
if prompt['content'].strip():
self._add_message("system", prompt['content'].strip())
self.logger.info(f"已加载 {len(prompts)} 个系统提示词")
else:
# 如果没有任何提示词,添加一个默认的
default_prompt = "你是一个智能AI助手。"
self._add_message("system", default_prompt)
self.logger.info("使用默认系统提示词")
# 在 DeepSeekChat 类的 __init__ 方法后添加
def _add_assistant_message_with_reasoning(self, content: str, reasoning_content: str = None, tool_calls: List = None):
"""添加助手消息,包含 reasoning_content"""
msg = {"role": "assistant"}
if content:
msg["content"] = content
if reasoning_content:
msg["reasoning_content"] = reasoning_content
if tool_calls:
msg["tool_calls"] = tool_calls
msg["content"] = None
self.messages.append(msg)
def _register_tools(self, exit_callback: Callable = None, command_executor: CommandExecutor = None):
# 内置工具列表
builtin_tools = [
GetCurrentTimeTool(self.logger),
CalculatorTool(self.logger),
CommandLineTool(command_executor or SubprocessCommandExecutor(self.logger), self.logger),
ReadFileTool(self.logger),
WriteFileTool(self.logger),
ListFilesTool(self.logger),
SimpleWebCrawlerTool(self.logger),
]
if self.memory_db:
builtin_tools.append(MemoryTool(self.memory_db, self.logger))
if self.memo_db:
builtin_tools.append(MemoTool(self.memo_db, self.logger))
# 更新身份工具
builtin_tools.append(UpdateIdentityTool(self.prompt_manager, self.logger))
# 新增:更新智能体自我认识工具
builtin_tools.append(UpdateSoulTool(self.prompt_manager, self.logger))
# 增强版保存记忆并结束对话工具
if exit_callback and self.memory_db:
builtin_tools.append(SaveMemoryAndEndConversationTool(exit_callback, self.memory_db, self.prompt_manager, self.logger))
# 添加询问用户的工具
builtin_tools.append(AskUserTool(self.logger))
# 添加技能热重载工具 & 技能安装指引工具 & 创建技能工具
builtin_tools.append(ReloadSkillsTool(self.tool_registry, self.logger))
builtin_tools.append(InstallSkillTool(self.logger))
builtin_tools.append(CreateSkillTool(self.logger))
builtin_tools.append(SetProxyTool(self.logger))
builtin_tools.append(GetProxyTool(self.logger))
# 注册内置工具(标记为内置)
self.tool_registry.register_many(builtin_tools, is_builtin=True)
# 创建技能管理器:启动时对每个技能执行 --help,收集第一行简介
skill_manager = SkillManager(self.logger)
self.tool_registry.skill_manager = skill_manager
# 注册扩展技能搜索工具(单一工具,替代逐个注册 ExtensionTool)
search_tool = SearchSkillsTool(skill_manager, self.logger)
self.tool_registry.register(search_tool, is_builtin=True)
builtin_count, skill_count = self.tool_registry.get_skills_count()
self.logger.info(f"已注册工具: 内置 {builtin_count} 个, 扩展技能 {skill_manager.count()} 个(通过 search_skills 工具访问)")
def _load_history_context(self):
"""加载历史对话作为上下文"""
if self.history_loaded or not self.history_manager:
return
context = self.history_manager.get_conversation_context()
if context:
history_prompt = f"""以下是之前的对话历史,请基于这些历史继续对话。
如果问题涉及到之前的对话内容,请参考这些历史记录。
当前时间: {datetime.now().strftime(constants.DATETIME_FORMAT)}"""
self._add_message("system", history_prompt)
for msg in context:
self.messages.append(msg)
self.history_loaded = True
self.logger.info(f"已加载 {len(context)} 条历史消息")
def _add_message(self, role: str, content: str):
"""
添加消息到历史
- 用户消息:添加时间戳前缀
- 助手消息:不添加时间戳前缀
"""
timestamp = datetime.now().strftime(constants.DATETIME_FORMAT)
if role == "assistant":
# 助手消息:不添加时间戳前缀,直接保存原始内容
formatted_content = content
elif role == "user":
# 用户消息:添加时间戳前缀
formatted_content = f"[用户 @ {timestamp}] {content}"
else:
# 系统消息:保持原样
formatted_content = content
self.messages.append({"role": role, "content": formatted_content})
self._cleanup_messages()
def _cleanup_messages(self):
"""清理消息历史,但保留系统提示词"""
if len(self.messages) <= self.max_history * 2:
return
if self.preserve_system:
system_messages = [msg for msg in self.messages if msg["role"] == "system"]
other_messages = [msg for msg in self.messages if msg["role"] != "system"]
other_messages = other_messages[-self.max_history * 2:]
self.messages = system_messages + other_messages
else:
self.messages = self.messages[-self.max_history * 2:]
def _save_tool_result_to_file(self, tool_name: str, result: str) -> str:
"""
将工具返回结果保存到文件,返回文件路径
"""
# 创建工具结果目录
tool_results_dir = os.path.join(constants.SAVE_DIR, "tool_results")
os.makedirs(tool_results_dir, exist_ok=True)
# 生成文件名:日期时间_工具名_哈希前8位.json
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 使用简单的哈希来避免文件名冲突
import hashlib
hash_suffix = hashlib.md5(result[:100].encode()).hexdigest()[:8]
safe_tool_name = re.sub(r'[^\w\-]', '_', tool_name)
filename = f"{timestamp}_{safe_tool_name}_{hash_suffix}.txt"
filepath = os.path.join(tool_results_dir, filename)
try:
# 保存结果到文件
with open(filepath, 'w', encoding='utf-8') as f:
f.write(result)
# 记录日志
result_size = len(result)
size_kb = result_size / 1024
if size_kb > 1024:
size_str = f"{size_kb/1024:.1f}MB"
else:
size_str = f"{size_kb:.1f}KB"
self.logger.info(f"工具 {tool_name} 返回结果过大 ({size_str}),已保存到: {filepath}")
return filepath
except Exception as e:
self.logger.error(f"保存工具结果失败: {e}")
return None
def _handle_tool_calls(self, tool_calls: List[Dict], output_callback: Callable = None) -> List[Dict]:
"""处理工具调用 - 所有工具的输出都进行发送,并在开始和结束时发送两个回车"""
if not tool_calls:
return []
self.stats["tool_calls"] += len(tool_calls)
tool_messages = []
# 创建输出队列用于流式输出
output_queue = queue.Queue()
self.tool_registry.set_output_queue(output_queue)
# 结果大小阈值(字符数),从配置获取
MAX_RESULT_LENGTH = constants.TOOL_MAX_RESULT_LENGTH
for tool_call in tool_calls:
function = tool_call.get("function", {})
tool_name = function.get("name")
arguments = function.get("arguments", "{}")
try:
args = json.loads(arguments) if arguments else {}
except json.JSONDecodeError:
args = {}
# 【修改】开始调用工具时发送两个回车
if output_callback:
output_callback("line", "\n\n")
output_callback("line", f"🔧 调用工具: {tool_name}\n")
# 直接调用 execute_tool(同步)。
# - 若工具支持流式:execute_tool 内部开线程调 execute_streaming(**args),
# 收集输出后转发到 current_output_queue(即此处的 output_queue),join 后返回。
# - 若工具不支持流式:execute_tool 直接调 execute(**args) 返回结果。
# 两种情况下 execute_tool 返回时执行均已完成,args 完整传入,不再有参数丢失。
result = self.tool_registry.execute_tool(tool_name, **args)
# 排空 output_queue,把流式行输出转发给 output_callback
has_stream_output = False
while not output_queue.empty():
try:
msg_type, content = output_queue.get_nowait()
if output_callback and msg_type in ("line", "complete"):
output_callback("line", content)
has_stream_output = True
except queue.Empty:
break
# 普通(非流式)工具没有队列输出,直接发送结果
if not has_stream_output and output_callback and result:
output_callback("line", result + "\n")
# 检查结果大小,如果超过阈值,不返回结果,只返回提示信息
if result and len(result) > MAX_RESULT_LENGTH:
# 不保存文件,直接返回提示信息
result = f"⚠️ 工具 {tool_name} 执行结果过大({len(result)} 字符),超过限制({MAX_RESULT_LENGTH} 字符),已放弃返回结果。\n\n请考虑:\n1. 使用更精确的查询条件\n2. 使用其他工具获取部分数据\n3. 优化查询方式减少结果数量"
if output_callback:
output_callback("line", result + "\n")
# 【修改】结束调用工具时发送两个回车
if output_callback:
output_callback("line", "\n\n")
if self.history_manager:
self.history_manager.add_tool_call(tool_name, args, result[:200] + "..." if len(result) > 200 else result)
if tool_name == "save_memory_and_end_conversation" and "✅" in result:
self.logger.info("检测到保存记忆并结束对话工具调用")
self.conversation_active = False
tool_messages.append({
"role": "tool",
"tool_call_id": tool_call.get("id"),
"content": result
})
# 【修复】不直接返回,而是添加后继续处理,但设置标志让后续停止
break
tool_messages.append({
"role": "tool",
"tool_call_id": tool_call.get("id"),
"content": result
})
# 清除输出队列
self.tool_registry.set_output_queue(None)
return tool_messages
def _should_search_memory(self, user_input: str) -> bool:
"""判断是否应该搜索记忆"""
# 检查用户输入是否包含搜索意图的关键词
search_triggers = [
'还记得', '之前', '上次', '以前', '我们说过', '我们讨论过',
'你记得', '你还记得', '我们聊过', '我们谈到', '我们之前',
'根据之前的', '依据历史', 'refer', 'previous', 'before',
'last time', 'earlier', '曾经', '过往'
]
user_input_lower = user_input.lower()
for trigger in search_triggers:
if trigger.lower() in user_input_lower:
return True
# 检查是否是简单问候(可能不需要搜索)
greetings = ['你好', '您好', '嗨', 'hello', 'hi', 'hey']
if user_input_lower.strip() in greetings:
return False
# 默认情况下,如果记忆库存在,搜索一下也无妨
return True
def think_and_respond(self, input_text: str, output_callback: Callable = None) -> Optional[str]:
"""思考并回应 - 支持实时流式输出,支持思考模式"""
if not self.conversation_active:
self.logger.info("对话已结束,重置状态")
self.conversation_active = True
if self.preserve_system:
system_messages = [msg for msg in self.messages if msg["role"] == "system"]
self.messages = system_messages
else:
self.messages = []
self.logger.info("对话状态已重置")
# 压缩检查
if constants.COMPRESS_ENABLED:
should_compress, reason = self.compressor.should_compress(self.messages)
if should_compress:
self.logger.debug(f"压缩检查触发: {reason}")
original_count = len(self.messages)
original_tokens = self.compressor.estimate_tokens(self.messages)
print(f"\n📦 {i18n.get('conversation_compress')}...")
print(f" 触发原因: {reason}")
self.messages = self.compressor.compress_conversation(self.messages)
compressed_count = len(self.messages)
compressed_tokens = self.compressor.estimate_tokens(self.messages)
self.logger.debug(i18n.get('conversation_compressed', original_count, compressed_count))
self.logger.debug(f"Token数: {original_tokens} -> {compressed_tokens}")
self.stats["api_calls"] += 1
# 前置提示词
pre_prompt = """【系统指令 - 核心工作流程】
在开始任何任务之前,你必须严格按照以下优先级执行:
## 第一步:搜索可用技能(最高优先级)⭐
当用户提出任何任务、需求或问题时,**必须先使用 `search_skills` 工具**搜索相关技能:
- 使用 `search_skills keyword="[任务关键词]"` 搜索是否有现成的扩展技能可以完成该任务
- 例如:用户说"帮我查天气",你应该先执行 `search_skills keyword="天气"`
- 例如:用户说"帮我计算BMI",你应该先执行 `search_skills keyword="BMI"`
- 如果搜索结果中有匹配的技能,优先使用该技能完成任务
- 只有在没有合适技能的情况下,才考虑使用其他工具或方法
## 第二步:搜索相关记忆
在每次回答用户问题前,**必须**使用 `memory search` 工具搜索记忆库中与当前话题相关的记忆,确保回答的连贯性和准确性。
## 第三步:积极向用户提问
如果你有任何无法确定的事情,那么你需要用户提供额外信息,
请务必使用 `ask_user` 工具向用户提问,以便获得更准确的指引。
## 第四步:执行任务或回答问题
根据搜索结果,选择合适的工具或直接回答。
【重要提醒】
- 技能优先原则:有技能先用技能,不要自己硬编码实现
- 如果没有合适的技能,可以使用其他工具或直接回答
- 对话结束时,必须使用 `save_memory_and_end_conversation` 工具保存本次对话的重要信息并更新智能体状态
- 当了解到用户的身份信息时,使用 `update_identity` 工具更新 00_IDENTITY.md
- 当你的角色、性格、能力边界需要调整时,使用 `update_soul` 工具更新 99_SOUL.md 文件来重新定义自己
- 绝对不要直接打开大文件来读,应该使用额外的压缩工具
现在请处理用户的输入:"""
# 后置提示词
post_prompt = """
【注意】
当本次对话的话题讨论完成时,请使用 `save_memory_and_end_conversation` 工具结束对话并保存重要信息到记忆库,同时更新你的状态到 100_STATUS.md。
【自我认知更新】
在对话过程中,如果你发现:
- 自己的角色定位需要调整
- 性格特点需要改变
- 能力边界需要重新定义
- 行为准则需要优化
请使用 `update_soul` 工具更新 99_SOUL.md 文件,这有助于你更好地理解自己并为用户提供更精准的服务。"""
enhanced_input = pre_prompt + "\n\n" + input_text + post_prompt
self._add_message("user", enhanced_input)
if self.history_manager:
self.history_manager.add_message("user", enhanced_input, "用户")
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
# 构建消息列表,确保包含 reasoning_content
enhanced_messages = []
# 添加所有系统消息
for msg in self.messages:
if msg["role"] == "system":
enhanced_messages.append(msg)
# 添加非系统消息,但确保消息结构正确
for msg in self.messages:
if msg["role"] != "system":
if msg["role"] == "assistant":
assistant_msg = {"role": "assistant"}
if "content" in msg and msg["content"]:
assistant_msg["content"] = msg["content"]
if "reasoning_content" in msg:
assistant_msg["reasoning_content"] = msg["reasoning_content"]
if "tool_calls" in msg and msg["tool_calls"]:
assistant_msg["tool_calls"] = msg["tool_calls"]
assistant_msg["content"] = None
enhanced_messages.append(assistant_msg)
else:
enhanced_messages.append(msg)
payload = {
"model": constants.DEEPSEEK_MODEL,
"messages": enhanced_messages,
"temperature": constants.DEFAULT_TEMPERATURE,
"max_tokens": constants.DEFAULT_MAX_TOKENS,
"stream": True
}
# 可选:添加 thinking 模式配置(如果有)
payload["thinking"] = {
"type": "enabled"
}
tools = self.tool_registry.get_tools_schemas()
if tools:
payload["tools"] = tools
payload["tool_choice"] = "auto"
current_time = datetime.now().strftime("%H:%M:%S")
if output_callback:
output_callback("line", f"\n{i18n.get('thinking', current_time)}\n")
output_callback("line", i18n.get('assistant_prefix'))
full_response = ""
full_reasoning = "" # 存储思考内容
tool_calls_buffer = []
current_tool_calls = {}
chunk_buffer = ""
last_send_time = time.time()
MIN_CHUNK_SIZE = 5
MAX_CHUNK_DELAY = 0.1
try:
proxies = self.config.get_proxy()
response = requests.post(
constants.DEEPSEEK_API_URL,
headers=headers,
json=payload,
timeout=constants.DEFAULT_TIMEOUT,
stream=True,
proxies=proxies # 动态代理
)
if response.status_code != 200:
error_msg = f"\n错误状态码: {response.status_code}\n错误响应: {response.text}"
if output_callback:
output_callback("error", error_msg)
response.raise_for_status()
for line in response.iter_lines():
if line:
line = line.decode('utf-8')
if line.startswith('data: '):
data = line[6:]
if data == '[DONE]':
if chunk_buffer and output_callback:
output_callback("chunk", chunk_buffer)
chunk_buffer = ""
if output_callback:
output_callback("line", "\n")
break
try:
chunk = json.loads(data)
choices = chunk.get('choices', [])
if not choices:
continue
delta = choices[0].get('delta', {})
# 【关键】处理 reasoning_content(思考内容)
if 'reasoning_content' in delta and delta['reasoning_content']:
reasoning = delta['reasoning_content']
full_reasoning += reasoning
# 可选:将思考内容也发送给前端(可以单独处理)
if output_callback and self.debug_level >= 3:
output_callback("reasoning", reasoning)
# 处理正常内容
if 'content' in delta and delta['content']:
content = delta['content']
chunk_buffer += content
full_response += content
if (len(chunk_buffer) >= MIN_CHUNK_SIZE or
time.time() - last_send_time >= MAX_CHUNK_DELAY):
if output_callback:
output_callback("chunk", chunk_buffer)
chunk_buffer = ""
last_send_time = time.time()
# 处理工具调用
if 'tool_calls' in delta:
tool_calls = delta['tool_calls']
for tc in tool_calls:
index = tc.get('index', 0)
if index not in current_tool_calls:
current_tool_calls[index] = {
'id': tc.get('id', ''),
'type': 'function',
'function': {
'name': '',
'arguments': ''
}
}
if 'function' in tc:
if 'name' in tc['function']:
current_tool_calls[index]['function']['name'] = tc['function']['name']
if 'arguments' in tc['function']:
current_tool_calls[index]['function']['arguments'] += tc['function']['arguments']
except json.JSONDecodeError as e:
self.logger.error(f"JSON解析错误: {e}, 数据: {data}")
continue
# 处理工具调用
if current_tool_calls:
tool_calls_buffer = list(current_tool_calls.values())
if output_callback:
output_callback("line", i18n.get('calling_tools') + "\n")
# 【关键修复1】先添加助手消息,包含 tool_calls
assistant_msg = {
"role": "assistant",
"content": full_response if full_response else None
}
if full_reasoning:
assistant_msg["reasoning_content"] = full_reasoning
if tool_calls_buffer:
assistant_msg["tool_calls"] = tool_calls_buffer
assistant_msg["content"] = None
# 添加助手消息到消息列表
self.messages.append(assistant_msg)
if self.history_manager:
self.history_manager.add_message("assistant", full_response, self.name)
# 【关键修复2】执行工具调用并获取结果
tool_responses = self._handle_tool_calls(tool_calls_buffer, output_callback)
# 【关键修复3】添加所有 tool 响应消息
if tool_responses:
for tool_response in tool_responses:
self.messages.append(tool_response)
# 检查对话是否应该结束
if not self.conversation_active:
self.logger.info("对话已结束,停止响应")
if output_callback:
output_callback("dialogue_ended", i18n.get('dialogue_ended'))
return None
if output_callback:
output_callback("line", i18n.get('analyzing_tool_results') + "\n")
# 【关键修复4】递归调用,让AI基于工具结果继续回答
# 使用一个新的输入提示,避免重复
return self.think_and_respond("请基于以上工具执行结果继续回答用户的问题。", output_callback)
elif full_response:
# 正常添加助手消息,包含 reasoning_content
assistant_msg = {
"role": "assistant",
"content": full_response
}
if full_reasoning:
assistant_msg["reasoning_content"] = full_reasoning
self.messages.append(assistant_msg)
if self.history_manager:
self.history_manager.add_message("assistant", full_response, self.name)
if output_callback:
output_callback("complete", full_response)
return full_response if full_response else ""
except requests.exceptions.RequestException as e:
error_msg = f"\n{i18n.get('error_prefix')}API请求失败: {str(e)}"
if output_callback:
output_callback("error", error_msg)
self.stats["errors"].append(str(e))
return error_msg
except json.JSONDecodeError as e:
error_msg = f"\n{i18n.get('error_prefix')}API响应解析失败: {str(e)}"
if output_callback:
output_callback("error", error_msg)
self.stats["errors"].append(str(e))
return error_msg
except Exception as e:
error_msg = f"\n{i18n.get('error_prefix')}{str(e)}"
if output_callback:
output_callback("error", error_msg)
self.stats["errors"].append(str(e))
return error_msg
def _add_assistant_message_with_reasoning(self, content: str, reasoning_content: str = None, tool_calls: List = None):
"""添加助手消息,包含 reasoning_content"""
msg = {"role": "assistant"}
if content:
msg["content"] = content
if reasoning_content:
msg["reasoning_content"] = reasoning_content
if tool_calls:
msg["tool_calls"] = tool_calls
msg["content"] = None
self.messages.append(msg)
def reset_conversation(self, reload_prompts: bool = True):
"""
重置对话状态 - 清除所有对话记忆,可选择是否重新加载系统提示词
Args:
reload_prompts: 是否重新加载系统提示词,默认为 True
"""
self.conversation_active = True
self.history_loaded = False # 重置历史加载标记
if reload_prompts:
# 完全重置:清除所有消息,重新加载系统提示词
self.messages = []
self._init_system_prompts()
self.logger.info("对话已完全重置,系统提示词已重新加载")
else:
# 只清除非系统消息,保留系统提示词
if self.preserve_system:
system_messages = [msg for msg in self.messages if msg["role"] == "system"]
self.messages = system_messages
else:
self.messages = []
self.logger.info("对话已重置(仅清除对话历史)")
def reload_prompts(self):
"""重新加载提示词"""
old_system_count = len([msg for msg in self.messages if msg["role"] == "system"])
# 移除所有旧的系统消息
self.messages = [msg for msg in self.messages if msg["role"] != "system"]
# 重新加载提示词
self.prompt_manager.reload()
self._init_system_prompts()
new_system_count = len([msg for msg in self.messages if msg["role"] == "system"])
self.logger.info(f"提示词已重新加载: {old_system_count} -> {new_system_count} 个")
return new_system_count
# ==================== WebSocket处理器(修改版:添加重置对话功能) ====================
class WebSocketHandler:
"""WebSocket处理器 - 处理WebSocket连接和消息"""
def __init__(self, ai_manager):
self.ai_manager = ai_manager
self.logger = Logger("WebSocket")
self.connected_clients = set()
self.client_sessions = {}
async def register(self, websocket):
"""注册新客户端"""
self.connected_clients.add(websocket)
self.client_sessions[websocket] = {
"connected_at": datetime.now(),
"message_count": 0,
"current_conversation": None
}
self.logger.info(i18n.get('client_connected', websocket.remote_address))
# 发送连接确认消息
await self.send_message(websocket, {
"type": "connection_ack",
"message": i18n.get('ws_connected'),
"timestamp": datetime.now().strftime(constants.DATETIME_FORMAT)
})
# 发送欢迎信息和初始状态
await self.send_welcome_info(websocket)
async def unregister(self, websocket):
"""注销客户端"""
self.connected_clients.remove(websocket)
if websocket in self.client_sessions:
del self.client_sessions[websocket]
self.logger.info(i18n.get('client_disconnected', websocket.remote_address))
async def send_welcome_info(self, websocket):
"""发送欢迎信息"""
# 发送存储位置
await self.send_message(websocket, {
"type": "info",
"content": i18n.get('data_dir', constants.SAVE_DIR)
})
await self.send_message(websocket, {
"type": "info",
"content": i18n.get('skills_dir', constants.SKILLS_DIR)
})
await self.send_message(websocket, {
"type": "info",
"content": i18n.get('prompts_dir', constants.PROMPTS_DIR)
})
# 发送任务调度器状态
if self.ai_manager.scheduler and self.ai_manager.scheduler.running:
stats = self.ai_manager.scheduler.get_stats()
await self.send_message(websocket, {
"type": "info",
"content": i18n.get('scheduler_started')
})
await self.send_message(websocket, {
"type": "info",
"content": f"📊 任务统计: 总执行次数={stats.get('total_executions', 0)}, 最近执行={len(stats.get('recent_executions', []))}"
})
# 发送任务统计
memo_stats = self.ai_manager.memo_db.get_memo_stats()
if memo_stats['total'] > 0:
await self.send_message(websocket, {
"type": "info",
"content": i18n.get('memo_stats')
})
await self.send_message(websocket, {
"type": "info",
"content": i18n.get('total_memos', memo_stats['total'])
})
await self.send_message(websocket, {
"type": "info",
"content": i18n.get('pending_memos', memo_stats['pending'])
})
if memo_stats['overdue'] > 0:
await self.send_message(websocket, {
"type": "info",
"content": i18n.get('overdue_memos', memo_stats['overdue'])
})
await self.send_message(websocket, {
"type": "info",
"content": i18n.get('completed_memos', memo_stats['completed'])
})
# 发送待执行任务列表
pending = self.ai_manager.memo_db.get_pending_tasks_for_user()
if pending:
await self.send_message(websocket, {
"type": "info",
"content": i18n.get('pending_memos_title')
})
for task in pending[:5]:
status = i18n.get('memo_overdue') if task['is_overdue'] else i18n.get('memo_pending')
immediate_tag = "⚡" if task['is_immediate'] else ""
await self.send_message(websocket, {
"type": "task_status",
"task_id": task['id'],
"title": task['title'],
"reminder_time": task['reminder_time'],
"status": "overdue" if task['is_overdue'] else "pending",
"content": f"{status}{immediate_tag} [ID: {task['id']}] {task['title']} - {task['reminder_time']}"
})
if len(pending) > 5:
await self.send_message(websocket, {
"type": "info",
"content": i18n.get('more_skills', len(pending)-5)
})
async def send_message(self, websocket, message):
"""发送消息到客户端 - 确保完整发送不被截断"""
try:
# 将消息转换为JSON字符串
json_str = json.dumps(message, ensure_ascii=False)
# 直接发送整个字符串,WebSocket协议会自动处理分片
await websocket.send(json_str)
except websockets.exceptions.ConnectionClosed:
self.logger.warning("连接已关闭,无法发送消息")
except Exception as e:
self.logger.error(f"发送消息失败: {e}")
async def broadcast(self, message):
"""广播消息到所有客户端"""
for websocket in self.connected_clients.copy():
try:
await self.send_message(websocket, message)
except:
await self.unregister(websocket)
async def broadcast_task_status(self, task: Dict, status: str, message: str = None):
"""广播任务状态"""
await self.broadcast({
"type": "task_status",
"task_id": task.get('id', 0),
"title": task.get('title', '未知任务'),
"status": status,
"message": message,
"timestamp": datetime.now().strftime(constants.DATETIME_FORMAT)
})
async def handle_message(self, websocket, message):
"""处理接收到的消息"""
try:
data = json.loads(message)
msg_type = data.get("type", "unknown")
# 更新会话统计
if websocket in self.client_sessions:
self.client_sessions[websocket]["message_count"] += 1
if msg_type == "command":
await self.handle_command(websocket, data)
elif msg_type == "message":
await self.handle_user_message(websocket, data)
elif msg_type == "task":
await self.handle_task_message(websocket, data)
elif msg_type == "ping":
await self.send_message(websocket, {
"type": "pong",
"timestamp": datetime.now().strftime(constants.DATETIME_FORMAT)
})
else:
await self.send_message(websocket, {
"type": "error",
"content": i18n.get('ws_unknown_command', msg_type)
})
except json.JSONDecodeError:
await self.send_message(websocket, {
"type": "error",
"content": "无效的JSON格式"
})
except Exception as e:
self.logger.error(f"处理消息失败: {e}")
await self.send_message(websocket, {
"type": "error",
"content": f"处理失败: {str(e)}"
})
async def handle_task_message(self, websocket, data):
"""处理任务相关消息 - 简化版,所有任务都是对话任务"""
action = data.get("action", "").strip()
if action == "add":
# 添加任务
title = data.get("title")
content = data.get("content")
delay = data.get("delay", 60) # 默认60秒后执行
is_immediate = data.get("is_immediate", False) # 是否为即时任务
# 重复任务参数
repeat_type = data.get("repeat_type", "none")
repeat_interval = data.get("repeat_interval", 0)
repeat_interval_unit = data.get("repeat_interval_unit", "days")
repeat_end_time = data.get("repeat_end_time")
if not title:
await self.send_message(websocket, {
"type": "error",
"content": "需要提供任务标题"
})
return
reminder_time = None
if is_immediate:
reminder_time = datetime.now().strftime(constants.DATETIME_FORMAT)
else:
reminder_time = (datetime.now() + timedelta(seconds=delay)).strftime(constants.DATETIME_FORMAT)
memo_id = self.ai_manager.memo_db.add_user_task(
title=title,
content=content,
reminder_time=reminder_time,
repeat_type=repeat_type,
repeat_interval=repeat_interval,
repeat_interval_unit=repeat_interval_unit,
repeat_end_time=repeat_end_time,
is_immediate=is_immediate
)
immediate_msg = i18n.get('immediate_memo_added') if is_immediate else ""
repeat_msg = ""
if repeat_type != "none":
if repeat_type == "custom":
repeat_msg = f" (每{repeat_interval} {repeat_interval_unit})"
else:
repeat_msg = f" ({repeat_type})"
await self.send_message(websocket, {
"type": "task_added",
"task_id": memo_id,
"title": title,
"reminder_time": reminder_time,
"content": i18n.get('task_added', title, memo_id) + repeat_msg + immediate_msg
})
# 广播任务添加
await self.broadcast_task_status(
{"id": memo_id, "title": title},
"added",
f"新任务: {title}" + repeat_msg + (" (即时)" if is_immediate else "")
)
elif action == "list":
pending = self.ai_manager.memo_db.get_pending_tasks_for_user()
await self.send_message(websocket, {
"type": "task_list",
"tasks": pending
})
elif action == "complete":
memo_id = data.get("task_id")
if memo_id:
if self.ai_manager.memo_db.complete_memo(memo_id):
memo = self.ai_manager.memo_db.get_memo(memo_id)
await self.send_message(websocket, {
"type": "task_completed",
"task_id": memo_id,
"content": i18n.get('memo_completed', memo_id)
})
if memo:
await self.broadcast_task_status(memo, "completed", f"任务已完成: {memo['title']}")
else:
await self.send_message(websocket, {
"type": "error",
"content": i18n.get('memo_complete_failed', memo_id)
})
async def handle_command(self, websocket, data):
"""处理命令消息"""
command = data.get("command", "").strip()
if not command:
return
self.logger.info(f"收到命令: {command}")
if command == "/exit":
await self.send_message(websocket, {
"type": "command_result",
"content": i18n.get('goodbye')
})
await websocket.close()
elif command == "/new":
# 【新增】开始新对话:保存当前对话,重置AI状态,重新加载系统提示词
self.ai_manager.history_manager.archive_current_conversation()
if self.ai_manager.ai:
# 重置对话状态,重新加载系统提示词
self.ai_manager.ai.reset_conversation(reload_prompts=True)
# 重置对话进行中标记
self.ai_manager.dialogue_in_progress = False
# 重新初始化提示词管理器(确保最新)
self.ai_manager.prompt_manager.reload()
self.logger.info(i18n.get('new_conversation_reset'))
await self.send_message(websocket, {
"type": "command_result",
"content": i18n.get('new_conversation_reset')
})
# 发送重置完成通知
await self.send_message(websocket, {
"type": "dialogue_reset",
"content": i18n.get('dialogue_reset_complete')
})
else:
await self.send_message(websocket, {
"type": "command_result",
"content": i18n.get('new_conversation')
})
elif command == "/list":
conversations = self.ai_manager.history_manager.list_conversations()
result = f"\n{i18n.get('saved_conversations')}\n"
for conv in conversations:
status = i18n.get('conv_status_current') if conv.get('is_current') else i18n.get('conv_status_archived')
result += f" {conv['file']} - {conv['topic']} [{status}] ({conv['start_time']})\n"
await self.send_message(websocket, {
"type": "command_result",
"content": result
})
elif command == "/memories":
memories = self.ai_manager.memory_db.get_recent_memories(10)
result = f"\n{i18n.get('recent_memories')}\n"
for mem in memories:
result += f" [ID: {mem['id']}] {mem['topic']} - {mem['summary'][:50]}...\n"
await self.send_message(websocket, {
"type": "command_result",
"content": result
})
elif command == "/memos":
memos = self.ai_manager.memo_db.get_all_memos(include_completed=False)
result = f"\n{i18n.get('pending_memos_title')}\n"
for memo in memos:
status = i18n.get('memo_overdue') if memo['reminder_time'] and datetime.strptime(memo['reminder_time'], constants.DATETIME_FORMAT) < datetime.now() else i18n.get('memo_pending')
immediate_tag = "⚡" if memo.get('is_immediate') else ""
repeat_info = ""
if memo['repeat_type'] != 'none':
if memo['repeat_type'] == 'custom':
unit = memo.get('repeat_interval_unit', 'days')
repeat_info = f" [每{memo['repeat_interval']} {unit}]"
else:
repeat_info = f" [{memo['repeat_type']}]"
result += f" {status}{immediate_tag} [ID: {memo['id']}] {memo['title']}{repeat_info} - {memo.get('reminder_time', '无提醒')}\n"
await self.send_message(websocket, {
"type": "command_result",
"content": result
})
elif command.startswith("/search "):
query = command[8:].strip()
results = self.ai_manager.memory_db.search_memories([query])
result = i18n.get('search_results', query, len(results)) + "\n"
for mem in results:
result += f" [ID: {mem['id']}] {mem['topic']} - {mem['summary'][:100]}...\n"
await self.send_message(websocket, {
"type": "command_result",
"content": result
})
elif command == "/config":
result = f"\n{i18n.get('current_config')}\n"
result += i18n.get('config_debug', self.ai_manager.config.get('system.debug_level')) + "\n"
result += i18n.get('config_data_dir', constants.SAVE_DIR) + "\n"
result += i18n.get('config_skills_dir', constants.SKILLS_DIR) + "\n"
result += i18n.get('config_prompts_dir', constants.PROMPTS_DIR) + "\n"
result += i18n.get('config_commands', i18n.get('enabled') if constants.COMMANDS_ENABLED else i18n.get('disabled')) + "\n"
result += i18n.get('config_history', i18n.get('enabled') if self.ai_manager.history_manager.load_history else i18n.get('disabled')) + "\n"
result += i18n.get('config_executor', type(self.ai_manager.command_executor).__name__) + "\n"
result += i18n.get('config_scheduler', i18n.get('enabled') if constants.SCHEDULER_ENABLED else i18n.get('disabled')) + "\n"
await self.send_message(websocket, {
"type": "command_result",
"content": result
})
elif command == "/reload":
self.ai_manager.config.reload()
await self.send_message(websocket, {
"type": "command_result",
"content": i18n.get('config_reloaded')
})
elif command == "/reload_prompts":
if self.ai_manager.ai:
new_count = self.ai_manager.ai.reload_prompts()
else:
new_count = self.ai_manager.prompt_manager.reload()
await self.send_message(websocket, {
"type": "command_result",
"content": i18n.get('prompts_reloaded', new_count)
})
elif command == "/check":
result = i18n.get('checking_memos') + "\n"
overdue = self.ai_manager.memo_db.get_overdue_memos()
if overdue:
result += i18n.get('overdue_found', len(overdue)) + "\n"
for memo in overdue:
immediate_tag = "⚡" if memo.get('is_immediate') else ""
repeat_info = ""
if memo['repeat_type'] != 'none':
if memo['repeat_type'] == 'custom':
unit = memo.get('repeat_interval_unit', 'days')
repeat_info = f" (每{memo['repeat_interval']} {unit})"
else:
repeat_info = f" ({memo['repeat_type']})"
result += f" • {immediate_tag}[ID: {memo['id']}] {memo['title']}{repeat_info} - {memo['reminder_time']}\n"
else:
result += i18n.get('no_overdue') + "\n"
await self.send_message(websocket, {
"type": "command_result",
"content": result
})
elif command == "/tasks":
if self.ai_manager.scheduler:
stats = self.ai_manager.scheduler.get_stats()
result = f"\n{i18n.get('task_stats', stats.get('total_executions', 0), stats.get('total_executions', 0), stats.get('total_executions', 0))}\n\n"
pending = self.ai_manager.memo_db.get_pending_tasks_for_user()
result += f"待执行任务: {len(pending)} 个\n"
for task in pending[:10]:
status = "⏰" if not task['is_overdue'] else "⚠️"
immediate_tag = "⚡" if task['is_immediate'] else ""
repeat_info = ""
if task['repeat_type'] != 'none':
if task['repeat_type'] == 'custom':
unit = task.get('repeat_interval_unit', 'days')
repeat_info = f" [每{task['repeat_interval']} {unit}]"
else:
repeat_info = f" [{task['repeat_type']}]"
result += f" {status}{immediate_tag} [ID: {task['id']}] {task['title']}{repeat_info} - {task['reminder_time']}\n"
else:
result = "❌ 任务调度器未启用"
await self.send_message(websocket, {
"type": "command_result",
"content": result
})
elif command == "/help":
help_text = f"\n可用命令:\n"
help_text += " /exit - 退出连接\n"
help_text += " /new - 开始新对话(保存当前对话,重置AI记忆,重新加载系统提示词)\n"
help_text += " /list - 列出所有对话\n"
help_text += " /memories - 查看最近的记忆\n"
help_text += " /memos - 查看待办任务\n"
help_text += " /search <关键词> - 搜索记忆\n"
help_text += " /config - 查看配置\n"
help_text += " /reload - 重新加载配置\n"
help_text += " /reload_prompts - 重新加载提示词\n"
help_text += " /check - 检查过期任务\n"
help_text += " /tasks - 查看任务状态\n"
help_text += " /help - 显示此帮助\n\n"
help_text += "任务类型:\n"
help_text += " {type: 'task', action: 'add', title: '任务', delay: 60} - 添加一次性任务\n"
help_text += " {type: 'task', action: 'add', title: '即时', is_immediate: true} - 添加即时任务(立即执行)\n"
help_text += " {type: 'task', action: 'add', title: '重复任务', repeat_type: 'custom', repeat_interval: 5, repeat_interval_unit: 'minutes'} - 每5分钟重复\n"
help_text += " {type: 'task', action: 'list'} - 获取任务列表\n"
help_text += " {type: 'task', action: 'complete', task_id: 1} - 完成任务\n"
await self.send_message(websocket, {
"type": "command_result",
"content": help_text
})
else:
# 如果不是命令,当作普通消息处理
await self.handle_user_message(websocket, {"type": "message", "content": command})
async def handle_user_message(self, websocket, data):
"""处理用户消息 - 实时流式输出"""
content = data.get("content", "").strip()
if not content:
return
self.logger.info(f"收到用户消息: {content[:50]}...")
# 确保AI已初始化
if not self.ai_manager.ai:
self.logger.info("初始化AI...")
self.ai_manager.add_ai()
# 如果还没有活跃对话,开始新对话
if not self.ai_manager.dialogue_in_progress:
self.logger.info("开始新对话")
self.ai_manager.dialogue_in_progress = True
self.ai_manager.history_manager.start_new_conversation(content[:50] + "...")
# 发送对话开始消息
await self.send_message(websocket, {
"type": "info",
"content": i18n.get('conversation_start')
})
# 获取当前事件循环
loop = asyncio.get_running_loop()
# 定义输出回调函数 - 实时发送
def output_callback(msg_type, content):
"""AI输出的回调函数 - 实时发送每个chunk"""
self.logger.info(f"输出回调被调用: {msg_type}, 内容长度: {len(content)}")
# 创建发送消息的协程
async def send_msg():
try:
await websocket.send(json.dumps({
"type": msg_type,
"content": content
}, ensure_ascii=False))
self.logger.info(f"实时消息发送成功: {msg_type}")
except Exception as e:
self.logger.error(f"实时发送消息失败: {e}")
# 提交到事件循环
asyncio.run_coroutine_threadsafe(send_msg(), loop)
# 让AI思考并回应 - 不等待完整响应,通过回调实时发送
try:
self.logger.info("开始AI思考...")
# 在线程池中运行AI思考,通过回调实时发送消息
response = await loop.run_in_executor(
None,
self.ai_manager.ai.think_and_respond,
content,
output_callback # 传递回调函数,AI会在生成每个chunk时调用
)
self.logger.info(f"AI思考完成,完整响应长度: {len(response) if response else 0}")
# 检查对话是否结束
if not self.ai_manager.ai.conversation_active:
self.logger.info("对话结束")
self.ai_manager.dialogue_in_progress = False
self.ai_manager.history_manager.end_current_conversation()
await self.send_message(websocket, {
"type": "info",
"content": i18n.get('conversation_ended')
})
# 【修复】发送明确的对话结束信号
await self.send_message(websocket, {
"type": "dialogue_ended",
"content": i18n.get('dialogue_ended')
})
except Exception as e:
self.logger.error(f"AI处理消息失败: {e}")
import traceback
traceback.print_exc()
try:
await websocket.send(json.dumps({
"type": "error",
"content": f"{i18n.get('error_prefix')}{str(e)}"
}, ensure_ascii=False))
except:
pass
# ==================== 对话管理器(修改版:添加重置对话功能) ====================
class EnhancedAIManager:
"""增强的AI对话管理器 - 集成任务调度器"""
def __init__(self, debug_level: int = None, load_history: bool = None, command_executor: CommandExecutor = None):
self.ai: Optional[DeepSeekChat] = None
self.dialogue_history: List[Dict] = []
self.config = ConfigManager()
self.command_executor = command_executor or SubprocessCommandExecutor()
self.prompt_manager = PromptManager() # 创建提示词管理器
self.scheduler: Optional[TaskScheduler] = None # 任务调度器
if debug_level is None:
debug_level = self.config.get("system.debug_level", 1)
self.logger = Logger("Manager", debug_level)
self.running = True
# 确保 ~/.aibox 目录存在
os.makedirs(constants.SAVE_DIR, exist_ok=True)
# 确保 prompts 目录存在(PromptManager 会创建)
os.makedirs(constants.PROMPTS_DIR, exist_ok=True)
self.memory_db = MemoryDatabase()
self.memo_db = MemoDatabase()
self.history_manager = ConversationHistory(memory_db=self.memory_db, load_history=load_history)
self.dialogue_in_progress = False
self.ws_handler = None
def global_exit_handler(self, reason: str, summary: str):
"""对话结束处理器"""
self.logger.info(f"对话结束 - 原因: {reason}")
self.history_manager.end_current_conversation(summary)
if self.ai:
# 智能体主动结束对话后,重置对话状态但不清除系统提示词
# 注意:这里只重置对话标记,不清除消息历史
self.ai.conversation_active = True
# 但为了下次对话不加载旧的历史,我们清除非系统消息
if self.ai.preserve_system:
system_messages = [msg for msg in self.ai.messages if msg["role"] == "system"]
self.ai.messages = system_messages
else:
self.ai.messages = []
self.ai.history_loaded = False # 重置历史加载标记
self.dialogue_in_progress = False
self.logger.info("对话状态已重置,可以开始新对话")
def reset_dialogue(self):
"""重置对话 - 清除所有对话记忆,重新加载系统提示词"""
if self.ai:
# 完全重置AI状态,重新加载系统提示词
self.ai.reset_conversation(reload_prompts=True)
self.dialogue_in_progress = False
self.history_manager.reset_conversation()
self.logger.info(i18n.get('dialogue_reset_complete'))
def add_ai(self) -> DeepSeekChat:
"""添加AI"""
ai = DeepSeekChat(
api_key=None,
max_history=constants.DEFAULT_MAX_HISTORY,
debug_level=self.logger.level,
exit_callback=self.global_exit_handler,
history_manager=self.history_manager,
memory_db=self.memory_db,
memo_db=self.memo_db,
command_executor=self.command_executor,
prompt_manager=self.prompt_manager
)
self.ai = ai
self.logger.info("AI已初始化")
return ai
def show_welcome_message(self):
"""显示欢迎信息"""
print("\n" + "="*70)
print(i18n.get('welcome_title'))
print("="*70)
# 显示存储位置
print(i18n.get('data_dir', constants.SAVE_DIR))
print(i18n.get('skills_dir', constants.SKILLS_DIR))
print(i18n.get('prompts_dir', constants.PROMPTS_DIR))
config_path = self.config._config_path
if os.path.exists(config_path):
print(i18n.get('config_file', config_path))
# 显示提示词数量
prompt_count = self.prompt_manager.get_prompt_count()
if prompt_count > 0:
prompts = self.prompt_manager.get_prompts_list()
print(i18n.get('loaded_prompts', prompt_count))
for prompt in prompts[:5]:
print(i18n.get('prompt_item', prompt['file']))
if prompt_count > 5:
print(i18n.get('more_skills', prompt_count-5))
# 显示任务调度器状态
if constants.SCHEDULER_ENABLED:
print(i18n.get('scheduler_started'))
memo_stats = self.memo_db.get_memo_stats()
if memo_stats['total'] > 0:
print(f"\n{i18n.get('memo_stats')}")
print(i18n.get('total_memos', memo_stats['total']))
print(i18n.get('pending_memos', memo_stats['pending']))
if memo_stats['overdue'] > 0:
print(i18n.get('overdue_memos', memo_stats['overdue']))
print(i18n.get('completed_memos', memo_stats['completed']))
# 显示待执行任务
pending = self.memo_db.get_pending_tasks_for_user()
if pending:
print(f"\n{i18n.get('pending_memos_title')}")
for task in pending[:5]:
status = i18n.get('memo_overdue') if task['is_overdue'] else i18n.get('memo_pending')
immediate_tag = "⚡" if task['is_immediate'] else ""
repeat_info = ""
if task['repeat_type'] != 'none':
if task['repeat_type'] == 'custom':
unit = task.get('repeat_interval_unit', 'days')
repeat_info = f" [每{task['repeat_interval']} {unit}]"
else:
repeat_info = f" [{task['repeat_type']}]"
print(f" {status}{immediate_tag} [ID: {task['id']}] {task['title']}{repeat_info} - {task['reminder_time']}")
if len(pending) > 5:
print(i18n.get('more_skills', len(pending)-5))
# 显示已加载的技能(含简介)
skill_manager = getattr(getattr(self.ai, 'tool_registry', None), 'skill_manager', None)
if skill_manager and skill_manager.count() > 0:
all_skills = skill_manager.get_all()
print(i18n.get('loaded_skills', skill_manager.count()))
for s in all_skills[:5]:
print(f" • {s['name']} — {s['summary']}")
if skill_manager.count() > 5:
print(i18n.get('more_skills', skill_manager.count() - 5))
# 显示WebSocket服务器信息
host = constants.WEBSOCKET_HOST
port = constants.WEBSOCKET_PORT
print(i18n.get('websocket_started', host, port))
async def handle_connection(self, websocket, path):
"""处理WebSocket连接"""
self.logger.info(f"新连接: {websocket.remote_address}, path: {path}")
await self.ws_handler.register(websocket)
try:
async for message in websocket:
await self.ws_handler.handle_message(websocket, message)
except websockets.exceptions.ConnectionClosed as e:
self.logger.info(f"连接关闭: {e}")
except Exception as e:
self.logger.error(f"处理错误: {e}")
finally:
await self.ws_handler.unregister(websocket)
async def websocket_server(self):
"""启动WebSocket服务器并集成任务调度器"""
host = constants.WEBSOCKET_HOST
port = constants.WEBSOCKET_PORT
self.ws_handler = WebSocketHandler(self)
# 如果启用了任务调度器,创建并启动
if constants.SCHEDULER_ENABLED:
self.scheduler = TaskScheduler(asyncio.get_running_loop(), self.logger)
self.scheduler.set_websocket_handler(self.ws_handler)
self.scheduler.set_ai_manager(self) # 设置AI管理器引用
async def handler(websocket):
await self.handle_connection(websocket, None)
server = await websockets.serve(handler, host, port)
self.logger.info(i18n.get('websocket_started', host, port))
try:
if self.scheduler:
# 同时运行服务器和调度器
await asyncio.gather(
server.wait_closed(),
self.scheduler.run(self.memo_db)
)
else:
# 只运行服务器
await server.wait_closed()
finally:
if self.scheduler:
self.scheduler.stop()
def run(self):
"""运行WebSocket服务器"""
self.add_ai()
self.show_welcome_message()
# 检查过期任务
overdue = self.memo_db.get_overdue_memos()
if overdue:
print(f"\n⏰ {i18n.get('overdue_found', len(overdue))}")
for memo in overdue[:5]:
immediate_tag = "⚡" if memo.get('is_immediate') else ""
repeat_info = ""
if memo['repeat_type'] != 'none':
if memo['repeat_type'] == 'custom':
unit = memo.get('repeat_interval_unit', 'days')
repeat_info = f" (每{memo['repeat_interval']} {unit})"
else:
repeat_info = f" ({memo['repeat_type']})"
print(f" • {immediate_tag}[ID: {memo['id']}] {memo['title']}{repeat_info} - {memo['reminder_time']}")
if len(overdue) > 5:
print(f" ... {i18n.get('more_skills', len(overdue)-5)}")
# 运行WebSocket服务器
try:
# 使用 asyncio.run() 运行服务器
asyncio.run(self.websocket_server())
except KeyboardInterrupt:
print(f"\n\n{i18n.get('user_interrupted')}")
self.history_manager.end_current_conversation()
except Exception as e:
print(f"\n{i18n.get('error_prefix')}{e}")
import traceback
traceback.print_exc()
# ==================== 命令行参数处理 ====================
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description='单AI智能体系统 - WebSocket版本(所有任务都是对话任务)')
parser.add_argument('--verbose', '-v', action='store_true', help='显示详细日志')
parser.add_argument('--check-memo', action='store_true', help='立即检查到期任务并退出')
parser.add_argument('--memo-add', type=str, help='添加任务,格式: "标题|内容|提醒时间|重复类型|重复间隔|单位"')
parser.add_argument('--memo-list', action='store_true', help='列出任务')
parser.add_argument('--memo-complete', type=int, help='完成指定ID的任务')
parser.add_argument('-d', '--debug', type=int, choices=[0, 1, 2, 3], default=None, help='调试级别')
parser.add_argument('-c', '--config', type=str, help='指定配置文件路径')
# 添加语言选项
parser.add_argument('--lang', type=str, choices=['zh', 'en'], help='强制指定语言 (zh/en)')
# WebSocket选项
parser.add_argument('--host', type=str, help='WebSocket服务器主机')
parser.add_argument('--port', type=int, help='WebSocket服务器端口')
# 调度器选项
parser.add_argument('--no-scheduler', action='store_true', help='禁用任务调度器')
parser.add_argument('--scheduler-interval', type=float, default=1.0, help='调度器检查间隔(秒)')
return parser.parse_args()
def create_command_executor(args):
"""根据命令行参数创建命令执行器"""
# 现在只使用本地执行器
return SubprocessCommandExecutor()
def handle_check_commands(manager):
"""处理检查命令"""
args = parse_args()
if args.check_memo:
print(i18n.get('memo_check_title'))
overdue = manager.memo_db.get_overdue_memos()
if overdue:
print(i18n.get('memo_check_found', len(overdue)))
for memo in overdue:
immediate_tag = "⚡" if memo.get('is_immediate') else ""
repeat_info = ""
if memo['repeat_type'] != 'none':
if memo['repeat_type'] == 'custom':
unit = memo.get('repeat_interval_unit', 'days')
repeat_info = f" (每{memo['repeat_interval']} {unit})"
else:
repeat_info = f" ({memo['repeat_type']})"
print(f" • {immediate_tag}[ID: {memo['id']}] {memo['title']}{repeat_info} (提醒: {memo['reminder_time']})")
else:
print(i18n.get('memo_check_none'))
return True
return False
def handle_memo_commands(manager):
"""处理备忘录命令"""
args = parse_args()
if args.memo_list:
memos = manager.memo_db.get_all_memos(include_completed=False)
if not memos:
print("📭 " + i18n.get('memo_check_none'))
else:
print(i18n.get('memo_list_title', len(memos)))
for memo in memos:
overdue = ""
if memo['reminder_time']:
try:
reminder = datetime.strptime(memo['reminder_time'], constants.DATETIME_FORMAT)
if reminder < datetime.now():
overdue = " ⚠️"
except:
pass
immediate_tag = "⚡" if memo.get('is_immediate') else ""
repeat_info = ""
if memo['repeat_type'] != 'none':
if memo['repeat_type'] == 'custom':
unit = memo.get('repeat_interval_unit', 'days')
repeat_info = f" [每{memo['repeat_interval']} {unit}]"
else:
repeat_info = f" [{memo['repeat_type']}]"
print(f" {immediate_tag}[ID: {memo['id']}] {memo['title']}{repeat_info}{overdue}")
return True
if args.memo_add:
try:
parts = args.memo_add.split('|')
title = parts[0]
content = parts[1] if len(parts) > 1 else ""
reminder = parts[2] if len(parts) > 2 else None
# 解析重复参数
repeat_type = parts[3] if len(parts) > 3 else "none"
repeat_interval = int(parts[4]) if len(parts) > 4 else 0
repeat_interval_unit = parts[5] if len(parts) > 5 else "days"
is_immediate = reminder == "immediate" # 如果提醒时间是"immediate",则创建即时任务
memo_id = manager.memo_db.add_memo(
title=title,
content=content,
reminder_time=None if is_immediate else reminder,
created_by="cli",
source="cli",
repeat_type=repeat_type,
repeat_interval=repeat_interval,
repeat_interval_unit=repeat_interval_unit,
is_immediate=is_immediate
)
repeat_msg = ""
if repeat_type != "none":
if repeat_type == "custom":
repeat_msg = f" (每{repeat_interval} {repeat_interval_unit})"
else:
repeat_msg = f" ({repeat_type})"
print(i18n.get('memo_added', memo_id) + repeat_msg + (" (即时任务)" if is_immediate else ""))
except Exception as e:
print(i18n.get('memo_add_failed', e))
return True
if args.memo_complete:
if manager.memo_db.complete_memo(args.memo_complete):
print(i18n.get('memo_completed', args.memo_complete))
else:
print(i18n.get('memo_complete_failed', args.memo_complete))
return True
return False
# ==================== 主程序 ====================
def main():
"""主程序入口"""
args = parse_args()
# 如果命令行指定了语言,覆盖自动检测
if args.lang:
os.environ["AI_LANGUAGE"] = args.lang
# 重新初始化i18n
global i18n
i18n = I18n()
if args.config:
os.environ["AI_CONFIG_PATH"] = args.config
config = ConfigManager()
if args.debug is not None:
config._config["system"]["debug_level"] = args.debug
if args.verbose:
config._config["system"]["debug_level"] = 3
# 覆盖WebSocket主机和端口
if args.host:
config._config["system"]["websocket_host"] = args.host
if args.port:
config._config["system"]["websocket_port"] = args.port
# 调度器设置
if args.no_scheduler:
config._config["scheduler"]["enabled"] = False
if args.scheduler_interval:
config._config["scheduler"]["check_interval"] = args.scheduler_interval
# 确保 ~/.aibox 目录存在
os.makedirs(constants.SAVE_DIR, exist_ok=True)
# 确保 skills 目录存在
os.makedirs(constants.SKILLS_DIR, exist_ok=True)
# 确保 prompts 目录存在
os.makedirs(constants.PROMPTS_DIR, exist_ok=True)
Logger.init_file_logging()
# 创建命令执行器 - 现在只使用本地执行器
command_executor = SubprocessCommandExecutor()
manager = EnhancedAIManager(
debug_level=args.debug,
command_executor=command_executor
)
if handle_check_commands(manager):
return
if handle_memo_commands(manager):
return
# 运行WebSocket服务器
manager.run()
if __name__ == "__main__":
main()