chore: save working baseline before v3.0 architecture refactoring
This commit is contained in:
@@ -1,82 +1,297 @@
|
||||
"""
|
||||
===============================================================================
|
||||
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", "")
|
||||
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) -> List[Dict[str, Any]]:
|
||||
def db_get_tasks(user_id: int, status: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""Получает список всех задач пользователя."""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
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,))
|
||||
|
||||
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) -> Dict[str, Any]:
|
||||
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 (?, ?, ?, ?, 'BACKLOG', ?, ?)
|
||||
""", (new_task_id, module, title, priority.upper(), 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"Задача {new_task_id} создана"}
|
||||
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)
|
||||
if due_date:
|
||||
cursor.execute("""
|
||||
UPDATE tasks
|
||||
SET status = ?, due_date = ?
|
||||
WHERE (UPPER(task_id) = ? OR task_id LIKE ?) AND user_id = ?
|
||||
""", (status.upper(), due_date, formatted_id, f"%{task_id.strip()}", user_id))
|
||||
else:
|
||||
cursor.execute("""
|
||||
UPDATE tasks
|
||||
SET status = ?
|
||||
WHERE (UPPER(task_id) = ? OR task_id LIKE ?) AND user_id = ?
|
||||
""", (status.upper(), formatted_id, f"%{task_id.strip()}", user_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"Статус задачи {formatted_id} обновлен на {status.upper()}"}
|
||||
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 (UPPER(task_id) = ? OR task_id LIKE ?) AND user_id = ?
|
||||
""", (formatted_id, f"%{task_id.strip()}", user_id))
|
||||
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"Задача {formatted_id} удалена"}
|
||||
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}'"}
|
||||
Reference in New Issue
Block a user