diff --git a/max_bot/app/config.py b/max_bot/app/config.py index 063b28f..52010cc 100644 --- a/max_bot/app/config.py +++ b/max_bot/app/config.py @@ -1,3 +1,12 @@ +""" +Конфигурация max_bot (Pydantic Settings). + +Читает из .env с префиксом BOT_: + - database_url: PostgreSQL URL + - max_token: токен MAX Platform + - webhook_url/webhook_secret: настройки webhook + - config_php_path: путь к config.php сайта +""" from typing import List, Optional from pydantic_settings import BaseSettings diff --git a/max_bot/app/config_reader.py b/max_bot/app/config_reader.py index 28f71e0..9fcc04a 100644 --- a/max_bot/app/config_reader.py +++ b/max_bot/app/config_reader.py @@ -1,3 +1,9 @@ +""" +Чтение конфигурации из config.php (AegisOne сайт). + +Парсит PHP define() константы и возвращает их как dict. +Fallback на hardcoded значения если файл не найден. +""" from __future__ import annotations import re import os diff --git a/max_bot/app/database.py b/max_bot/app/database.py index b157a4f..5103b75 100644 --- a/max_bot/app/database.py +++ b/max_bot/app/database.py @@ -1,3 +1,9 @@ +""" +Асинхронное подключение к PostgreSQL через SQLAlchemy 2.0. + +engine: create_async_engine с pool_size=10 +async_session: async_sessionmaker для создания сессий +""" from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession from app.config import settings diff --git a/max_bot/app/email_sender.py b/max_bot/app/email_sender.py index 9247764..88856c5 100644 --- a/max_bot/app/email_sender.py +++ b/max_bot/app/email_sender.py @@ -1,3 +1,8 @@ +""" +Отправка email через sendmail (с fallback на логирование в файл). + +Используется для уведомлений о заявках. +""" import subprocess import os import logging diff --git a/max_bot/app/handlers/consent.py b/max_bot/app/handlers/consent.py index b119937..09ff9fd 100644 --- a/max_bot/app/handlers/consent.py +++ b/max_bot/app/handlers/consent.py @@ -1,3 +1,11 @@ +""" +Обработчик согласия на обработку персональных данных. + +Управляет жизненным циклом согласия: + - запрос согласия → да/нет + - отзыв согласия (consent_revoked_date) + - повторное согласие после отказа +""" import datetime import re import logging @@ -12,6 +20,12 @@ logger = logging.getLogger(__name__) def _matches_any(text: str, words: list) -> bool: + """ + Проверяет, содержит ли текст хотя бы одно из указанных слов. + + Использует split для точного совпадения коротких слов (≤3 символов) + и подстроку для длинных (>3 символов). + """ text_words = set(text.split()) for w in words: if w in text_words: @@ -22,6 +36,7 @@ def _matches_any(text: str, words: list) -> bool: async def handle_consent_callback(user_id: int, conv_id: int, callback_id: str) -> None: + """Маршрутизация callback-кнопок согласия (да/нет).""" if callback_id == "consent_yes": await handle_consent_yes(user_id, conv_id) elif callback_id == "consent_no": @@ -29,6 +44,7 @@ async def handle_consent_callback(user_id: int, conv_id: int, callback_id: str) async def handle_consent_response(user_id: int, conv_id: int, text: str) -> None: + """Обработка текстового ответа на запрос согласия (да/нет/другое).""" text_lower = text.strip().lower() consent_words = ["да", "даю", "согласен", "согласна", "yes", "ok", "хорошо", "даю согласие"] refuse_words = ["нет", "не даю", "отказ", "no", "не согласен", "не согласна"] @@ -48,6 +64,7 @@ async def handle_consent_response(user_id: int, conv_id: int, text: str) -> None async def handle_consent_yes(user_id: int, conv_id: int) -> None: + """Обработка согласия: устанавливает consent_given=True, переводит в awaiting_contact.""" async with async_session() as db: result = await db.execute(select(BotUser).where(BotUser.id == user_id)) user = result.scalar_one_or_none() @@ -78,6 +95,7 @@ async def handle_consent_yes(user_id: int, conv_id: int) -> None: async def handle_consent_no(user_id: int, conv_id: int) -> None: + """Обработка отказа: устанавливает consent_refused, показывает контакты.""" async with async_session() as db: result = await db.execute( select(BotConversation).where(BotConversation.id == conv_id) @@ -106,6 +124,7 @@ async def handle_consent_no(user_id: int, conv_id: int) -> None: async def handle_refused_again(user_id: int, conv_id: int, text: str) -> None: + """Повторная обработка после отказа: позволяет дать согласие или показывает контакты.""" text_lower = text.strip().lower() consent_words = ["да", "даю", "согласен", "согласна", "yes", "ok", "хорошо", "даю согласие", "передумал", "согласен дать"] refuse_words = ["нет", "не даю", "не хочу", "отказ", "отмена", "no", "cancel", "я отказался"] @@ -145,6 +164,7 @@ async def handle_refused_again(user_id: int, conv_id: int, text: str) -> None: async def handle_revoke_consent(user_id: int, conv_id: int) -> None: + """Отзыв согласия: устанавливает consent_given=False, consent_revoked_date=now.""" async with async_session() as db: user = await db.get(BotUser, user_id) if user and user.consent_given: diff --git a/max_bot/app/handlers/contact.py b/max_bot/app/handlers/contact.py index a9738ef..2d32282 100644 --- a/max_bot/app/handlers/contact.py +++ b/max_bot/app/handlers/contact.py @@ -1,3 +1,10 @@ +""" +Обработчик контактов пользователя. + +Принимает контакты (телефон, email) через: + - sharing контакта из мессенджера (VCF) + - ручной ввод текстом +""" import datetime import re import logging @@ -13,6 +20,7 @@ EMAIL_PATTERN = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") async def handle_contact_received(user_id: int, conv_id: int, contact_text: str) -> None: + """Обработка контакта из мессенджера: парсинг телефона/email, переход к заявке.""" async with async_session() as db: result = await db.execute(select(BotUser).where(BotUser.id == user_id)) user = result.scalar_one_or_none() @@ -37,6 +45,7 @@ async def handle_contact_received(user_id: int, conv_id: int, contact_text: str) async def handle_manual_contact(user_id: int, conv_id: int, text: str) -> None: + """Обработка ручного ввода контакта: валидация телефона/email, отказ → consent_refused.""" text = text.strip() text_lower = text.lower() diff --git a/max_bot/app/handlers/greeting.py b/max_bot/app/handlers/greeting.py index 61ea484..6baccb3 100644 --- a/max_bot/app/handlers/greeting.py +++ b/max_bot/app/handlers/greeting.py @@ -1,3 +1,13 @@ +""" +Обработчик приветствий и основных сообщений бота. + +Управляет жизненным циклом диалога: + - приветствие → создание пользователя и диалога + - маршрутизация по состояниям (state machine) + - анализ намерений через YandexGPT + - быстрые паттерны (заявка/приветствие) до NLP + - контекст из последних 10 минут +""" import logging import datetime from typing import Optional @@ -7,6 +17,7 @@ from app.database import async_session MOSCOW_TZ = datetime.timezone(datetime.timedelta(hours=3)) def moscow_now(): + """Возвращает текущее время в UTC+3 (Москва) без tzinfo.""" return datetime.datetime.now(MOSCOW_TZ).replace(tzinfo=None) from app.models import BotUser, BotConversation, BotMessage, BotKnowledgeBase, BotCategory, BotUnknownQuestion from app.max_api import max_api @@ -26,6 +37,7 @@ async def handle_greeting( last_name: str = "", username: str = "", ) -> None: + """Обработка приветствия: создаёт/обновляет пользователя, создаёт диалог, отправляет приветствие.""" assistant_name = await settings_cache.get("assistant_name", "София") greeting_text = ( @@ -94,6 +106,14 @@ async def handle_greeting( async def handle_message(user_id: int, text: str) -> None: + """ + Основной обработчик сообщений. Маршрутизирует по состояниям: + - greeting/analyzing/awaiting_input → analyze_and_respond + - awaiting_consent/consent_refused → обработка согласия + - awaiting_contact → ручной ввод контакта + - awaiting_inquiry → обработка заявки + - completed/escalated → повторный анализ (escape hatch) + """ async with async_session() as db: result = await db.execute( select(BotConversation) @@ -199,6 +219,7 @@ async def handle_message(user_id: int, text: str) -> None: async def _get_user_history_context(user_id: int) -> str: + """Формирует контекст из последних сообщений (10 мин) и открытых заявок пользователя.""" from app.models import BotTicket async with async_session() as db: ten_min_ago = moscow_now() - datetime.timedelta(minutes=10) @@ -237,12 +258,20 @@ async def _get_user_history_context(user_id: int) -> str: async def _select_ai(): + """Выбор AI-провайдера (только YandexGPT).""" if await yandex_gpt.is_available(): return yandex_gpt return None async def analyze_and_respond(user_id: int, conv_id: int, text: str) -> None: + """ + Анализ намерений и генерация ответа. + + Быстрые паттерны (заявка/приветствие) проверяются ПЕРЕД NLP. + Затем: analyze_intent → ticket/question/unknown → обработка. + clarify_intent вызывается не более 1 раза за диалог. + """ try: logger.info(f"analyze_and_respond: user={user_id} conv={conv_id} text={text[:80]}") @@ -359,6 +388,7 @@ async def analyze_and_respond(user_id: int, conv_id: int, text: str) -> None: async def _limited_mode(user_id: int, conv_id: int) -> None: + """Режим без AI: показывает контакты компании.""" async with async_session() as db: result = await db.execute( select(BotConversation).where(BotConversation.id == conv_id) @@ -372,6 +402,7 @@ async def _limited_mode(user_id: int, conv_id: int) -> None: async def _send_contacts(user_id: int, conv_id: int = None) -> None: + """Отправка контактных данных компании (телефон, email).""" phones = config_reader.get_phones() email = config_reader.get_site_mail() @@ -387,6 +418,7 @@ async def _send_contacts(user_id: int, conv_id: int = None) -> None: async def _get_knowledge_base_context(query: str) -> str: + """Формирует контекст из базы знаний по ключевым словам (ранжирование по score).""" async with async_session() as db: result = await db.execute( select(BotKnowledgeBase) @@ -433,6 +465,7 @@ async def _get_knowledge_base_context(query: str) -> str: def _texts_match(text_a: str, text_b: str) -> bool: + """Проверка схожести двух текстов по пересечению слов (≥50%).""" stopwords = {"какой", "какая", "какие", "какое", "ваш", "ваша", "ваши", "где", "когда", "сколько", "что", "это", "или", "для", "при", "подскажите", "расскажите", "объясните", "пожалуйста"} @@ -445,6 +478,7 @@ def _texts_match(text_a: str, text_b: str) -> bool: async def _auto_generate_and_add_kb(db, question_record): + """Автогенерация карточки KB через YandexGPT при 3+ обращениях.""" card_data = await yandex_gpt.generate_kb_card([question_record.question_text]) if not card_data: return @@ -466,6 +500,7 @@ async def _auto_generate_and_add_kb(db, question_record): async def _save_unknown_question(user_id: int, question_text: str) -> None: + """Сохранение неизвестного вопроса с дедупликацией и автогенерацией при 3+ обращениях.""" normalized_input = question_text.lower().strip() async with async_session() as db: diff --git a/max_bot/app/handlers/handoff.py b/max_bot/app/handlers/handoff.py index e3d29ee..812b4bb 100644 --- a/max_bot/app/handlers/handoff.py +++ b/max_bot/app/handlers/handoff.py @@ -1,3 +1,8 @@ +""" +Обработчик callback-кнопок и шаринга контактов. + +Маршрутизирует callback-и по типу (consent_* и др.). +""" import datetime import logging from sqlalchemy import select @@ -10,6 +15,7 @@ logger = logging.getLogger(__name__) async def handle_callback(user_id: int, callback_id: str, callback_data: dict) -> None: + """Маршрутизация callback-кнопок (consent_* → consent handler).""" async with async_session() as db: result = await db.execute( select(BotConversation) @@ -40,6 +46,7 @@ async def handle_contact_share( vcf_data: dict = None, contact_hash: str = "", ) -> None: + """Обработка шаринга контакта из мессенджера: парсинг VCF, сохранение в BotUser.""" if vcf_data is None: vcf_data = {} async with async_session() as db: diff --git a/max_bot/app/handlers/inquiry.py b/max_bot/app/handlers/inquiry.py index 19c7f5e..fd42c86 100644 --- a/max_bot/app/handlers/inquiry.py +++ b/max_bot/app/handlers/inquiry.py @@ -1,3 +1,9 @@ +""" +Обработчик заявок (inquiry). + +Создаёт BotTicket с контактными данными пользователя, +отправляет уведомление на email, подтверждает пользователю. +""" import datetime import logging from sqlalchemy import select @@ -12,10 +18,12 @@ logger = logging.getLogger(__name__) MOSCOW_TZ = datetime.timezone(datetime.timedelta(hours=3)) def moscow_now(): + """Возвращает текущее время в UTC+3 (Москва) без tzinfo.""" return datetime.datetime.now(MOSCOW_TZ).replace(tzinfo=None) async def handle_inquiry(user_id: int, conv_id: int, text: str) -> None: + """Обработка заявки: создаёт.ticket, отправляет email, подтверждает пользователю.""" async with async_session() as db: result = await db.execute( select(BotConversation).where(BotConversation.id == conv_id) diff --git a/max_bot/app/keyboards.py b/max_bot/app/keyboards.py index f3881b2..9876b9d 100644 --- a/max_bot/app/keyboards.py +++ b/max_bot/app/keyboards.py @@ -1,3 +1,12 @@ +""" +Клавиатуры для MAX Platform бота. + +Типы кнопок: + - callback: callback-кнопка (consent_yes/consent_no) + - link: ссылка (политика конфиденциальности) + - request_contact: запрос контакта из мессенджера + - message: текстовая кнопка +""" from typing import List, Dict, Any POLITICA_URL = "https://aegisone.ru/politica.php" diff --git a/max_bot/app/main.py b/max_bot/app/main.py index fa12555..a6dd7f1 100644 --- a/max_bot/app/main.py +++ b/max_bot/app/main.py @@ -1,3 +1,11 @@ +""" +Главный модуль max_bot FastAPI-приложения. + +Эндпоинты: + - /webhook: входящие webhook-и от MAX Platform + - /health: healthcheck + - /api/bot/*: CRUD для настроек, KB, заявок, диалогов, пользователей +""" from __future__ import annotations import logging from contextlib import asynccontextmanager @@ -54,6 +62,7 @@ app.add_middleware( async def _apply_migrations(): + """Автоматические миграции: добавление колонок/таблиц при старте.""" async with engine.begin() as conn: try: await conn.execute(text(""" @@ -101,6 +110,7 @@ async def _apply_migrations(): async def _add_column_if_not_exists(conn, table: str, column: str, col_type: str): + """Безопасное добавление колонки (idempotent).""" from sqlalchemy import text as sa_text _validate_sql_name(table) _validate_sql_name(column) @@ -121,6 +131,7 @@ async def _add_column_if_not_exists(conn, table: str, column: str, col_type: str async def _create_table_if_not_exists(conn, table: str, create_sql: str): + """Безопасное создание таблицы (idempotent).""" from sqlalchemy import text as sa_text try: await conn.execute(sa_text(f""" @@ -138,12 +149,14 @@ async def _create_table_if_not_exists(conn, table: str, create_sql: str): def _validate_sql_name(name: str) -> None: + """Валидация SQL-идентификатора (только буквы, цифры, подчёркивания).""" import re if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_ ]*$', name): raise ValueError(f"Invalid SQL identifier: {name}") def _validate_sql_type(col_type: str) -> None: + """Валидация SQL-типа колонки (разрешённые типы).""" import re allowed = r'^(VARCHAR\(\d+\)|TEXT|INTEGER( DEFAULT \d+)?|BOOLEAN( DEFAULT (TRUE|FALSE))?|TIMESTAMP)$' if not re.match(allowed, col_type.strip()): @@ -151,6 +164,7 @@ def _validate_sql_type(col_type: str) -> None: async def _log_processing(user_id: int, step: str, status: str = "ok", message: str = None, detail: str = None, conversation_id: int = None): + """Запись лога обработки в bot_processing_logs.""" try: async with async_session() as db: db.add(BotProcessingLog( @@ -167,6 +181,7 @@ async def _log_processing(user_id: int, step: str, status: str = "ok", message: async def _seed_default_settings(): + """Заполнение настроек по умолчанию при первом запуске.""" defaults = { "assistant_name": "София", "assistant_role": "Представитель компании AegisOne Engineering, помогающий клиентам с запросами по безопасности", @@ -185,6 +200,7 @@ async def _seed_default_settings(): async def _register_webhook(): + """Регистрация webhook-а в MAX Platform.""" try: subs = await max_api.get_subscriptions() logger.info(f"Current subscriptions: {subs}") @@ -199,6 +215,7 @@ async def _register_webhook(): def _verify_secret(request: Request) -> bool: + """Проверка webhook-секрета (пустой заголовок = OK для обратной совместимости).""" secret = request.headers.get("X-Max-Bot-Api-Secret", "") if not secret: return True @@ -206,6 +223,7 @@ def _verify_secret(request: Request) -> bool: async def _get_active_conv_id(user_id: int) -> int | None: + """Получение ID активного диалога пользователя.""" try: async with async_session() as db: result = await db.execute( @@ -798,6 +816,7 @@ async def api_delete_conversation(conv_id: int): @app.delete("/api/bot/users/{user_id}") async def api_delete_bot_user(user_id: int): + """Каскадное удаление пользователя: BotUser + BotConversation + BotMessage + BotProcessingLog. Заявки остаются.""" from app.models import BotMessage, BotProcessingLog from sqlalchemy import delete as sa_delete async with async_session() as db: diff --git a/max_bot/app/max_api.py b/max_bot/app/max_api.py index eb07cfb..3c31aa9 100644 --- a/max_bot/app/max_api.py +++ b/max_bot/app/max_api.py @@ -1,3 +1,11 @@ +""" +API-клиент для MAX Platform (Мой Мир / VK). + +Основные методы: + - send_message: отправка сообщений (с логированием outgoing BotMessage) + - subscribe_webhook: регистрация webhook-а + - answer_callback: ответ на callback-кнопки +""" import logging import httpx from typing import Optional @@ -9,6 +17,7 @@ TEST_USER_THRESHOLD = 999999000 class MaxAPI: + """Клиент MAX Platform API с поддержкой mock-реплик для тестов.""" def __init__(self): self.base_url = settings.max_api_base self.token = settings.max_token @@ -65,6 +74,7 @@ class MaxAPI: notify: bool = True, conversation_id: int = None, ) -> dict: + """Отправка сообщения. Если conversation_id — сохраняет outgoing BotMessage.""" if user_id >= TEST_USER_THRESHOLD: logger.info(f"MOCK send_message to test user {user_id}: {text[:50]}") self.mock_replies.append(text) diff --git a/max_bot/app/models.py b/max_bot/app/models.py index 92883f9..de25713 100644 --- a/max_bot/app/models.py +++ b/max_bot/app/models.py @@ -1,3 +1,20 @@ +""" +ORM-модели для PostgreSQL (SQLAlchemy 2.0). + +Таблицы: + - bot_settings: настройки бота (ключ-значение) + - bot_features: фичи бота (вкл/выкл) + - bot_categories: категории базы знаний + - bot_knowledge_base: карточки базы знаний + - bot_users: пользователи бота (контакты, согласие) + - bot_conversations: диалоги (state machine) + - bot_messages: сообщения (incoming/outgoing) + - bot_tickets: заявки + - bot_ticket_messages: сообщения заявок + - bot_processing_logs: логи обработки + - bot_ticket_statuses: история смены статусов заявок + - bot_unknown_questions: неизвестные вопросы (автогенерация KB) +""" from __future__ import annotations import datetime from sqlalchemy import Column, Integer, BigInteger, String, Text, Boolean, DateTime, ForeignKey, ARRAY @@ -53,6 +70,7 @@ class BotKnowledgeBase(Base): class BotUser(Base): + """Пользователь бота: профиль, контакты, согласие на обработку ПД.""" __tablename__ = "bot_users" id = Column(BigInteger, primary_key=True) first_name = Column(String(255), nullable=True) diff --git a/max_bot/app/settings_cache.py b/max_bot/app/settings_cache.py index 4b73c5e..08e0b1a 100644 --- a/max_bot/app/settings_cache.py +++ b/max_bot/app/settings_cache.py @@ -1,3 +1,9 @@ +""" +In-memory кэш настроек бота с синхронизацией с БД. + +При первом обращении загружает все настройки из bot_settings. +При set() обновляет и БД, и кэш. +""" import asyncio from typing import Optional, Dict from sqlalchemy import select diff --git a/max_bot/app/yandex_gpt.py b/max_bot/app/yandex_gpt.py index 1835f3f..9ed0021 100644 --- a/max_bot/app/yandex_gpt.py +++ b/max_bot/app/yandex_gpt.py @@ -1,3 +1,9 @@ +""" +YandexGPT интеграция для анализа намерений и генерации ответов. + +Модель: yandexgpt-lite +API: https://llm.api.cloud.yandex.net/foundationModels/v1/completion +""" import httpx import json import logging @@ -14,6 +20,7 @@ YANDEX_GPT_MODEL = "yandexgpt-lite" class YandexGPT: + """Клиент YandexGPT для анализа намерений и генерации ответов.""" def __init__(self): self._api_key: Optional[str] = None self._folder_id: Optional[str] = None @@ -27,6 +34,7 @@ class YandexGPT: return await self._load_credentials() async def _request(self, prompt: str, system_prompt: str = "", temperature: float = 0.3, max_tokens: int = 1000) -> Optional[str]: + """Базовый запрос к YandexGPT API.""" if not await self._load_credentials(): return None @@ -65,11 +73,13 @@ class YandexGPT: return None def _sanitize_input(self, text: str) -> str: + """Очистка и обрезка входного текста (макс. 2000 символов).""" text = text[:MAX_INPUT_LENGTH] text = text.replace("\r\n", "\n").replace("\r", "\n") return text async def analyze_intent(self, text: str, history_context: str = "") -> str: + """Анализ намерения: ticket/question/unknown. Aggressive ticket detection для услуг.""" safe_text = self._sanitize_input(text) safe_history = self._sanitize_input(history_context) if history_context else "" system_prompt = ( @@ -97,6 +107,7 @@ class YandexGPT: return "unknown" async def generate_answer(self, question: str, context: str, history_context: str = "") -> Optional[str]: + """Генерация ответа на вопрос с контекстом из базы знаний.""" safe_question = self._sanitize_input(question) safe_history = self._sanitize_input(history_context) if history_context else "" system_prompt = ( @@ -116,6 +127,7 @@ class YandexGPT: return await self._request(user_prompt, system_prompt) async def clarify_intent(self, text: str, history_context: str = "") -> str: + """Уточнение намерения: если есть слова-маркеры — отвечает без 'уточните'.""" safe_text = self._sanitize_input(text) safe_history = self._sanitize_input(history_context) if history_context else "" system_prompt = ( @@ -159,6 +171,7 @@ class YandexGPT: return result or "Подскажите, чем я могу помочь? Вы хотите задать вопрос об услугах AegisOne или оставить заявку?" async def generate_kb_card(self, user_questions: list) -> Optional[dict]: + """Генерация карточки базы знаний из вопросов пользователей (JSON).""" joined = "\n".join(f"- {q}" for q in user_questions) system_prompt = ( "Ты — редактор базы знаний компании AegisOne Engineering. " diff --git a/py_service/app/config.py b/py_service/app/config.py index c77065a..e70f57b 100644 --- a/py_service/app/config.py +++ b/py_service/app/config.py @@ -1,3 +1,12 @@ +""" +Конфигурация service portal (Pydantic Settings). + +Читает из .env: + - DATABASE_URL: PostgreSQL URL + - SECRET_KEY: ключ сессий + - SESSION_TTL: время жизни сессии (сек) + - DOCS_DIR/UPLOADS_DIR: пути к документам +""" import os from pathlib import Path from pydantic_settings import BaseSettings diff --git a/py_service/app/main.py b/py_service/app/main.py index 46df01a..50d9168 100644 --- a/py_service/app/main.py +++ b/py_service/app/main.py @@ -1,3 +1,13 @@ +""" +Главный модуль service portal FastAPI-приложения. + +Портал для управления: + - Сотрудниками (owner/engineer/technician) + - Клиентами и объектами + - Задачами и инцидентами + - Настройками бота и базой знаний + - Документами и отчётами +""" import os import asyncio from contextlib import asynccontextmanager diff --git a/py_service/app/models/models.py b/py_service/app/models/models.py index e7034ab..12bbdc9 100644 --- a/py_service/app/models/models.py +++ b/py_service/app/models/models.py @@ -1,3 +1,13 @@ +""" +ORM-модели для service portal (SQLAlchemy 2.0). + +Основные таблицы: + - users: сотрудники (owner/engineer/technician) + - customers: клиенты + - objects: объекты обслуживания + - tasks/ incidents: задачи и инциденты + - blog/cases/ideas: контент +""" from datetime import datetime from sqlalchemy import ( Column, Integer, String, Boolean, Text, DateTime, Numeric, Date, BigInteger, SmallInteger, JSON, ForeignKey, CheckConstraint diff --git a/py_service/app/routers/service_pages.py b/py_service/app/routers/service_pages.py index fd64fbc..ffd1044 100644 --- a/py_service/app/routers/service_pages.py +++ b/py_service/app/routers/service_pages.py @@ -1293,6 +1293,7 @@ BOT_SERVICE_URL = "http://127.0.0.1:8002" async def _proxy(method: str, path: str, body=None): + """Прокси запросов от service portal к max_bot API.""" async with httpx.AsyncClient() as client: if body is not None: resp = await client.request(method, f"{BOT_SERVICE_URL}{path}", json=body) @@ -1431,11 +1432,13 @@ async def proxy_bot_conversation_logs(conv_id: int, user: dict = Depends(get_cur @router.delete("/api/bot/conversations/{conv_id}") async def proxy_delete_conversation(conv_id: int, request: Request, user: dict = Depends(require_role("owner"))): + """Удаление диалога (каскадно: сообщения + логи). Доступно только owner.""" return await _proxy("DELETE", f"/api/bot/conversations/{conv_id}") @router.delete("/api/bot/users/{user_id}") async def proxy_delete_bot_user(user_id: int, request: Request, user: dict = Depends(require_role("owner"))): + """Удаление пользователя бота (каскадно: диалоги + сообщения + логи). Заявки остаются. Доступно только owner.""" return await _proxy("DELETE", f"/api/bot/users/{user_id}")