feat: интеллектуальная cron-система с AI-агентом

Основные изменения:
- CronJob теперь хранит prompt для ИИ вместо команды
- Добавлены поля: user_id, notify, log_results
- Задачи выполняются через AI-агент (автономный выбор инструмента)
- Планировщик проверяет задачи каждую минуту
- Уведомления отправляются в Telegram (если notify=True)
- Результаты сохраняются в cron_logs/ (если log_results=True)
- Добавлена команда /cron для управления задачами
- Обновлена БД и модель данных

Новые файлы:
- bot/services/cron_scheduler.py - планировщик задач
- CRON_SYSTEM.md - документация

Изменённые файлы:
- bot/tools/cron_tool.py - обновлён для работы с промптами
- bot/handlers/commands.py - добавлена cron_command
- bot.py - интеграция планировщика, регистрация команды
- .gitignore - исключение cron_logs/

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
mirivlad 2026-02-25 12:36:32 +08:00
parent bff74741a6
commit f559c83baa
6 changed files with 874 additions and 59 deletions

1
.gitignore vendored
View File

@ -22,6 +22,7 @@ bot_config.json
# Logs # Logs
*.log *.log
cron_logs/
# Cache # Cache
.cache/ .cache/

219
CRON_SYSTEM.md Normal file
View File

@ -0,0 +1,219 @@
# 🕐 Интеллектуальная Cron-система
Интеллектуальная система планирования задач для Telegram CLI Bot.
## 📋 Особенности
В отличие от классического cron, задачи выполняются не как команды, а как **промпты для ИИ-агента**.
### Структура задачи
```python
CronJob:
- id: int # Уникальный ID
- name: str # Название задачи
- prompt: str # Промпт для ИИ-агента
- schedule: str # Расписание (@hourly, @daily, */5 * * * *)
- user_id: int # ID пользователя Telegram
- enabled: bool # Включена ли задача
- notify: bool # Уведомлять пользователя в Telegram
- log_results: bool # Сохранять результат в лог-файл
- last_run: datetime # Последнее выполнение
- next_run: datetime # Следующее выполнение
- created_at: datetime # Дата создания
```
## 🔄 Процесс выполнения
```
┌─────────────────────────────────────────────────────────┐
│ Cron Scheduler (проверяет каждую минуту) │
│ ↓ (если время пришло) │
│ Отправляет промпт ИИ-агенту │
│ ↓ │
│ ИИ-агент (Рик) анализирует промпт: │
│ - Решает какой инструмент использовать │
│ - Выполняет инструмент (поиск, SSH, RSS) │
│ ↓ │
│ Если notify=True → отправка уведомления в Telegram │
│ Если log_results=True → сохранение в лог-файл │
└─────────────────────────────────────────────────────────┘
```
## 📝 Команды управления
### `/cron list` - Показать все задачи
```
/cron list
```
**Пример вывода:**
```
⏰ Ваши задачи:
✅ Проверка диска (ID: 1)
🔔📝 Промпт: Проверить свободное место на сервере...
Расписание: @daily
Следующий запуск: 2026-02-26 00:00:00
Последний запуск: 2026-02-25 00:00:00
```
### `/cron add` - Добавить задачу
```
/cron add <name> <schedule> <prompt>
```
**Параметры:**
- `name` - название задачи
- `schedule` - расписание:
- `@hourly` - каждый час
- `@daily` - каждый день
- `@weekly` - каждую неделю
- `*/5 * * * *` - каждые 5 минут (cron format)
- `prompt` - промпт для ИИ-агента
**Примеры:**
```bash
# Ежедневная проверка диска
/cron add check_disk @daily Проверить свободное место на сервере home
# Ежечасные новости
/cron add tech_news @hourly Что нового в Linux сегодня
# Каждые 5 минут мониторинг
/cron add monitor */5 * * * * Проверить нагрузку на сервер
```
### `/cron run` - Выполнить задачу немедленно
```
/cron run <id>
```
**Пример:**
```
/cron run 1
```
### `/cron remove` - Удалить задачу
```
/cron remove <id>
```
### `/cron toggle` - Включить/выключить задачу
```
/cron toggle <id>
```
## 🛠️ Инструменты ИИ-агента
При выполнении задачи ИИ-агент может использовать:
| Инструмент | Назначение | Триггеры |
|------------|------------|----------|
| `ddgs_tool` | Поиск в интернете | "найди", "поиск", "узнай" |
| `rss_tool` | Чтение RSS лент | "новости", "почитай", "лента" |
| `ssh_tool` | SSH-команды | "проверь сервер", "выполни команду" |
| `cron_tool` | Управление задачами | "напомни", "запланируй" |
## 📂 Логирование
Результаты выполнения задач сохраняются в:
```
cron_logs/
cron_job_1_check_disk.log
cron_job_2_tech_news.log
...
```
**Формат лога:**
```
============================================================
[2026-02-25 10:30:00] Задача: Проверка диска (ID: 1)
============================================================
Промпт:
Проверить свободное место на сервере home
Результат:
Задача 'Проверка диска' выполнена.
Использован инструмент: ssh_tool
Результат: Filesystem Size Used Avail Use% Mounted on
...
```
## 🔔 Уведомления
Если `notify=True`, бот отправляет уведомление в Telegram:
```
✅ Задача 'Проверка диска' выполнена.
Использован инструмент: ssh_tool
Результат: Свободно 45GB на /dev/sda1
```
## 💡 Примеры использования
### 1. Ежедневный мониторинг диска
```bash
/cron add disk_daily @daily Проверить свободное место на сервере home. Если меньше 10GB - предупредить
```
### 2. Ежечасные новости IT
```bash
/cron add it_news @hourly Найти свежие новости про Python и Linux за последний час
```
### 3. Мониторинг нагрузки каждые 5 минут
```bash
/cron add load_monitor */5 * * * * Проверить нагрузку CPU и RAM на сервере
```
### 4. Еженедельный поиск уязвимостей
```bash
/cron add security_scan @weekly Найти информацию о новых уязвимостях в Linux за неделю
```
## 🚀 Архитектура
```
bot/
tools/
cron_tool.py # Инструмент управления задачами
services/
cron_scheduler.py # Планировщик (проверка каждую минуту)
handlers/
commands.py # Обработчик команды /cron
```
## ⚙️ Технические детали
- **Проверка задач:** каждую минуту (60 секунд)
- **Хранение:** SQLite (`cron.db`)
- **Логи:** текстовые файлы (`cron_logs/`)
- **Формат расписания:** cron format или специальные (@hourly, @daily, @weekly)
## 🎯 Отличия от классического cron
| Классический cron | Интеллектуальный cron |
|-------------------|----------------------|
| Выполняет команды | Выполняет промпты для ИИ |
| Жёсткая логика | Гибкое решение через ИИ |
| Вывод в stdout/email | Уведомления в Telegram + логи |
| Нет контекста | ИИ использует контекст и память |
---
*Версия: 0.5.3*
*Интеллектуальная cron-система с AI-агентом*

