refactor: step 2 - extract domain services (tasks, prompts, snapshots, knowledge)

This commit is contained in:
2026-08-21 10:03:12 +03:00
parent 304208760b
commit 16928de5bd
19 changed files with 1204 additions and 518 deletions
+101
View File
@@ -0,0 +1,101 @@
"""
===============================================================================
FILE: services/tasks/exporter.py
PROJECT: SCUD Orion AI (Unified Architecture)
MODULE: services / tasks
ROLE: Экспорт бэклога задач в форматированный Markdown файл (ROADMAP).
AI-CONTEXT-ANCHORS:
- ANCHOR[TASK_EXPORT_MARKDOWN]: Построение структуры Markdown с чекбоксами.
===============================================================================
"""
import os
import uuid
import logging
from datetime import datetime
from typing import Dict, Any, Optional, List
from config import OUTPUT_DIR
from .repository import repo_get_tasks
logger = logging.getLogger("TASK_EXPORTER")
# ANCHOR[TASK_EXPORT_MARKDOWN]
def export_tasks_to_markdown(user_id: int, filename: Optional[str] = None, status_filter: Optional[str] = None) -> Dict[str, Any]:
"""Генерирует Markdown-отчет задач с сохранением в output/web/tasks_export/{uuid}/."""
tasks = repo_get_tasks(user_id)
if not tasks:
return {"status": "error", "message": "Список задач пуст, экспорт отменен"}
# 1. Фильтрация задач по статусу
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_str = datetime.now().strftime("%Y-%m-%d %H:%M")
# 2. Группировка по модулям
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)
# 3. Сохранение файла в изолированную сессионную папку
tool_dir = os.path.join(OUTPUT_DIR, "web", "tasks_export")
os.makedirs(tool_dir, exist_ok=True)
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)
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)})."
}