702 lines
28 KiB
Python
702 lines
28 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Cron Tool - инструмент для управления интеллектуальными задачами.
|
||
|
||
Позволяет создавать, планировать и выполнять периодические задачи через AI-агент.
|
||
Задачи хранятся как промпты для ИИ, а не как команды.
|
||
"""
|
||
|
||
import logging
|
||
import sqlite3
|
||
import json
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
from typing import List, Dict, Any, Optional, Callable
|
||
from dataclasses import dataclass, field
|
||
|
||
from croniter import croniter
|
||
from bot.tools import BaseTool, ToolResult, register_tool
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
async def _translate_title(title: str, max_length: int = 100) -> str:
|
||
"""
|
||
Перевести заголовок на русский через Qwen.
|
||
|
||
Args:
|
||
title: Заголовок для перевода
|
||
max_length: Максимальная длина
|
||
|
||
Returns:
|
||
Переведённый заголовок
|
||
"""
|
||
try:
|
||
import subprocess
|
||
import json
|
||
|
||
# Создаём временный промпт для перевода
|
||
translate_prompt = f"Translate this news title to Russian. Keep it concise, natural, and informative. Maximum {max_length} characters. Return ONLY the translation, no quotes or explanations.\n\nTitle: {title[:200]}"
|
||
|
||
# Используем qwen-cli если доступен
|
||
result = subprocess.run(
|
||
['qwen', 'chat', '--json', '--prompt', translate_prompt],
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=15
|
||
)
|
||
|
||
if result.returncode == 0:
|
||
# Парсим JSON ответ
|
||
try:
|
||
response = json.loads(result.stdout)
|
||
translated = response.get('content', response.get('response', title))
|
||
except json.JSONDecodeError:
|
||
translated = result.stdout.strip()
|
||
|
||
# Очищаем от кавычек
|
||
translated = translated.strip('"\'')
|
||
return translated[:max_length]
|
||
except Exception as e:
|
||
logger.debug(f"Ошибка перевода заголовка: {e}")
|
||
|
||
# Fallback - обрезаем оригинал
|
||
return title[:max_length]
|
||
|
||
|
||
@dataclass
|
||
class CronJob:
|
||
"""
|
||
Интеллектуальная задача 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]
|
||
name: str
|
||
prompt: str # Промпт для ИИ вместо команды
|
||
schedule: str
|
||
user_id: Optional[int] = None # ID пользователя Telegram
|
||
enabled: bool = True
|
||
notify: bool = False # Уведомлять пользователя в Telegram
|
||
log_results: bool = True # Сохранять в лог
|
||
last_run: Optional[datetime] = None
|
||
next_run: Optional[datetime] = None
|
||
created_at: datetime = field(default_factory=datetime.now)
|
||
|
||
|
||
class CronTool(BaseTool):
|
||
"""Инструмент для управления интеллектуальными задачами пользователя."""
|
||
|
||
name = "cron_tool"
|
||
description = "Управление периодическими задачами через AI-агент. Создание, планирование и выполнение задач по расписанию."
|
||
category = "automation"
|
||
|
||
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.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._init_db()
|
||
|
||
def _init_db(self):
|
||
"""Инициализировать БД."""
|
||
conn = sqlite3.connect(self.db_path)
|
||
c = conn.cursor()
|
||
|
||
# Создаём таблицу со всеми колонками
|
||
c.execute('''
|
||
CREATE TABLE IF NOT EXISTS cron_jobs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
prompt TEXT NOT NULL,
|
||
schedule TEXT NOT NULL,
|
||
user_id INTEGER,
|
||
enabled INTEGER DEFAULT 1,
|
||
notify INTEGER DEFAULT 0,
|
||
log_results INTEGER DEFAULT 1,
|
||
last_run DATETIME,
|
||
next_run DATETIME,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
''')
|
||
|
||
# Проверяем наличие всех колонок (для обратной совместимости)
|
||
c.execute("PRAGMA table_info(cron_jobs)")
|
||
columns = [col[1] for col in c.fetchall()]
|
||
|
||
# Миграции для старых БД
|
||
migrations = {
|
||
'prompt': 'ALTER TABLE cron_jobs ADD COLUMN prompt TEXT DEFAULT ""',
|
||
'user_id': 'ALTER TABLE cron_jobs ADD COLUMN user_id INTEGER',
|
||
'enabled': 'ALTER TABLE cron_jobs ADD COLUMN enabled INTEGER DEFAULT 1',
|
||
'notify': 'ALTER TABLE cron_jobs ADD COLUMN notify INTEGER DEFAULT 0',
|
||
'log_results': 'ALTER TABLE cron_jobs ADD COLUMN log_results INTEGER DEFAULT 1',
|
||
'last_run': 'ALTER TABLE cron_jobs ADD COLUMN last_run DATETIME',
|
||
'next_run': 'ALTER TABLE cron_jobs ADD COLUMN next_run DATETIME'
|
||
}
|
||
|
||
for col_name, alter_query in migrations.items():
|
||
if col_name not in columns:
|
||
logger.info(f"Добавление колонки {col_name} в таблицу cron_jobs")
|
||
try:
|
||
c.execute(alter_query)
|
||
except sqlite3.OperationalError as e:
|
||
# Игнорируем ошибку если колонка уже существует (race condition)
|
||
if "duplicate column" not in str(e).lower():
|
||
raise
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
def _parse_schedule(self, schedule: str, base_time: datetime = None) -> Optional[datetime]:
|
||
"""
|
||
Распарсить расписание и вернуть следующее время выполнения.
|
||
|
||
Поддерживает полноценный cron-синтаксис через croniter:
|
||
- "*/5 * * * *" - каждые 5 минут
|
||
- "0 * * * *" - каждый час в 0 минут
|
||
- "0 5 * * *" - каждый день в 05:00
|
||
- "0 0 1 * *" - каждый месяц 1 числа в 00:00
|
||
- "0 0 * * 0" - каждое воскресенье в 00:00
|
||
- "@hourly", "@daily", "@weekly", "@monthly", "@yearly"
|
||
|
||
Args:
|
||
schedule: Cron-выражение или special string
|
||
base_time: Базовое время для расчёта (по умолчанию сейчас)
|
||
|
||
Returns:
|
||
Следующее время выполнения или None если ошибка парсинга
|
||
"""
|
||
if base_time is None:
|
||
base_time = datetime.now()
|
||
|
||
# Поддержка special strings
|
||
special_schedules = {
|
||
'@hourly': '0 * * * *',
|
||
'@daily': '0 0 * * *',
|
||
'@midnight': '0 0 * * *',
|
||
'@weekly': '0 0 * * 0',
|
||
'@monthly': '0 0 1 * *',
|
||
'@yearly': '0 0 1 1 *',
|
||
'@annually': '0 0 1 1 *'
|
||
}
|
||
|
||
cron_expr = special_schedules.get(schedule.lower(), schedule)
|
||
|
||
try:
|
||
# croniter возвращает следующее время выполнения
|
||
cron = croniter(cron_expr, base_time)
|
||
next_run = cron.get_next(datetime)
|
||
return next_run
|
||
except Exception as e:
|
||
logger.error(f"Ошибка парсинга cron-расписания '{schedule}': {e}")
|
||
return None
|
||
|
||
def _calculate_next_run(self, schedule: str, last_run: datetime = None) -> Optional[datetime]:
|
||
"""
|
||
Рассчитать следующее время выполнения на основе last_run.
|
||
|
||
Args:
|
||
schedule: Cron-выражение
|
||
last_run: Время последнего выполнения (по умолчанию сейчас)
|
||
|
||
Returns:
|
||
Следующее время выполнения
|
||
"""
|
||
base_time = last_run if last_run else datetime.now()
|
||
return self._parse_schedule(schedule, base_time)
|
||
|
||
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)
|
||
c = conn.cursor()
|
||
|
||
try:
|
||
next_run = self._calculate_next_run(schedule)
|
||
next_run_str = next_run.strftime('%Y-%m-%d %H:%M:%S') if next_run else None
|
||
|
||
c.execute('''
|
||
INSERT INTO cron_jobs (name, prompt, schedule, user_id, notify, log_results, next_run)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||
''', (name, prompt, schedule, user_id, 1 if notify else 0, 1 if log_results else 0, next_run_str))
|
||
|
||
job_id = c.lastrowid
|
||
conn.commit()
|
||
|
||
self._jobs[job_id] = CronJob(
|
||
id=job_id,
|
||
name=name,
|
||
prompt=prompt,
|
||
schedule=schedule,
|
||
user_id=user_id,
|
||
notify=notify,
|
||
log_results=log_results,
|
||
next_run=next_run
|
||
)
|
||
|
||
return ToolResult(
|
||
success=True,
|
||
data={'id': job_id, 'name': name, 'prompt': prompt, 'schedule': schedule, 'user_id': user_id, 'next_run': next_run_str},
|
||
metadata={'status': 'added', 'notify': notify, 'log_results': log_results}
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.exception(f"Ошибка добавления задачи: {e}")
|
||
return ToolResult(
|
||
success=False,
|
||
error=str(e)
|
||
)
|
||
finally:
|
||
conn.close()
|
||
|
||
async def update_next_run(self, job_id: int) -> ToolResult:
|
||
"""
|
||
Пересчитать время следующего выполнения после успешного запуска.
|
||
|
||
Args:
|
||
job_id: ID задачи
|
||
|
||
Returns:
|
||
ToolResult с новым next_run
|
||
"""
|
||
conn = sqlite3.connect(self.db_path)
|
||
c = conn.cursor()
|
||
|
||
try:
|
||
# Получаем текущие данные задачи
|
||
c.execute("SELECT schedule, last_run FROM cron_jobs WHERE id = ?", (job_id,))
|
||
row = c.fetchone()
|
||
|
||
if not row:
|
||
return ToolResult(
|
||
success=False,
|
||
error=f"Задача не найдена: {job_id}"
|
||
)
|
||
|
||
schedule, last_run_str = row
|
||
|
||
# Рассчитываем следующее время выполнения на основе last_run
|
||
last_run = datetime.strptime(last_run_str, '%Y-%m-%d %H:%M:%S') if last_run_str else datetime.now()
|
||
next_run = self._calculate_next_run(schedule, last_run)
|
||
|
||
if not next_run:
|
||
return ToolResult(
|
||
success=False,
|
||
error=f"Не удалось рассчитать next_run для расписания '{schedule}'"
|
||
)
|
||
|
||
next_run_str = next_run.strftime('%Y-%m-%d %H:%M:%S')
|
||
|
||
# Обновляем next_run в БД
|
||
c.execute("UPDATE cron_jobs SET next_run = ? WHERE id = ?", (next_run_str, job_id))
|
||
conn.commit()
|
||
|
||
return ToolResult(
|
||
success=True,
|
||
data={'id': job_id, 'next_run': next_run_str},
|
||
metadata={'status': 'next_run_updated'}
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.exception(f"Ошибка обновления next_run: {e}")
|
||
return ToolResult(
|
||
success=False,
|
||
error=str(e)
|
||
)
|
||
finally:
|
||
conn.close()
|
||
|
||
async def list_jobs(self, user_id: int = None) -> ToolResult:
|
||
"""
|
||
Получить список всех задач.
|
||
|
||
Args:
|
||
user_id: ID пользователя для фильтрации (если None - все задачи)
|
||
"""
|
||
conn = sqlite3.connect(self.db_path)
|
||
c = conn.cursor()
|
||
|
||
if user_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()
|
||
conn.close()
|
||
|
||
jobs = []
|
||
for row in rows:
|
||
jobs.append({
|
||
'id': row[0],
|
||
'name': row[1],
|
||
'prompt': row[2],
|
||
'schedule': row[3],
|
||
'user_id': row[4],
|
||
'enabled': bool(row[5]),
|
||
'notify': bool(row[6]),
|
||
'log_results': bool(row[7]),
|
||
'last_run': row[8],
|
||
'next_run': row[9],
|
||
'created_at': row[10]
|
||
})
|
||
|
||
return ToolResult(
|
||
success=True,
|
||
data=jobs,
|
||
metadata={'count': len(jobs)}
|
||
)
|
||
|
||
async def remove_job(self, job_id: int) -> ToolResult:
|
||
"""Удалить задачу."""
|
||
conn = sqlite3.connect(self.db_path)
|
||
c = conn.cursor()
|
||
c.execute("DELETE FROM cron_jobs WHERE id = ?", (job_id,))
|
||
if c.rowcount == 0:
|
||
conn.close()
|
||
return ToolResult(
|
||
success=False,
|
||
error=f"Задача не найдена: {job_id}"
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
if job_id in self._jobs:
|
||
del self._jobs[job_id]
|
||
|
||
return ToolResult(
|
||
success=True,
|
||
data={'id': job_id},
|
||
metadata={'status': 'removed'}
|
||
)
|
||
|
||
async def toggle_job(self, job_id: int, enabled: bool) -> ToolResult:
|
||
"""Включить/выключить задачу."""
|
||
conn = sqlite3.connect(self.db_path)
|
||
c = conn.cursor()
|
||
c.execute("UPDATE cron_jobs SET enabled = ? WHERE id = ?", (1 if enabled else 0, job_id))
|
||
if c.rowcount == 0:
|
||
conn.close()
|
||
return ToolResult(
|
||
success=False,
|
||
error=f"Задача не найдена: {job_id}"
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
return ToolResult(
|
||
success=True,
|
||
data={'id': job_id, 'enabled': enabled},
|
||
metadata={'status': 'toggled'}
|
||
)
|
||
|
||
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)
|
||
c = conn.cursor()
|
||
c.execute("SELECT name, prompt, notify, log_results FROM cron_jobs WHERE id = ?", (job_id,))
|
||
row = c.fetchone()
|
||
|
||
if not row:
|
||
conn.close()
|
||
return ToolResult(
|
||
success=False,
|
||
error=f"Задача не найдена: {job_id}"
|
||
)
|
||
|
||
name, prompt, notify, log_results = row
|
||
conn.close()
|
||
|
||
logger.info(f"🕐 Выполнение задачи #{job_id}: {name}")
|
||
logger.info(f" Промпт: {prompt}")
|
||
|
||
result_data = {
|
||
'id': job_id,
|
||
'name': name,
|
||
'prompt': prompt,
|
||
'executed_at': datetime.now().isoformat()
|
||
}
|
||
|
||
# Выполняем задачу через AI-агент
|
||
if ai_agent:
|
||
try:
|
||
# Отправляем промпт ИИ-агенту
|
||
logger.info(f"🤖 Отправка промпта AI-агенту для задачи {name}")
|
||
|
||
# ИИ-агент анализирует промпт и решает какой инструмент использовать
|
||
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\n"
|
||
|
||
# Форматируем результат инструмента в читаемый вид
|
||
if tool_result.success:
|
||
formatted_result = await self._format_tool_result_for_cron(
|
||
decision.tool_name, tool_result.data, tool_result.error
|
||
)
|
||
result_text += formatted_result
|
||
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
|
||
)
|
||
|
||
async def _format_tool_result_for_cron(self, tool_name: str, data: Any, error: str = None) -> str:
|
||
"""
|
||
Отформатировать результат выполнения инструмента в читаемый вид.
|
||
|
||
Args:
|
||
tool_name: Название инструмента
|
||
data: Данные результата
|
||
error: Ошибка (если есть)
|
||
|
||
Returns:
|
||
Отформатированная строка с результатом
|
||
"""
|
||
# Поддерживаем оба имени: 'rss_reader' (старое) и 'rss_tool' (новое)
|
||
if tool_name in ('rss_reader', 'rss_tool'):
|
||
if not data:
|
||
return "📰 Новостей не найдено."
|
||
|
||
output = "📰 **Последние новости:**\n\n"
|
||
# Берём не более 15 новостей для читаемости
|
||
news_count = min(len(data), 15)
|
||
|
||
for i in range(news_count):
|
||
item = data[i]
|
||
title = item.get('title', 'Без названия')
|
||
pub_date = item.get('pub_date', '')
|
||
link = item.get('link', '')
|
||
|
||
# Переводим заголовок на русский
|
||
translated_title = await _translate_title(title, max_length=100)
|
||
|
||
# Форматируем дату
|
||
date_str = ""
|
||
if pub_date:
|
||
try:
|
||
dt = datetime.strptime(pub_date[:19], '%Y-%m-%d %H:%M:%S')
|
||
date_str = dt.strftime('%d.%m.%Y %H:%M')
|
||
except:
|
||
date_str = pub_date[:16]
|
||
|
||
# Обрезаем заголовок если слишком длинный
|
||
if len(translated_title) > 120:
|
||
translated_title = translated_title[:117] + "..."
|
||
|
||
output += f"**{i+1}. {translated_title}**\n"
|
||
if date_str:
|
||
output += f" 📅 {date_str}\n"
|
||
if link:
|
||
short_link = link[:60] + "..." if len(link) > 63 else link
|
||
output += f" 🔗 {short_link}\n"
|
||
output += "\n"
|
||
|
||
return output
|
||
|
||
elif tool_name == 'ddgs_search':
|
||
if not data:
|
||
return "🔍 Ничего не найдено по вашему запросу."
|
||
|
||
output = "🔍 **Результаты поиска:**\n\n"
|
||
for i, item in enumerate(data[:5], 1):
|
||
title = item.get('title', 'Без названия')
|
||
href = item.get('href', '')
|
||
body = item.get('body', '')[:200]
|
||
output += f"{i}. **{title}**\n"
|
||
if href:
|
||
output += f" {href}\n"
|
||
if body:
|
||
output += f" {body}\n\n"
|
||
|
||
return output
|
||
|
||
elif tool_name == 'ssh_executor':
|
||
if not data:
|
||
return "❌ **Ошибка SSH:** Нет данных"
|
||
|
||
output = "🖥️ **SSH результат:**\n"
|
||
|
||
if isinstance(data, dict):
|
||
if data.get('stdout'):
|
||
output += f"**Вывод:**\n```\n{data['stdout']}\n```\n\n"
|
||
if data.get('stderr'):
|
||
output += f"**Ошибки:**\n```\n{data['stderr']}\n```\n\n"
|
||
if data.get('returncode') == 0:
|
||
output += "✅ **Успешно**"
|
||
else:
|
||
output += f"❌ **Код возврата:** {data.get('returncode', 'N/A')}"
|
||
else:
|
||
output += str(data)
|
||
|
||
return output
|
||
|
||
elif tool_name == 'cron_tool':
|
||
if isinstance(data, dict):
|
||
return f"✅ **Результат:**\n{data}"
|
||
return str(data)
|
||
|
||
# Fallback для неизвестных инструментов
|
||
return str(data) if data else "Выполнено"
|
||
|
||
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 задачами.
|
||
|
||
Args:
|
||
action: Действие - list, add, remove, toggle, run
|
||
ai_agent: Экземпляр AI-агента (для run)
|
||
user_id: ID пользователя (для add, run, list)
|
||
kwargs: Дополнительные аргументы
|
||
"""
|
||
actions = {
|
||
'list': lambda: self.list_jobs(user_id=user_id),
|
||
'add': lambda: self.add_job(
|
||
name=kwargs.get('name'),
|
||
prompt=kwargs.get('prompt'),
|
||
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')),
|
||
'toggle': lambda: self.toggle_job(
|
||
job_id=kwargs.get('job_id'),
|
||
enabled=kwargs.get('enabled', True)
|
||
),
|
||
'run': lambda: self.run_job(job_id=kwargs.get('job_id'), ai_agent=ai_agent, user_id=user_id)
|
||
}
|
||
|
||
if action not in actions:
|
||
return ToolResult(
|
||
success=False,
|
||
error=f"Неизвестное действие: {action}. Доступные: {list(actions.keys())}"
|
||
)
|
||
|
||
logger.info(f"Cron действие: {action} с аргументами: {kwargs}")
|
||
return await actions[action]()
|
||
|
||
|
||
# Автоматическая регистрация при импорте
|
||
@register_tool
|
||
class CronToolAuto(CronTool):
|
||
"""Авто-регистрируемая версия CronTool."""
|
||
pass
|