63
bot.py
View File

@ -77,13 +77,14 @@ from bot.utils.decorators import check_access
from bot.keyboards.menus import MenuItem, init_menus from bot.keyboards.menus import MenuItem, init_menus
# Импорты хендлеров из модулей # Импорты хендлеров из модулей
from bot.handlers.commands import start_command, menu_command, help_command, settings_command from bot.handlers.commands import start_command, menu_command, help_command, settings_command, cron_command
from bot.handlers.callbacks import menu_callback from bot.handlers.callbacks import menu_callback
from bot.services.command_executor import execute_cli_command from bot.services.command_executor import execute_cli_command
# Импорты инструментов и AI агента # Импорты инструментов и AI агента
from bot.ai_agent import ai_agent from bot.ai_agent import ai_agent
from bot.tools import tools_registry from bot.tools import tools_registry
from bot.services.cron_scheduler import init_scheduler, get_scheduler
# Глобальные менеджеры сессий # Глобальные менеджеры сессий
ssh_session_manager = SSHSessionManager() ssh_session_manager = SSHSessionManager()
@ -371,30 +372,41 @@ def format_tool_result(tool_name: str, result: 'ToolResult') -> str:
return output return output
elif tool_name == 'cron_manager': elif tool_name == 'cron_tool':
action = result.metadata.get('action', 'list') action = result.metadata.get('action', 'list')
if action == 'list' and result.data: if action == 'list' and result.data:
output = "⏰ **Ваши задачи:**\n\n" output = "⏰ **Ваши задачи:**\n\n"
for job in result.data: for job in result.data:
status = "" if job.get('enabled') else "" status = "" if job.get('enabled') else ""
output += f"{status} **{job.get('name', 'Без названия')}**\n" notify_icon = "🔔" if job.get('notify') else "🔕"
output += f" Команда: `{job.get('command', '')}`\n" log_icon = "📝" if job.get('log_results') else "🚫"
output += f" Расписание: {job.get('schedule', '')}\n" output += f"{status} **{job.get('name', 'Без названия')}** (ID: {job.get('id')})\n"
output += f" {notify_icon}{log_icon} Промпт: _{job.get('prompt', '')[:100]}_{'...' if len(job.get('prompt', '')) > 100 else ''}\n"
output += f" Расписание: `{job.get('schedule', '')}`\n"
if job.get('next_run'): if job.get('next_run'):
output += f" Следующий запуск: {job.get('next_run')}\n" output += f" Следующий запуск: {job.get('next_run')}\n"
if job.get('last_run'):
output += f" Последний запуск: {job.get('last_run')}\n"
output += "\n" output += "\n"
return output return output
elif action == 'add' and result.success: elif action == 'add' and result.success:
data = result.data data = result.data
return f"✅ **Задача добавлена:**\n• ID: {data.get('id')}\n• Название: {data.get('name')}\n• Расписание: {data.get('schedule')}\n• Следующий запуск: {data.get('next_run', 'N/A')}" notify_status = "🔔 Уведомлять" if result.metadata.get('notify') else "🔕 Без уведомлений"
log_status = "📝 Логировать" if result.metadata.get('log_results') else "🚫 Без логов"
return f"✅ **Задача добавлена:**\n• ID: {data.get('id')}\n• Название: {data.get('name')}\n• Расписание: {data.get('schedule')}\n{notify_status}, {log_status}\n• Следующий запуск: {data.get('next_run', 'N/A')}"
elif action == 'remove' and result.success: elif action == 'remove' and result.success:
return f"✅ **Задача удалена:** ID {result.data.get('id')}" return f"✅ **Задача удалена:** ID {result.data.get('id')}"
elif action == 'run' and result.success: elif action == 'run' and result.success:
return f"✅ **Задача выполнена:** {result.data.get('message', '')}" result_text = result.metadata.get('result_text', 'Задача выполнена')
tool_used = result.data.get('tool_used', 'не указан')
return f"✅ **Задача выполнена:**\n\n{result_text}\n\n🔧 Инструмент: {tool_used}"
elif action == 'run' and not result.success:
return f"❌ **Ошибка выполнения задачи:**\n{result.error}"
return f"Cron: {result.data}" return f"Cron: {result.data}"
@ -1277,6 +1289,7 @@ async def post_init(application: Application):
BotCommand("menu", "Главное меню с кнопками"), BotCommand("menu", "Главное меню с кнопками"),
BotCommand("help", "Справка"), BotCommand("help", "Справка"),
BotCommand("settings", "Настройки"), BotCommand("settings", "Настройки"),
BotCommand("cron", "Управление задачами"),
BotCommand("stop", "Прервать SSH-сессию"), BotCommand("stop", "Прервать SSH-сессию"),
BotCommand("ai", "Задача для Qwen Code AI"), BotCommand("ai", "Задача для Qwen Code AI"),
BotCommand("memory", "Статистика памяти ИИ"), BotCommand("memory", "Статистика памяти ИИ"),
@ -1285,9 +1298,44 @@ async def post_init(application: Application):
] ]
await application.bot.set_my_commands(commands) await application.bot.set_my_commands(commands)
# Инициализация планировщика cron-задач
cron_tool = tools_registry.get_tool('cron_tool')
if cron_tool:
scheduler = init_scheduler(cron_tool, ai_agent, send_notification=send_cron_notification)
await scheduler.start()
logger.info("🕐 Планировщик cron-задач инициализирован")
else:
logger.warning("⚠️ Cron инструмент не найден, планировщик не запущен")
logger.info("Бот инициализирован") logger.info("Бот инициализирован")
async def send_cron_notification(user_id: int, message: str):
"""
Отправить уведомление пользователю о результате cron-задачи.
Args:
user_id: ID пользователя в Telegram
message: Текст уведомления
"""
try:
# Получаем application из контекста
from telegram.ext import Application
app = Application.get_instance()
if app:
await app.bot.send_message(
chat_id=user_id,
text=message,
parse_mode="Markdown"
)
logger.info(f"🔔 Уведомление отправлено пользователю {user_id}")
else:
logger.warning("Application не инициализирован, уведомление не отправлено")
except Exception as e:
logger.exception(f"Ошибка отправки уведомления: {e}")
@check_access @check_access
async def stop_command(update: Update, context: ContextTypes.DEFAULT_TYPE): async def stop_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработка команды /stop - прерывание активной SSH-сессии.""" """Обработка команды /stop - прерывание активной SSH-сессии."""
@ -1629,6 +1677,7 @@ def main():
application.add_handler(CommandHandler("start", start_command)) application.add_handler(CommandHandler("start", start_command))
application.add_handler(CommandHandler("help", help_command)) application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler("settings", settings_command)) application.add_handler(CommandHandler("settings", settings_command))
application.add_handler(CommandHandler("cron", cron_command))
application.add_handler(CommandHandler("menu", menu_command)) application.add_handler(CommandHandler("menu", menu_command))
application.add_handler(CommandHandler("stop", stop_command)) application.add_handler(CommandHandler("stop", stop_command))
application.add_handler(CommandHandler("memory", memory_command)) application.add_handler(CommandHandler("memory", memory_command))

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Обработчики команд бота (/start, /menu, /help, /settings).""" """Обработчики команд бота (/start, /menu, /help, /settings, /cron)."""
import logging import logging
from telegram import Update from telegram import Update
@ -8,6 +8,8 @@ from telegram.ext import ContextTypes
# Импорты из модулей bot/ # Импорты из модулей bot/
from bot.config import config, state_manager, server_manager, menu_builder from bot.config import config, state_manager, server_manager, menu_builder
from bot.utils.decorators import check_access from bot.utils.decorators import check_access
from bot.tools import tools_registry
from bot.ai_agent import ai_agent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -114,3 +116,207 @@ async def settings_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
parse_mode="Markdown", parse_mode="Markdown",
reply_markup=menu_builder.get_keyboard("settings") reply_markup=menu_builder.get_keyboard("settings")
) )
@check_access
async def cron_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Обработка команды /cron - управление задачами.
Использование:
/cron list - показать все задачи
/cron add <name> <schedule> <prompt> - добавить задачу
/cron run <id> - выполнить задачу немедленно
/cron remove <id> - удалить задачу
/cron toggle <id> - включить/выключить задачу
"""
user_id = update.effective_user.id
args = context.args
# Получаем cron инструмент
cron_tool = tools_registry.get_tool('cron_tool')
if not cron_tool:
await update.message.reply_text("❌ Ошибка: cron инструмент не найден")
return
# Парсим команду
if not args:
# По умолчанию показываем список задач
action = 'list'
else:
action = args[0].lower()
try:
if action == 'list':
# Показать все задачи пользователя
result = await cron_tool.execute(action='list', user_id=user_id)
if result.success and result.data:
output = "⏰ **Ваши задачи:**\n\n"
for job in result.data:
status = "" if job.get('enabled') else ""
notify_icon = "🔔" if job.get('notify') else "🔕"
log_icon = "📝" if job.get('log_results') else "🚫"
output += f"{status} **{job.get('name', 'Без названия')}** (ID: {job.get('id')})\n"
output += f" {notify_icon}{log_icon} Промпт: _{job.get('prompt', '')[:100]}_{'...' if len(job.get('prompt', '')) > 100 else ''}\n"
output += f" Расписание: `{job.get('schedule', '')}`\n"
if job.get('next_run'):
output += f" Следующий запуск: {job.get('next_run')}\n"
if job.get('last_run'):
output += f" Последний запуск: {job.get('last_run')}\n"
output += "\n"
if not output.strip():
output = "📭 У вас пока нет задач.\n\nДобавьте задачу командой:\n`/cron add <name> <schedule> <prompt>`"
await update.message.reply_text(output, parse_mode="Markdown")
else:
await update.message.reply_text("📭 У вас пока нет задач.")
elif action == 'add':
if len(args) < 4:
await update.message.reply_text(
"❌ **Недостаточно аргументов**\n\n"
"**Использование:**\n"
"`/cron add <name> <schedule> <prompt> [notify] [log]`\n\n"
"**Примеры:**\n"
"`/cron check_disk Ежедневно проверять диск на сервере`\n"
"`/cron news hourly Что нового в Linux сегодня`\n\n"
"**Расписание:**\n"
"• `@hourly` - каждый час\n"
"• `@daily` - каждый день\n"
"• `@weekly` - каждую неделю\n"
"• `*/5 * * * *` - каждые 5 минут",
parse_mode="Markdown"
)
return
name = args[1]
schedule = args[2]
# Промпт может содержать пробелы - берём всё после schedule
prompt = ' '.join(args[3:])
# Парсим опциональные параметры
notify = 'notify' in prompt.lower()
log_results = 'no_log' not in prompt.lower() and 'без_лога' not in prompt.lower()
result = await cron_tool.execute(
action='add',
name=name,
prompt=prompt,
schedule=schedule,
user_id=user_id,
notify=notify,
log_results=log_results
)
if result.success:
notify_status = "🔔 Уведомлять" if notify else "🔕 Без уведомлений"
log_status = "📝 Логировать" if log_results else "🚫 Без логов"
await update.message.reply_text(
f"✅ **Задача добавлена:**\n"
f"• ID: {result.data.get('id')}\n"
f"• Название: {name}\n"
f"• Промпт: _{prompt}_\n"
f"• Расписание: `{schedule}`\n"
f"{notify_status}, {log_status}\n"
f"• Следующий запуск: {result.data.get('next_run', 'N/A')}",
parse_mode="Markdown"
)
else:
await update.message.reply_text(f"❌ Ошибка: {result.error}")
elif action == 'run':
if len(args) < 2:
await update.message.reply_text("❌ Укажите ID задачи: `/cron run <id>`")
return
try:
job_id = int(args[1])
except ValueError:
await update.message.reply_text("❌ ID должен быть числом")
return
status_msg = await update.message.reply_text("⏳ Выполняю задачу...")
# Выполняем задачу через AI-агент
result = await cron_tool.execute(
action='run',
job_id=job_id,
ai_agent=ai_agent,
user_id=user_id
)
await status_msg.delete()
if result.success:
result_text = result.metadata.get('result_text', 'Задача выполнена')
tool_used = result.data.get('tool_used', 'не указан')
await update.message.reply_text(
f"✅ **Задача выполнена:**\n\n{result_text}\n\n🔧 Инструмент: {tool_used}",
parse_mode="Markdown"
)
else:
await update.message.reply_text(f"❌ Ошибка: {result.error}")
elif action == 'remove':
if len(args) < 2:
await update.message.reply_text("❌ Укажите ID задачи: `/cron remove <id>`")
return
try:
job_id = int(args[1])
except ValueError:
await update.message.reply_text("❌ ID должен быть числом")
return
result = await cron_tool.execute(action='remove', job_id=job_id)
if result.success:
await update.message.reply_text(f"✅ Задача удалена: ID {job_id}")
else:
await update.message.reply_text(f"❌ Ошибка: {result.error}")
elif action == 'toggle':
if len(args) < 2:
await update.message.reply_text("❌ Укажите ID задачи: `/cron toggle <id>`")
return
try:
job_id = int(args[1])
except ValueError:
await update.message.reply_text("❌ ID должен быть числом")
return
# Получаем текущее состояние задачи
list_result = await cron_tool.execute(action='list', user_id=user_id)
current_state = True
for job in list_result.data:
if job['id'] == job_id:
current_state = job.get('enabled', True)
break
new_state = not current_state
result = await cron_tool.execute(action='toggle', job_id=job_id, enabled=new_state)
if result.success:
state_text = "включена" if new_state else "выключена"
await update.message.reply_text(f"✅ Задача ID {job_id} {state_text}")
else:
await update.message.reply_text(f"❌ Ошибка: {result.error}")
else:
await update.message.reply_text(
"❌ Неизвестная команда.\n\n"
"**Доступные команды:**\n"
"• `/cron list` - показать все задачи\n"
"• `/cron add <name> <schedule> <prompt>` - добавить задачу\n"
"• `/cron run <id>` - выполнить задачу\n"
"• `/cron remove <id>` - удалить задачу\n"
"• `/cron toggle <id>` - включить/выключить задачу",
parse_mode="Markdown"
)
except Exception as e:
logger.exception(f"Ошибка в команде /cron: {e}")
await update.message.reply_text(f"❌ Ошибка: {e}")

View File

@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""
Cron Scheduler - планировщик задач для автоматического выполнения.
Проверяет задачи каждую минуту и выполняет те, у которых наступило время.
"""
import logging
import asyncio
from datetime import datetime
from typing import Optional, Callable, Awaitable
from pathlib import Path
logger = logging.getLogger(__name__)
class CronScheduler:
"""
Планировщик cron-задач.
Автоматически проверяет задачи и выполняет их через AI-агент.
"""
def __init__(
self,
cron_tool,
ai_agent,
send_notification: Optional[Callable[[int, str], Awaitable[None]]] = None
):
"""
Инициализировать планировщик.
Args:
cron_tool: Экземпляр CronTool
ai_agent: Экземпляр AI-агента для выполнения задач
send_notification: Асинхронная функция для отправки уведомлений (user_id, message)
"""
self.cron_tool = cron_tool
self.ai_agent = ai_agent
self.send_notification = send_notification
self._running = False
self._task: Optional[asyncio.Task] = None
self._check_interval = 60 # Проверка каждую минуту
async def start(self):
"""Запустить планировщик в фоновом режиме."""
if self._running:
logger.warning("Планировщик уже запущен")
return
self._running = True
self._task = asyncio.create_task(self._run_loop())
logger.info("🕐 Планировщик cron-задач запущен")
async def stop(self):
"""Остановить планировщик."""
self._running = False
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
logger.info("🕐 Планировщик cron-задач остановлен")
async def _run_loop(self):
"""Основной цикл планировщика."""
while self._running:
try:
await self._check_and_run_tasks()
except Exception as e:
logger.exception(f"Ошибка в цикле планировщика: {e}")
await asyncio.sleep(self._check_interval)
async def _check_and_run_tasks(self):
"""Проверить задачи и выполнить те, у которых наступило время."""
now = datetime.now()
logger.debug(f"🕐 Проверка задач на {now.strftime('%Y-%m-%d %H:%M:%S')}")
# Получаем список всех задач
result = await self.cron_tool.list_jobs()
if not result.success:
logger.error(f"Ошибка получения списка задач: {result.error}")
return
jobs = result.data
executed_count = 0
for job in jobs:
if not job.get('enabled'):
continue
next_run_str = job.get('next_run')
if not next_run_str:
continue
try:
next_run = datetime.strptime(next_run_str, '%Y-%m-%d %H:%M:%S')
except ValueError:
logger.error(f"Ошибка парсинга next_run для задачи {job['id']}: {next_run_str}")
continue
# Если время пришло
if now >= next_run:
logger.info(f"⏰ Время задачи #{job['id']}: {job['name']}")
await self._execute_job(job)
executed_count += 1
if executed_count > 0:
logger.info(f"✅ Выполнено задач: {executed_count}")
async def _execute_job(self, job: dict):
"""
Выполнить задачу.
Args:
job: Словарь с данными задачи
"""
job_id = job['id']
job_name = job['name']
notify = job.get('notify', False)
log_results = job.get('log_results', True)
user_id = job.get('user_id') # ID пользователя который создал задачу
# Выполняем задачу через AI-агент
result = await self.cron_tool.run_job(
job_id=job_id,
ai_agent=self.ai_agent,
user_id=user_id
)
if result.success:
logger.info(f"✅ Задача '{job_name}' выполнена успешно")
# Отправляем уведомление если нужно
if notify and self.send_notification and user_id:
result_text = result.metadata.get('result_text', 'Задача выполнена')
await self.send_notification(user_id, result_text)
else:
logger.error(f"❌ Задача '{job_name}' не выполнена: {result.error}")
if notify and self.send_notification and user_id:
await self.send_notification(
user_id,
f"❌ **Ошибка задачи '{job_name}':**\n{result.error}"
)
def set_notification_callback(self, callback: Callable[[int, str], Awaitable[None]]):
"""Установить callback для отправки уведомлений."""
self.send_notification = callback
# Глобальный планировщик
scheduler: Optional[CronScheduler] = None
def init_scheduler(cron_tool, ai_agent, send_notification=None) -> CronScheduler:
"""Инициализировать глобальный планировщик."""
global scheduler
scheduler = CronScheduler(cron_tool, ai_agent, send_notification)
return scheduler
def get_scheduler() -> Optional[CronScheduler]:
"""Получить глобальный планировщик."""
return scheduler

