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:
parent
bff74741a6
commit
f559c83baa
|
|
@ -22,6 +22,7 @@ bot_config.json
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
cron_logs/
|
||||||
|
|
||||||
# Cache
|
# Cache
|
||||||
.cache/
|
.cache/
|
||||||
|
|
|
||||||
|
|
@ -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
63
bot.py
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue