""" =============================================================================== FILE: modules/web_api/llm/db/db_tasks.py PROJECT: SCUD Orion AI (Unified Architecture) MODULE: web_api / llm / db ROLE: Комплексное управление задачами, единый диспетчер db_tasks_edit и генерация структурированных отчетов в Markdown. =============================================================================== """ import uuid import logging import os import re from datetime import datetime from typing import List, Dict, Any, Optional from .connection import get_db_connection logger = logging.getLogger("DB_TASKS") def normalize_task_id(task_id_input: str) -> str: """Нормализует идентификатор задачи к формату TASK-XX.""" if not task_id_input: return "" clean_id = str(task_id_input).strip().upper().replace("TASK-", "").replace("TASK", "").replace("#", "") if clean_id.isdigit(): num = int(clean_id) return f"TASK-{(num):02d}" if num < 100 else f"TASK-{(num):03d}" return f"TASK-{clean_id}" def db_get_tasks(user_id: int, status: Optional[str] = None) -> List[Dict[str, Any]]: """Получает список всех задач пользователя.""" conn = get_db_connection() cursor = conn.cursor() if status and status.upper() != "ALL": target_status = status.upper() if target_status in ["PROGRESS", "В РАБОТЕ"]: target_status = "IN_PROGRESS" elif target_status in ["DONE", "ГОТОВО"]: target_status = "COMPLETED" elif target_status in ["PLANNED", "ПЛАНЫ"]: target_status = "BACKLOG" cursor.execute(""" SELECT id, task_id, module, title, priority, status, due_date, created_at FROM tasks WHERE user_id = ? AND (status = ? OR (status = 'BACKLOG' AND ? = 'PLANNED')) ORDER BY id DESC """, (user_id, target_status, target_status)) else: cursor.execute(""" SELECT id, task_id, module, title, priority, status, due_date, created_at FROM tasks WHERE user_id = ? ORDER BY id DESC """, (user_id,)) rows = cursor.fetchall() conn.close() return [dict(r) for r in rows] def db_add_task( user_id: int, module: str, title: str, priority: str = "MEDIUM", due_date: Optional[str] = None, status: str = "BACKLOG" ) -> Dict[str, Any]: """Добавление новой задачи со статусом по умолчанию BACKLOG (В планах).""" conn = get_db_connection() cursor = conn.cursor() cursor.execute("SELECT MAX(id) FROM tasks") max_id = cursor.fetchone()[0] or 0 new_task_id = f"TASK-{(max_id + 1):02d}" target_status = status.upper() if status else "BACKLOG" if target_status in ["PROGRESS", "В РАБОТЕ"]: target_status = "IN_PROGRESS" elif target_status in ["DONE", "ГОТОВО"]: target_status = "COMPLETED" elif target_status in ["PLANNED", "ПЛАНЫ", "BACKLOG"]: target_status = "BACKLOG" cursor.execute(""" INSERT INTO tasks (task_id, module, title, priority, status, due_date, user_id) VALUES (?, ?, ?, ?, ?, ?, ?) """, (new_task_id, module or "general", title.strip(), priority.upper(), target_status, due_date, user_id)) conn.commit() conn.close() return {"status": "success", "task_id": new_task_id, "message": f"Задача #{max_id + 1} создана и добавлена в планы"} def db_update_task_status(user_id: int, task_id: str, status: str = "COMPLETED", due_date: Optional[str] = None) -> Dict[str, Any]: """Быстрое обновление статуса задачи.""" return db_update_task_details(user_id=user_id, task_id=task_id, status=status, due_date=due_date) def db_update_task_details( user_id: int, task_id: str, title: Optional[str] = None, priority: Optional[str] = None, status: Optional[str] = None, due_date: Optional[str] = None ) -> Dict[str, Any]: """Комплексное обновление любых параметров задачи.""" conn = get_db_connection() cursor = conn.cursor() clean_num = re.sub(r'\D', '', str(task_id)) formatted_id = normalize_task_id(task_id) updates = [] params = [] if title is not None and title.strip(): updates.append("title = ?") params.append(title.strip()) if priority is not None and priority.strip(): updates.append("priority = ?") params.append(priority.strip().upper()) if status is not None and status.strip(): target_status = status.strip().upper() if target_status in ["PROGRESS", "В РАБОТЕ"]: target_status = "IN_PROGRESS" elif target_status in ["DONE", "ГОТОВО"]: target_status = "COMPLETED" elif target_status in ["PLANNED", "ПЛАНЫ"]: target_status = "BACKLOG" updates.append("status = ?") params.append(target_status) if due_date is not None: updates.append("due_date = ?") params.append(due_date.strip() if due_date.strip() else None) if not updates: conn.close() return {"status": "success", "message": "Нет данных для обновления"} params.extend([clean_num, formatted_id, f"%{task_id.strip()}", user_id]) sql = f""" UPDATE tasks SET {', '.join(updates)} WHERE (id = ? OR UPPER(task_id) = ? OR task_id LIKE ?) AND user_id = ? """ cursor.execute(sql, params) if cursor.rowcount == 0: conn.close() return {"error": f"Задача {task_id} не найдена или принадлежит другому пользователю"} conn.commit() conn.close() return {"status": "success", "message": f"Задача #{task_id} успешно обновлена"} def db_delete_task(user_id: int, task_id: str) -> Dict[str, Any]: """Удаление задачи.""" conn = get_db_connection() cursor = conn.cursor() clean_num = re.sub(r'\D', '', str(task_id)) formatted_id = normalize_task_id(task_id) cursor.execute(""" DELETE FROM tasks WHERE (id = ? OR UPPER(task_id) = ? OR task_id LIKE ?) AND user_id = ? """, (clean_num, formatted_id, f"%{task_id.strip()}", user_id)) if cursor.rowcount == 0: conn.close() return {"error": f"Задача {task_id} не найдена"} conn.commit() conn.close() return {"status": "success", "message": f"Задача #{task_id} удалена"} def db_export_tasks_markdown(user_id: int, filename: Optional[str] = None, status_filter: Optional[str] = None) -> Dict[str, Any]: """Генерирует Markdown-отчет задач с сохранением в output/web/tasks_export/{uuid}/.""" tasks = db_get_tasks(user_id) if not tasks: return {"status": "error", "message": "Список задач пуст, экспорт отменен"} # Фильтрация по статусу if status_filter and status_filter.upper() != "ALL": tgt = status_filter.upper() if tgt in ["COMPLETED", "DONE", "ВЫПОЛНЕННЫЕ"]: tasks = [t for t in tasks if str(t.get("status", "")).upper() in ["COMPLETED", "DONE"]] elif tgt in ["IN_PROGRESS", "PROGRESS", "В РАБОТЕ"]: tasks = [t for t in tasks if str(t.get("status", "")).upper() in ["IN_PROGRESS", "PROGRESS"]] elif tgt in ["BACKLOG", "PLANNED", "В ПЛАНАХ"]: tasks = [t for t in tasks if str(t.get("status", "")).upper() in ["BACKLOG", "PLANNED"]] if not tasks: return {"status": "error", "message": f"Нет задач с фильтром '{status_filter}' для экспорта"} # Имя файла target_filename = filename.strip() if (filename and filename.strip()) else "ROADMAP.md" if not target_filename.endswith(".md"): target_filename = f"{target_filename}.md" now_dt = datetime.now() now_str = now_dt.strftime("%Y-%m-%d %H:%M") modules: Dict[str, List[Dict[str, Any]]] = {} for t in tasks: mod = t.get("module") or "general" modules.setdefault(mod, []).append(t) md_lines = [ "# 🗺️ Дорожная карта задач проекта (ROADMAP)\n", f"> **Сформировано:** {now_str} | **Всего задач:** {len(tasks)}\n", "---\n" ] for mod_name, mod_tasks in sorted(modules.items()): md_lines.append(f"## Модуль `{mod_name}`\n") for t in sorted(mod_tasks, key=lambda x: x.get("id", 0)): status = str(t.get("status", "BACKLOG")).upper() is_done = status in ["COMPLETED", "DONE"] is_progress = status in ["IN_PROGRESS", "PROGRESS"] check_box = "[x]" if is_done else "[ ]" t_id = t.get("id") title = t.get("title", "Без названия") prio = t.get("priority", "MEDIUM") due = f" *(срок: {t['due_date']})*" if t.get("due_date") else "" status_tag = " `[В РАБОТЕ]`" if is_progress else (" `[ЗАВЕРШЕНО]`" if is_done else "") md_lines.append(f"- {check_box} **#{t_id}** [{prio}]{status_tag} {title}{due}") md_lines.append("\n---\n") content = "\n".join(md_lines) # Точный путь к корню scud_ai/output/web/tasks_export/ current_file_dir = os.path.dirname(os.path.abspath(__file__)) root_dir = os.path.abspath(os.path.join(current_file_dir, "../../../../")) tool_dir = os.path.join(root_dir, "output", "web", "tasks_export") os.makedirs(tool_dir, exist_ok=True) try: from routers.files import purge_old_tool_sessions purge_old_tool_sessions(tool_dir) except Exception: pass session_token = uuid.uuid4().hex[:8] session_dir = os.path.join(tool_dir, session_token) os.makedirs(session_dir, exist_ok=True) filepath = os.path.join(session_dir, target_filename) with open(filepath, "w", encoding="utf-8") as f: f.write(content) logger.info(f"Файл успешно создан: {filepath}") return { "status": "success", "filename": target_filename, "filepath": filepath, "download_url": f"/api/v1/files/download/tasks_export/{session_token}/{target_filename}", "tasks_count": len(tasks), "message": f"Отчет успешно сформирован в файл `{target_filename}` (всего задач: {len(tasks)})." } def db_tasks_edit( user_id: int, action: str, task_id: Optional[str] = None, title: Optional[str] = None, priority: Optional[str] = "MEDIUM", status: Optional[str] = None, module: Optional[str] = "general", due_date: Optional[str] = None, filename: Optional[str] = "ROADMAP.md" ) -> Dict[str, Any]: """Единый консолидированный диспетчер операций над задачами.""" act = action.strip().upper() if act == "ADD": if not title: return {"status": "error", "message": "Для создания задачи требуется указать title"} return db_add_task( user_id=user_id, module=module or "general", title=title, priority=priority or "MEDIUM", due_date=due_date, status=status or "BACKLOG" ) elif act == "UPDATE": if not task_id: return {"status": "error", "message": "Для обновления требуется указать task_id"} return db_update_task_details(user_id=user_id, task_id=str(task_id), title=title, priority=priority, status=status, due_date=due_date) elif act == "DELETE": if not task_id: return {"status": "error", "message": "Для удаления требуется указать task_id"} return db_delete_task(user_id=user_id, task_id=str(task_id)) elif act == "EXPORT": return db_export_tasks_markdown(user_id=user_id, filename=filename or "ROADMAP.md") return {"status": "error", "message": f"Неизвестное действие action='{action}'"}