View File

@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Cron Tool - инструмент для управления задачами пользователя. Cron Tool - инструмент для управления интеллектуальными задачами.
Позволяет создавать, планировать и выполнять периодические задачи. Позволяет создавать, планировать и выполнять периодические задачи через AI-агент.
Задачи хранятся как промпты для ИИ, а не как команды.
""" """
import logging import logging
@ -20,26 +21,46 @@ logger = logging.getLogger(__name__)
@dataclass @dataclass
class CronJob: class CronJob:
"""Задача cron.""" """
Интеллектуальная задача cron.
Attributes:
id: ID задачи
name: Название задачи
prompt: Промпт для ИИ-агента (вместо команды)
schedule: Расписание (cron format: "*/5 * * * *" или "@daily", "@hourly")
enabled: Включена ли задача
user_id: ID пользователя Telegram
notify: Уведомлять ли пользователя в Telegram о результате
log_results: Сохранять ли результат в лог-файл
last_run: Время последнего выполнения
next_run: Время следующего выполнения
created_at: Время создания
"""
id: Optional[int] id: Optional[int]
name: str name: str
command: str prompt: str # Промпт для ИИ вместо команды
schedule: str # cron format: "*/5 * * * *" или "daily", "hourly" schedule: str
user_id: Optional[int] = None # ID пользователя Telegram
enabled: bool = True enabled: bool = True
notify: bool = False # Уведомлять пользователя в Telegram
log_results: bool = True # Сохранять в лог
last_run: Optional[datetime] = None last_run: Optional[datetime] = None
next_run: Optional[datetime] = None next_run: Optional[datetime] = None
created_at: datetime = field(default_factory=datetime.now) created_at: datetime = field(default_factory=datetime.now)
class CronTool(BaseTool): class CronTool(BaseTool):
"""Инструмент для управления задачами пользователя.""" """Инструмент для управления интеллектуальными задачами пользователя."""
name = "cron_tool" name = "cron_tool"
description = "Управление периодическими задачами пользователя. Создание, планирование и выполнение задач по расписанию." description = "Управление периодическими задачами через AI-агент. Создание, планирование и выполнение задач по расписанию."
category = "automation" category = "automation"
def __init__(self, db_path: str = None): def __init__(self, db_path: str = None, log_dir: str = None):
self.db_path = Path(db_path) if db_path else Path(__file__).parent.parent.parent / "cron.db" self.db_path = Path(db_path) if db_path else Path(__file__).parent.parent.parent / "cron.db"
self.log_dir = Path(log_dir) if log_dir else Path(__file__).parent.parent.parent / "cron_logs"
self.log_dir.mkdir(parents=True, exist_ok=True)
self._jobs: Dict[int, CronJob] = {} self._jobs: Dict[int, CronJob] = {}
self._init_db() self._init_db()
@ -47,18 +68,32 @@ class CronTool(BaseTool):
"""Инициализировать БД.""" """Инициализировать БД."""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
c = conn.cursor() c = conn.cursor()
# Создаём таблицу с user_id
c.execute(''' c.execute('''
CREATE TABLE IF NOT EXISTS cron_jobs ( CREATE TABLE IF NOT EXISTS cron_jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
command TEXT NOT NULL, prompt TEXT NOT NULL,
schedule TEXT NOT NULL, schedule TEXT NOT NULL,
user_id INTEGER,
enabled INTEGER DEFAULT 1, enabled INTEGER DEFAULT 1,
notify INTEGER DEFAULT 0,
log_results INTEGER DEFAULT 1,
last_run DATETIME, last_run DATETIME,
next_run DATETIME, next_run DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) )
''') ''')
# Проверяем есть ли колонка user_id (для обратной совместимости)
c.execute("PRAGMA table_info(cron_jobs)")
columns = [col[1] for col in c.fetchall()]
if 'user_id' not in columns:
logger.info("Добавление колонки user_id в таблицу cron_jobs")
c.execute('ALTER TABLE cron_jobs ADD COLUMN user_id INTEGER')
conn.commit() conn.commit()
conn.close() conn.close()
@ -93,8 +128,18 @@ class CronTool(BaseTool):
return None return None
async def add_job(self, name: str, command: str, schedule: str) -> ToolResult: async def add_job(self, name: str, prompt: str, schedule: str, user_id: int = None, notify: bool = False, log_results: bool = True) -> ToolResult:
"""Добавить задачу.""" """
Добавить интеллектуальную задачу.
Args:
name: Название задачи
prompt: Промпт для ИИ-агента
schedule: Расписание (cron format или @daily, @hourly, @weekly)
user_id: ID пользователя Telegram
notify: Уведомлять ли пользователя в Telegram
log_results: Сохранять ли результат в лог
"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
c = conn.cursor() c = conn.cursor()
@ -103,9 +148,9 @@ class CronTool(BaseTool):
next_run_str = next_run.strftime('%Y-%m-%d %H:%M:%S') if next_run else None next_run_str = next_run.strftime('%Y-%m-%d %H:%M:%S') if next_run else None
c.execute(''' c.execute('''
INSERT INTO cron_jobs (name, command, schedule, next_run) INSERT INTO cron_jobs (name, prompt, schedule, user_id, notify, log_results, next_run)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
''', (name, command, schedule, next_run_str)) ''', (name, prompt, schedule, user_id, 1 if notify else 0, 1 if log_results else 0, next_run_str))
job_id = c.lastrowid job_id = c.lastrowid
conn.commit() conn.commit()
@ -113,15 +158,18 @@ class CronTool(BaseTool):
self._jobs[job_id] = CronJob( self._jobs[job_id] = CronJob(
id=job_id, id=job_id,
name=name, name=name,
command=command, prompt=prompt,
schedule=schedule, schedule=schedule,
user_id=user_id,
notify=notify,
log_results=log_results,
next_run=next_run next_run=next_run
) )
return ToolResult( return ToolResult(
success=True, success=True,
data={'id': job_id, 'name': name, 'schedule': schedule, 'next_run': next_run_str}, data={'id': job_id, 'name': name, 'prompt': prompt, 'schedule': schedule, 'user_id': user_id, 'next_run': next_run_str},
metadata={'status': 'added'} metadata={'status': 'added', 'notify': notify, 'log_results': log_results}
) )
except Exception as e: except Exception as e:
@ -133,14 +181,27 @@ class CronTool(BaseTool):
finally: finally:
conn.close() conn.close()
async def list_jobs(self) -> ToolResult: async def list_jobs(self, user_id: int = None) -> ToolResult:
"""Получить список всех задач.""" """
Получить список всех задач.
Args:
user_id: ID пользователя для фильтрации (если None - все задачи)
"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
c = conn.cursor() c = conn.cursor()
c.execute('''
SELECT id, name, command, schedule, enabled, last_run, next_run, created_at if user_id:
FROM cron_jobs ORDER BY id c.execute('''
''') SELECT id, name, prompt, schedule, user_id, enabled, notify, log_results, last_run, next_run, created_at
FROM cron_jobs WHERE user_id = ? ORDER BY id
''', (user_id,))
else:
c.execute('''
SELECT id, name, prompt, schedule, user_id, enabled, notify, log_results, last_run, next_run, created_at
FROM cron_jobs ORDER BY id
''')
rows = c.fetchall() rows = c.fetchall()
conn.close() conn.close()
@ -149,12 +210,15 @@ class CronTool(BaseTool):
jobs.append({ jobs.append({
'id': row[0], 'id': row[0],
'name': row[1], 'name': row[1],
'command': row[2], 'prompt': row[2],
'schedule': row[3], 'schedule': row[3],
'enabled': bool(row[4]), 'user_id': row[4],
'last_run': row[5], 'enabled': bool(row[5]),
'next_run': row[6], 'notify': bool(row[6]),
'created_at': row[7] 'log_results': bool(row[7]),
'last_run': row[8],
'next_run': row[9],
'created_at': row[10]
}) })
return ToolResult( return ToolResult(
@ -206,11 +270,21 @@ class CronTool(BaseTool):
metadata={'status': 'toggled'} metadata={'status': 'toggled'}
) )
async def run_job(self, job_id: int) -> ToolResult: async def run_job(self, job_id: int, ai_agent=None, user_id: int = None) -> ToolResult:
"""Выполнить задачу немедленно.""" """
Выполнить интеллектуальную задачу через AI-агент.
Args:
job_id: ID задачи
ai_agent: Экземпляр AI-агента для выполнения промпта
user_id: ID пользователя для контекста
Returns:
ToolResult с результатом выполнения
"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
c = conn.cursor() c = conn.cursor()
c.execute("SELECT command FROM cron_jobs WHERE id = ?", (job_id,)) c.execute("SELECT name, prompt, notify, log_results FROM cron_jobs WHERE id = ?", (job_id,))
row = c.fetchone() row = c.fetchone()
if not row: if not row:
@ -220,47 +294,145 @@ class CronTool(BaseTool):
error=f"Задача не найдена: {job_id}" error=f"Задача не найдена: {job_id}"
) )
command = row[0] name, prompt, notify, log_results = row
conn.close() conn.close()
# Здесь должна быть логика выполнения команды logger.info(f"🕐 Выполнение задачи #{job_id}: {name}")
# Для демонстрации возвращаем заглушку logger.info(f" Промпт: {prompt}")
logger.info(f"Выполнение задачи {job_id}: {command}")
# Обновляем last_run result_data = {
conn = sqlite3.connect(self.db_path) 'id': job_id,
c = conn.cursor() 'name': name,
c.execute("UPDATE cron_jobs SET last_run = datetime('now') WHERE id = ?", (job_id,)) 'prompt': prompt,
conn.commit() 'executed_at': datetime.now().isoformat()
conn.close() }
return ToolResult( # Выполняем задачу через AI-агент
success=True, if ai_agent:
data={'id': job_id, 'command': command, 'message': 'Задача выполнена'}, try:
metadata={'status': 'executed'} # Отправляем промпт ИИ-агенту
) logger.info(f"🤖 Отправка промпта AI-агенту для задачи {name}")
async def execute(self, action: str = "list", **kwargs) -> ToolResult: # ИИ-агент анализирует промпт и решает какой инструмент использовать
decision = await ai_agent.decide(prompt, context={'user_id': user_id})
if decision.should_use_tool:
logger.info(f"🔧 AI-агент решил использовать инструмент: {decision.tool_name}")
tool_result = await ai_agent.execute_tool(decision.tool_name, **decision.tool_args)
result_data['tool_used'] = decision.tool_name
result_data['tool_result'] = tool_result.to_dict() if hasattr(tool_result, 'to_dict') else str(tool_result)
result_data['success'] = tool_result.success
# Формируем результат
result_text = f"Задача '{name}' выполнена.\n\n"
result_text += f"Использован инструмент: {decision.tool_name}\n"
if tool_result.success:
result_text += f"Результат: {tool_result.data or 'Успешно'}"
else:
result_text += f"Ошибка: {tool_result.error}"
else:
# ИИ решил что инструмент не нужен - выполняем промпт напрямую
logger.info(f" AI-агент решил что инструмент не требуется")
result_text = f"Задача '{name}' выполнена (без инструментов).\nПромпт: {prompt}"
result_data['success'] = True
result_data['ai_reasoning'] = decision.reasoning
# Сохраняем в лог если нужно
if log_results:
self._save_to_log(job_id, name, prompt, result_text)
# Обновляем last_run
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute("UPDATE cron_jobs SET last_run = datetime('now') WHERE id = ?", (job_id,))
conn.commit()
conn.close()
return ToolResult(
success=True,
data=result_data,
metadata={
'status': 'executed',
'notify': notify,
'log_results': log_results,
'result_text': result_text
}
)
except Exception as e:
logger.exception(f"Ошибка выполнения задачи через AI-агент: {e}")
if log_results:
self._save_to_log(job_id, name, prompt, f"Ошибка: {e}")
return ToolResult(
success=False,
error=str(e),
data=result_data
)
else:
# AI-агент не предоставлен - просто логируем
logger.warning(f"AI-агент не предоставлен, задача {name} не выполнена")
if log_results:
self._save_to_log(job_id, name, prompt, "Ошибка: AI-агент не предоставлен")
return ToolResult(
success=False,
error="AI-агент не предоставлен",
data=result_data
)
def _save_to_log(self, job_id: int, job_name: str, prompt: str, result: str):
"""Сохранить результат выполнения задачи в лог-файл."""
log_file = self.log_dir / f"cron_job_{job_id}_{job_name}.log"
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
log_entry = f"""
{'='*60}
[{timestamp}] Задача: {job_name} (ID: {job_id})
{'='*60}
Промпт:
{prompt}
Результат:
{result}
"""
with open(log_file, 'a', encoding='utf-8') as f:
f.write(log_entry)
logger.debug(f"Результат задачи {job_name} сохранён в лог: {log_file}")
async def execute(self, action: str = "list", ai_agent=None, user_id: int = None, **kwargs) -> ToolResult:
""" """
Выполнить действие с cron задачами. Выполнить действие с cron задачами.
Args: Args:
action: Действие - list, add, remove, toggle, run action: Действие - list, add, remove, toggle, run
ai_agent: Экземпляр AI-агента (для run)
user_id: ID пользователя (для add, run, list)
kwargs: Дополнительные аргументы kwargs: Дополнительные аргументы
""" """
actions = { actions = {
'list': self.list_jobs, 'list': lambda: self.list_jobs(user_id=user_id),
'add': lambda: self.add_job( 'add': lambda: self.add_job(
name=kwargs.get('name'), name=kwargs.get('name'),
command=kwargs.get('command'), prompt=kwargs.get('prompt'),
schedule=kwargs.get('schedule') schedule=kwargs.get('schedule'),
user_id=user_id,
notify=kwargs.get('notify', False),
log_results=kwargs.get('log_results', True)
), ),
'remove': lambda: self.remove_job(job_id=kwargs.get('job_id')), 'remove': lambda: self.remove_job(job_id=kwargs.get('job_id')),
'toggle': lambda: self.toggle_job( 'toggle': lambda: self.toggle_job(
job_id=kwargs.get('job_id'), job_id=kwargs.get('job_id'),
enabled=kwargs.get('enabled', True) enabled=kwargs.get('enabled', True)
), ),
'run': lambda: self.run_job(job_id=kwargs.get('job_id')) 'run': lambda: self.run_job(job_id=kwargs.get('job_id'), ai_agent=ai_agent, user_id=user_id)
} }
if action not in actions: if action not in actions: