projects/pr_office.md

66 KiB
Raw Permalink Blame History

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: PMO AI Assistant

Автоматизация проектного офиса с использованием искусственного интеллекта


1. РЕШАЕМАЯ ЗАДАЧА

1.1. Бизнес-контекст

Проектный офис компании ведет множество параллельных проектов, по которым регулярно проводятся встречи различных типов (рабочие, оперативные советы, управляющие советы). Текущие проблемы:

  • Ручное ведение протоколов занимает 30-60% времени встречи
  • Потеря контекста между встречами — ответственные не помнят статус предыдущих мероприятий
  • Отсутствие единой базы знаний по проектам — сложно найти информацию по похожим встречам
  • Разрозненность данных — транскрибации в одном месте, протоколы в другом, мероприятия в третьем
  • Сложность контроля исполнения — нет автоматического отслеживания просроченных задач

1.2. Целевое состояние

Создать единый AI-powered сервис для проектного офиса, который:

  • Автоматически транскрибирует аудио встреч (или принимает текстовые исходники)
  • Генерирует структурированные протоколы трех типов с учетом истории проекта
  • Обеспечивает семантический поиск по всем встречам ("найди встречи, где обсуждали риски по модулю авторизации")
  • Формирует актуальный статус проекта и диаграмму Ганта на основе всех мероприятий
  • Позволяет редактировать протоколы как текст, так и через AI-чат
  • Экспортирует протоколы в DOCX для рассылки подписантам

1.3. Описание сервиса

PMO AI Assistant — это веб-приложение (SPA + REST API), которое:

  • Работает в браузере (Chrome, Firefox, Edge, Safari последних версий)
  • Не требует установки на рабочие станции пользователей
  • Интегрируется с корпоративными ВКС (MTS Link, Bitrix24, Яндекс.Телемост) для автоматической загрузки транскрибаций
  • Использует LLM (OpenAI-совместимые API) для генерации протоколов
  • Хранит данные в PostgreSQL с поддержкой векторного поиска (pgvector)
  • Может работать как с облачными, так и с локальными моделями векторизации

2. СТРАНИЦЫ SPA И ФУНКЦИОНАЛ

2.1. Структура приложения

/                           → Дашборд (обзор всех проектов)
/projects                   → Таблица проектов (список)
/projects/:id               → Карточка проекта (детали)
/meetings                   → Таблица встреч (список)
/meetings/:id               → Карточка встречи (работа с протоколом)
/settings                   → Настройки системы
/settings/prompts           → Управление промптами
/settings/users             → Управление пользователями
/settings/integrations      → Настройки интеграций (ВКС)

2.2. Детальное описание страниц

Дашборд (/)

Назначение: Быстрый обзор состояния всех проектов и недавних встреч.

Элементы:

  • Карточки проектов (Bento Grid):
    • Краткое имя проекта
    • Текущий этап (цветовой индикатор)
    • AI-статус (краткое резюме от ИИ)
    • Количество активных/просроченных мероприятий
    • Дата последней встречи
  • Лента последних встреч (Timeline):
    • Дата, проект, тип встречи
    • Краткое резюме (первые 2 предложения)
    • Статус протокола (черновик / утвержден)
  • Виджет "Требует внимания":
    • Просроченные мероприятия
    • Проекты без встреч > 14 дней
    • Нерешенные вопросы с высоким приоритетом

Таблица проектов (/projects)

Назначение: Управление списком проектов, быстрый поиск.

Элементы:

  • Таблица с колонками:
    • Краткое имя (кликабельно → переход в карточку)
    • Полное наименование
    • Этап (badge с цветом)
    • Ответственный ПМ
    • AI-статус (краткий текст)
    • Дата создания
    • Действия (⋮ меню: редактировать, архивировать)
  • Панель фильтров (слева):
    • Текстовый поиск (по всем полям)
    • Фильтр по этапу (multi-select)
    • Фильтр по ответственному
    • Фильтр по дате создания
  • Кнопка "+ Новый проект" (открывает модальное окно)

Карточка проекта (/projects/:id)

Назначение: Детальная информация о проекте, история встреч, визуализация.

Вкладки:

  1. Обзор:
    • Карточка проекта (все поля, редактируемые)
    • AI-статус (большой блок, кнопка "Обновить статус")
    • Диаграмма Ганта (Mermaid.js, интерактивная)
    • Статистика: всего встреч, активных мероприятий, просроченных
  2. Встречи:
    • Таблица встреч этого проекта (отфильтрованная)
    • Кнопка "+ Новая встреча"
  3. Мероприятия:
    • Сводная таблица всех мероприятий по проекту
    • Фильтры: статус, ответственный, срок
    • Экспорт в Excel

Таблица встреч (/meetings)

Назначение: Управление всеми встречами, семантический поиск.

Элементы:

  • Таблица с колонками:
    • Дата
    • Проект (ссылка)
    • Тип встречи (badge)
    • Место
    • Ответственный
    • Краткое резюме (первые 100 символов)
    • Статус протокола
  • Панель фильтров:
    • Векторный поиск (текстовое поле: "О чем были встречи?")
    • Фильтр по проекту
    • Фильтр по дате (range picker)
    • Фильтр по ответственному
    • Фильтр по типу встречи
  • Кнопка "+ Новая встреча"

Карточка встречи (/meetings/:id)

Назначение: Основная рабочая область — загрузка исходников, генерация и редактирование протокола.

Вкладки:

  1. Исходник:
    • Если аудио: плеер + кнопка "Транскрибировать" (показывает прогресс)
    • Если текст: редактор транскрибации (textarea с подсветкой синтаксиса)
    • Кнопка "Сохранить исходник"
  2. Резюме:
    • Блок с универсальным резюме (генерируется автоматически)
    • Кнопка "✏️ Редактировать" (переключает в режим редактирования)
    • Кнопка "Перегенерировать"
  3. Протокол:
    • Три кнопки генерации (с иконками):
      • 📋 Протокол встречи
      • Протокол оперативного совета
      • 👔 Протокол управляющего совета
    • Markdown редактор (md-editor-v3):
      • Split view (редактор + превью)
      • Панель инструментов (жирный, списки, таблицы)
      • Возможность вставки таблиц (для перечня мероприятий)
    • AI-чат (справа, collapsible):
      • Выделяешь текст в протоколе → пишешь в чат: "Сделай более формальным"
      • ИИ стримит ответ, можно принять или отклонить
    • Кнопки действий:
      • 💾 Сохранить черновик
      • Утвердить протокол (триггерит обновление статуса проекта)
      • 📥 Экспорт в DOCX

Настройки (/settings)

Вкладки:

  1. Проекты: Управление справочником проектов (CRUD)
  2. Пользователи: Управление доступами (роли: admin, pmo, viewer)
  3. Промпты: Редактирование системных промптов для генерации протоколов
  4. Интеграции: Настройка API-ключей для ВКС (MTS Link, Bitrix24, Яндекс.Телемост)
  5. Векторизация: Выбор режима (OpenAI API / Локальная модель)

3. ИТОГОВЫЙ ФУНКЦИОНАЛ (MVP)

3.1. Функционал первого этапа (MVP)

Управление проектами (CRUD, поиск, фильтры)
Управление встречами (CRUD, загрузка аудио/текста)
Генерация универсального резюме встречи
Генерация протоколов трех типов (с учетом истории проекта)
Редактирование протоколов в Markdown
AI-чат для редактирования выделенного текста (стриминг)
Экспорт протокола в DOCX
Векторный поиск по встречам (OpenAI embeddings / локальная модель)
AI-статус проекта (генерируется при утверждении протокола)
Диаграмма Ганта (Mermaid.js)
Интеграция с одной ВКС (например, Яндекс.Телемост)

3.2. Функционал второго этапа

Очереди задач (BullMQ + Redis) для асинхронной транскрибации и генерации
WebSocket-уведомления о готовности транскрибации
Интеграция с остальными ВКС (MTS Link, Bitrix24)
Расширенная аналитика (дашборды, графики)
Транскрибация аудио через Whisper API (синхронно)


4. АРХИТЕКТУРА И СТЕК ТЕХНОЛОГИЙ

4.1. Backend

  • Runtime: Node.js 20+ (LTS)
  • Framework: Express.js 4.x
  • База данных: PostgreSQL 15+ с расширением pgvector
  • Валидация: Zod (для runtime-проверок)
  • ИИ-клиент: собственный модуль основанный на OpenAI REST API
  • Локальная векторизация: @xenova/transformers (Hugging Face, ONNX Runtime)
  • Генерация DOCX: docx (библиотека для создания Word-документов)
  • Аутентификация: JWT (jsonwebtoken + bcrypt)
  • Логирование: pino (быстрый структурированный логгер)

4.2. Frontend

  • Framework: Vue 3 (Composition API, <script setup>)
  • UI Kit: tailwindcss
  • State Management: Pinia
  • Markdown редактор: md-editor-v3
  • Визуализация: mermaid (для диаграммы Ганта)
  • HTTP клиент: axios
  • Стриминг: Fetch API + ReadableStream (для SSE)
  • Роутинг: Vue Router 4

4.3. Инфраструктура

  • Сервисы:
    • postgres задаётся в .env, в случае отсутствия, базы инициализируются
    • backend (Node.js + Express), запуск pm2
    • frontend раздаётся из dist через express static
  • Переменные окружения: .env файл (ключи API, настройки БД)

4.4. Deployment

  • Frontend собирается через Vite в папку dist
  • раздаётся из dist через express static
  • PostgreSQL работает в отдельном контейнере с volume для данных. настраивается через .env

5. МОДЕЛЬ ДАННЫХ (PostgreSQL)

5.1. Принципы проектирования

  1. Реляционное ядро — поля, по которым часто фильтруем/сортируем/делаем JOIN
  2. JSONB для гибкости — "подушки безопасности" для будущих изменений
  3. Векторные поля — для семантического поиска
  4. Аудитcreated_at, updated_at во всех таблицах
  5. Soft Delete — флаг is_deleted вместо физического удаления

5.2. Таблица users (Пользователи)

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    full_name VARCHAR(255) NOT NULL,
    role VARCHAR(50) NOT NULL DEFAULT 'viewer', -- 'admin', 'pmo', 'viewer'
    
    -- Подушка безопасности: дополнительные настройки пользователя
    settings JSONB NOT NULL DEFAULT '{}',
    -- Пример: {"theme": "dark", "notifications": true, "language": "ru"}
    
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Индексы
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);

5.3. Таблица projects (Проекты)

CREATE TABLE projects (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    short_name VARCHAR(100) NOT NULL,
    full_name VARCHAR(500) NOT NULL,
    stage VARCHAR(50) NOT NULL DEFAULT 'preparation',
    -- 'preparation', 'design', 'development', 'pre_ope', 'ope'
    
    pmo_responsible_id UUID REFERENCES users(id),
    
    -- Подушка безопасности: все расширяемые данные
    metadata JSONB NOT NULL DEFAULT '{}',
    -- Пример содержимого:
    -- {
    --   "customer": "Газпром",
    --   "signatories": ["Иванов И.И.", "Петров П.П."],
    --   "budget": 1000000,
    --   "custom_fields": {"priority": "high", "department": "IT"}
    -- }
    
    -- AI-статус проекта (генерируется ИИ)
    ai_status TEXT,
    
    -- Код Mermaid для диаграммы Ганта
    gantt_mermaid TEXT,
    
    -- Подушка безопасности: настройки интеграций для проекта
    integration_settings JSONB NOT NULL DEFAULT '{}',
    -- Пример: {"vcs_provider": "yandex", "vcs_meeting_id": "12345"}
    
    is_deleted BOOLEAN NOT NULL DEFAULT false,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Индексы
CREATE INDEX idx_projects_short_name ON projects(short_name);
CREATE INDEX idx_projects_stage ON projects(stage);
CREATE INDEX idx_projects_pmo_responsible ON projects(pmo_responsible_id);
CREATE INDEX idx_projects_metadata ON projects USING GIN (metadata);
CREATE INDEX idx_projects_full_text ON projects USING GIN (
    to_tsvector('russian', short_name || ' ' || full_name || ' ' || COALESCE(ai_status, ''))
);

-- Триггер для автоматического обновления updated_at
CREATE TRIGGER update_projects_updated_at
    BEFORE UPDATE ON projects
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

5.4. Таблица meetings (Встречи)

CREATE TABLE meetings (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
    meeting_date DATE NOT NULL,
    location VARCHAR(200),
    type VARCHAR(50) NOT NULL DEFAULT 'standard',
    -- 'standard', 'op_council', 'steering_council'
    
    pmo_responsible_id UUID REFERENCES users(id),
    
    -- Тип исходника
    source_type VARCHAR(50) NOT NULL DEFAULT 'text',
    -- 'audio', 'text', 'vcs_integration'
    
    -- Исходные данные
    raw_transcript TEXT,
    audio_file_url VARCHAR(500), -- путь к аудиофайлу в storage
    
    -- AI-обработанные данные
    summary TEXT, -- универсальное резюме
    protocol_markdown TEXT, -- итоговый протокол в Markdown
    
    -- Подушка безопасности: структурированные данные протокола
    protocol_data JSONB NOT NULL DEFAULT '{}',
    -- Пример содержимого:
    -- {
    --   "agenda": ["Пункт 1", "Пункт 2"],
    --   "decisions": ["Решили 1", "Решили 2"],
    --   "action_items": [
    --     {
    --       "task": "Подготовить ТЗ",
    --       "deadline": "2026-07-01",
    --       "assignee": "Иванов И.И.",
    --       "status": "active"
    --     }
    --   ],
    --   "unresolved_issues": ["Нет бюджета на Q3"],
    --   "previous_results": ["Мероприятие 1 выполнено"]
    -- }
    
    -- Статус протокола
    protocol_status VARCHAR(50) NOT NULL DEFAULT 'draft',
    -- 'draft', 'approved'
    
    -- Подушка безопасности: метаданные источника
    source_metadata JSONB NOT NULL DEFAULT '{}',
    -- Пример: {"vcs_provider": "yandex", "recording_url": "...", "duration": 3600}
    
    -- Подушка безопасности: сырой ответ ИИ (для отладки)
    ai_raw_response JSONB,
    
    -- Векторное представление транскрибации (для семантического поиска)
    transcript_embedding vector(1536), -- 1536 для text-embedding-3-small
    
    is_deleted BOOLEAN NOT NULL DEFAULT false,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Индексы
CREATE INDEX idx_meetings_project ON meetings(project_id);
CREATE INDEX idx_meetings_date ON meetings(meeting_date);
CREATE INDEX idx_meetings_type ON meetings(type);
CREATE INDEX idx_meetings_status ON meetings(protocol_status);
CREATE INDEX idx_meetings_protocol_data ON meetings USING GIN (protocol_data);

-- Векторный индекс (IVFFlat, можно заменить на HNSW для больших данных)
CREATE INDEX idx_meetings_embedding ON meetings 
    USING ivfflat (transcript_embedding vector_cosine_ops)
    WITH (lists = 100);

-- Триггер для updated_at
CREATE TRIGGER update_meetings_updated_at
    BEFORE UPDATE ON meetings
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

5.5. Таблица prompt_templates (Шаблоны промптов)

CREATE TABLE prompt_templates (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    key VARCHAR(100) UNIQUE NOT NULL,
    -- 'gen_standard_protocol', 'gen_op_council_protocol', 
    -- 'gen_steering_protocol', 'gen_summary', 'gen_project_status'
    
    name VARCHAR(255) NOT NULL,
    description TEXT,
    
    system_prompt TEXT NOT NULL,
    user_prompt_template TEXT NOT NULL,
    -- С плейсхолдерами: {{transcript}}, {{previous_protocols}}, {{project_status}}
    
    -- Подушка безопасности: дополнительные параметры для ИИ
    ai_parameters JSONB NOT NULL DEFAULT '{}',
    -- Пример: {"temperature": 0.7, "max_tokens": 2000, "top_p": 0.9}
    
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Индексы
CREATE INDEX idx_prompt_templates_key ON prompt_templates(key);

5.6. Таблица audit_log (Журнал действий)

CREATE TABLE audit_log (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES users(id),
    action VARCHAR(100) NOT NULL,
    -- 'project_created', 'meeting_approved', 'protocol_edited'
    
    entity_type VARCHAR(50) NOT NULL, -- 'project', 'meeting'
    entity_id UUID NOT NULL,
    
    -- Подушка безопасности: дополнительные данные действия
    details JSONB NOT NULL DEFAULT '{}',
    -- Пример: {"old_status": "draft", "new_status": "approved"}
    
    ip_address INET,
    user_agent TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Индексы
CREATE INDEX idx_audit_log_user ON audit_log(user_id);
CREATE INDEX idx_audit_log_entity ON audit_log(entity_type, entity_id);
CREATE INDEX idx_audit_log_created_at ON audit_log(created_at DESC);

5.7. Вспомогательная функция для updated_at

CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

6. ДИЗАЙН И UI/UX BEST PRACTICES 2026

6.1. Визуальный стиль: "Modern Minimal AI-Native"

Ключевые принципы:

  • Воздух и пространство — много white space, элементы не прижаты друг к другу
  • Глубина через тени — subtle shadows для создания иерархии (не flat design)
  • Скругленные углы — 12-16px для карточек, 8px для кнопок
  • Переменные шрифты — Inter Variable или Geist (оптимизированы для UI)
  • Цветовая палитра — нейтральные серые + акцентный цвет (синий/фиолетовый для AI)

Цветовая схема:

/* Light Mode */
--bg-primary: #FAFAFA;
--bg-secondary: #FFFFFF;
--bg-tertiary: #F5F5F5;
--text-primary: #1A1A1A;
--text-secondary: #6B7280;
--border: #E5E7EB;
--accent: #6366F1; /* Индиго для AI-элементов */
--success: #10B981;
--warning: #F59E0B;
--error: #EF4444;

/* Dark Mode */
--bg-primary: #0A0A0A;
--bg-secondary: #171717;
--bg-tertiary: #262626;
--text-primary: #FAFAFA;
--text-secondary: #A3A3A3;
--border: #2E2E2E;

6.2. Layout Best Practices 2026

Bento Grid для дашборда

┌─────────────────┬─────────┬─────────┐
│                 │ Статус  │ Актив.  │
│   Проект 1      │ ██████  │   12    │
│   AI-статус...  │         │         │
├─────────────────┼─────────┴─────────┤
│                 │                     │
│   Проект 2      │   Проект 3          │
│                 │                     │
└─────────────────┴─────────────────────┘
  • Карточки разных размеров создают визуальный интерес
  • Каждая карточка — самостоятельный виджет

Sidebar Navigation (слева)

┌──────┬──────────────────────────────┐
│ 🏠   │                              │
│ 📁   │   Основной контент           │
│ 📅   │                              │
│ ⚙️   │                              │
│      │                              │
│ 👤   │                              │
└──────┴──────────────────────────────┘
  • Компактная (64px), иконки + tooltip при hover
  • Активный пункт подсвечен акцентным цветом

Split View для работы с протоколом

┌──────────────────────────────────────┐
│  Вкладки: Исходник | Резюме | Прот. │
├──────────────────────────┬───────────┤
│                          │ 🤖 AI-чат │
│   Markdown редактор      │           │
│   (md-editor-v3)         │ [Ввод]    │
│                          │           │
│                          │           │
└──────────────────────────┴───────────┘
  • AI-чат collapsible (можно скрыть)
  • При выделении текста в редакторе — появляется floating toolbar с кнопкой "Спросить ИИ"

6.3. Компоненты и паттерны

Карточки проектов

  • Hover-эффект: легкое поднятие (translateY: -2px) + усиление тени
  • Badge для этапа проекта (цветной pill)
  • AI-статус — курсивом, с иконкой
  • Прогресс-бар для активных мероприятий

Таблицы

  • Sticky header (прилипает при скролле)
  • Ze- Zeбра (чередование строк) — повышает читаемость больших таблиц
  • Row actions — иконки действий появляются при hover над строкой (не загромождают UI)
  • Inline editing — двойной клик по ячейке → редактирование (для быстрых правок)
  • Skeleton loaders вместо спиннеров — плавнее восприятие загрузки
  • Empty state — иллюстрация + призыв к действию ("Создайте первый проект")

Формы и ввод данных

  • Floating labels (лейбл поднимается при фокусе)
  • Inline validation — ошибка появляется под полем сразу после blur
  • Auto-save черновиков (каждые 30 сек) с индикатором "Сохранено ✓"
  • Keyboard shortcuts — Ctrl+S сохранить, Ctrl+Enter отправить в чат
  • Drag & drop для загрузки файлов с визуальной зоной

AI-индикаторы и состояния

  • Пульсирующая иконка во время генерации ИИ (не спиннер, а мягкая анимация)
  • Streaming text — текст появляется посимвольно с курсором
  • Confidence badge — "ИИ-черновик" (желтый) vs "Утверждено" (зеленый)
  • Skeleton для AI-блоков — имитация структуры текста до загрузки

Toast-уведомления

  • Появляются в правом верхнем углу
  • Автоматически исчезают через 5 секунд (успех) или требуют закрытия (ошибка)
  • Группируются, если их много
  • С иконками и цветовой кодировкой

6.4. Типографика

Шрифтовая пара:

  • Заголовки: Inter Variable (600-700 weight)
  • Основной текст: Inter Variable (400-500 weight)
  • Моноширинный (код, JSON): JetBrains Mono

Размерная шкала:

H1: 32px / 40px line-height (заголовки страниц)
H2: 24px / 32px (заголовки секций)
H3: 20px / 28px (заголовки карточек)
Body Large: 16px / 24px (основной текст)
Body: 14px / 20px (таблицы, формы)
Caption: 12px / 16px (подписи, метаданные)

6.5. Анимации и микро-взаимодействия

Принцип: "Animations should be functional, not decorative"

  • Длительность: 150-250ms для UI-элементов, 300-500ms для переходов между страницами
  • Easing: cubic-bezier(0.4, 0, 0.2, 1) (Material standard)
  • Hover states: плавное изменение фона/тени (150ms)
  • Modal open: fade + scale (0.95 → 1.0) за 200ms
  • Tab switch: slide + fade контент (200ms)
  • AI streaming: typewriter effect с задержкой 20-30ms между символами

Reduced motion: Учитывать prefers-reduced-motion — отключать анимации для пользователей с соответствующей настройкой ОС.


7. ВЕКТОРИЗАЦИЯ: ДВА РЕЖИМА РАБОТЫ

7.1. Архитектура модуля векторизации

Создаем абстрактный интерфейс EmbeddingProvider, который имеет две реализации. Это позволяет переключать режимы через настройки без изменения бизнес-логики.

// src/modules/embeddings/types.ts
export interface EmbeddingProvider {
  name: string;
  dimension: number;
  embed(text: string): Promise<number[]>;
  embedBatch(texts: string[]): Promise<number[][]>;
  isAvailable(): Promise<boolean>;
}

// src/modules/embeddings/index.ts
export class EmbeddingService {
  private provider: EmbeddingProvider;
  
  constructor(mode: 'openai' | 'local') {
    this.provider = mode === 'openai' 
      ? new OpenAIEmbeddingProvider() 
      : new LocalEmbeddingProvider();
  }
  
  async embed(text: string): Promise<number[]> {
    return this.provider.embed(text);
  }
}

7.2. Режим 1: OpenAI API

Когда использовать:

  • Есть доступ к интернету
  • Нужно максимальное качество эмбеддингов
  • Готовы платить за API (~$0.02 за 1M токенов для text-embedding-3-small)

Реализация:

// src/modules/embeddings/openai.provider.ts
import OpenAI from 'openai';

export class OpenAIEmbeddingProvider implements EmbeddingProvider {
  name = 'openai';
  dimension = 1536; // text-embedding-3-small
  
  private client: OpenAI;
  private model = 'text-embedding-3-small';
  
  constructor() {
    this.client = new OpenAI({
      apiKey: process.env.OPENAI_API_KEY,
      baseURL: process.env.OPENAI_BASE_URL, // для прокси/совместимых API
    });
  }
  
  async embed(text: string): Promise<number[]> {
    const response = await this.client.embeddings.create({
      model: this.model,
      input: this.preprocessText(text),
    });
    return response.data[0].embedding;
  }
  
  async embedBatch(texts: string[]): Promise<number[][]> {
    // OpenAI поддерживает batch до 2048 элементов
    const chunks = this.chunkArray(texts, 100);
    const results = [];
    
    for (const chunk of chunks) {
      const response = await this.client.embeddings.create({
        model: this.model,
        input: chunk.map(t => this.preprocessText(t)),
      });
      results.push(...response.data.map(d => d.embedding));
    }
    
    return results;
  }
  
  private preprocessText(text: string): string {
    // Очистка, нормализация, удаление спецсимволов
    return text
      .replace(/\s+/g, ' ')
      .trim()
      .slice(0, 8000); // лимит токенов
  }
  
  async isAvailable(): Promise<boolean> {
    try {
      await this.embed('test');
      return true;
    } catch {
      return false;
    }
  }
}

Плюсы:

  • Высочайшее качество эмбеддингов
  • Поддержка русского языка из коробки
  • Не требует ресурсов сервера
  • Быстрая интеграция

Минусы:

  • Зависимость от интернета
  • Стоимость (хоть и небольшая)
  • Данные уходят во внешнее API (для sensitive данных может быть критично)
  • Rate limits

7.3. Режим 2: Локальная модель через @xenova/transformers

Когда использовать:

  • Работа в закрытом контуре (без интернета)
  • Требования к безопасности данных (ничего не покидает сервер)
  • Нужно избежать затрат на API
  • Готовы пожертвовать немного качества

Реализация:

// src/modules/embeddings/local.provider.ts
import { pipeline, env } from '@xenova/transformers';

// Отключаем загрузку моделей из интернета при первом запуске
// Модели предзагружены в ./models/
env.localModelPath = './models';
env.allowRemoteModels = false;

export class LocalEmbeddingProvider implements EmbeddingProvider {
  name = 'local';
  dimension = 384; // для all-MiniLM-L6-v2
  
  private embedder: any = null;
  private modelName = 'Xenova/all-MiniLM-L6-v2';
  // Альтернативы для русского:
  // - 'ai-forever/sbert_large_nlu_ru' (русскоязычная, 1024 dim)
  // - 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2' (мультиязычная, 384 dim)
  
  async initialize(): Promise<void> {
    if (this.embedder) return;
    
    console.log(`Loading embedding model: ${this.modelName}...`);
    this.embedder = await pipeline(
      'feature-extraction', 
      this.modelName,
      { quantized: true } // квантованная модель — быстрее и меньше
    );
    console.log('Embedding model loaded');
  }
  
  async embed(text: string): Promise<number[]> {
    await this.initialize();
    
    const output = await this.embedder(this.preprocessText(text), {
      pooling: 'mean',
      normalize: true,
    });
    
    return Array.from(output.data);
  }
  
  async embedBatch(texts: string[]): Promise<number[][]> {
    await this.initialize();
    
    // Обрабатываем батчами по 32 (оптимально для CPU)
    const results = [];
    for (let i = 0; i < texts.length; i += 32) {
      const batch = texts.slice(i, i + 32);
      const output = await this.embedder(batch, {
        pooling: 'mean',
        normalize: true,
      });
      
      // output имеет shape [batch_size, dimension]
      for (let j = 0; j < batch.length; j++) {
        const embedding = output[j];
        results.push(Array.from(embedding.data));
      }
    }
    
    return results;
  }
  
  private preprocessText(text: string): string {
    return text.replace(/\s+/g, ' ').trim().slice(0, 512);
  }
  
  async isAvailable(): Promise<boolean> {
    try {
      await this.initialize();
      return true;
    } catch {
      return false;
    }
  }
}

Важные нюансы локальной модели:

  1. Выбор модели:

    • all-MiniLM-L6-v2 (384 dim) — быстрая, легкая (~80MB), хорошее качество для английского
    • paraphrase-multilingual-MiniLM-L12-v2 (384 dim) — мультиязычная, хорошо работает с русским
    • sbert_large_nlu_ru (1024 dim) — специализированная русская модель, лучшее качество для русского, но тяжелее (~1.3GB)
  2. Предзагрузка моделей:

    # Скрипт для предзагрузки модели в docker-образ
    # scripts/download-models.js
    const { downloadFile } = require('@xenova/transformers');
    
    async function downloadModels() {
      await downloadFile('Xenova/paraphrase-multilingual-MiniLM-L12-v2');
    }
    
    downloadModels();
    
  3. Производительность:

    • CPU: ~50-100ms на один текст (зависит от длины)
    • RAM: ~500MB-1.5GB (зависит от модели)
    • Для batch из 100 текстов: ~5-10 секунд
  4. Переменная размерность:

    • При смене модели нужно пересоздать все эмбеддинги в БД
    • Поле transcript_embedding должно иметь размерность, соответствующую модели
    • Решение: хранить размерность в настройках и делать миграцию при смене

7.4. Переключение режимов через настройки

// src/config/embeddings.config.ts
export const embeddingsConfig = {
  mode: process.env.EMBEDDING_MODE as 'openai' | 'local', // 'openai' | 'local'
  
  openai: {
    model: process.env.OPENAI_EMBEDDING_MODEL || 'text-embedding-3-small',
    dimension: 1536,
  },
  
  local: {
    model: process.env.LOCAL_EMBEDDING_MODEL || 'Xenova/paraphrase-multilingual-MiniLM-L12-v2',
    dimension: 384,
    quantized: true,
  },
};

⚠️ Важно: При смене режима нужно пересоздать все эмбеддинги в БД. Добавляем в админку кнопку "Перегенерировать все эмбеддинги" (запускает фоновую задачу).


8. API КОНТРАКТЫ (REST)

8.1. Общие принципы

  • Все ответы в формате { data: ..., meta: ... } или { error: { code, message } }
  • Пагинация: ?page=1&limit=20 → ответ включает meta: { total, page, limit, pages }
  • Аутентификация: Authorization: Bearer <jwt>
  • Content-Type: application/json (для файлов — multipart/form-data)

8.2. Auth API

POST /api/auth/login
Body: { email, password }
Response: { data: { token, user } }

POST /api/auth/logout
Headers: Authorization: Bearer <token>
Response: { data: { success: true } }

GET /api/auth/me
Headers: Authorization: Bearer <token>
Response: { data: { id, email, fullName, role } }

8.3. Projects API

GET /api/projects
Query: ?search=...&stage=...&pmoResponsibleId=...&page=1&limit=20
Response: { 
  data: [{ id, shortName, fullName, stage, aiStatus, ... }],
  meta: { total, page, limit, pages }
}

POST /api/projects
Body: { shortName, fullName, stage, pmoResponsibleId, metadata }
Response: { data: { project } }

GET /api/projects/:id
Response: { data: { project, stats: { meetingsCount, activeTasks, overdueTasks } } }

PUT /api/projects/:id
Body: { ...fields }
Response: { data: { project } }

DELETE /api/projects/:id (soft delete)
Response: { data: { success: true } }

POST /api/projects/:id/regenerate-status
Response: { data: { aiStatus, ganttMermaid } }

8.4. Meetings API

GET /api/meetings
Query: ?projectId=...&vectorSearch=...&dateFrom=...&dateTo=...&type=...&page=1&limit=20
Response: { data: [...], meta: {...} }

POST /api/meetings
Body (multipart/form-data):
  - projectId: UUID
  - meetingDate: Date
  - type: 'standard' | 'op_council' | 'steering_council'
  - sourceType: 'audio' | 'text' | 'vcs_integration'
  - audioFile: File (опционально)
  - rawTranscript: string (если sourceType='text')
Response: { data: { meeting } }

GET /api/meetings/:id
Response: { data: { meeting, project, previousMeetings } }

PUT /api/meetings/:id
Body: { ...fields }
Response: { data: { meeting } }

POST /api/meetings/:id/transcribe
Response: { data: { taskId, status: 'processing' } }
# Синхронно для MVP (возвращает результат сразу)

POST /api/meetings/:id/generate-summary
Response: { data: { summary } }

POST /api/meetings/:id/generate-protocol
Body: { type: 'standard' | 'op_council' | 'steering_council' }
Response: { data: { protocolMarkdown, protocolData } }

POST /api/meetings/:id/approve
Response: { data: { meeting, projectAiStatus } }

GET /api/meetings/:id/export-docx
Response: File (application/vnd.openxmlformats-officedocument.wordprocessingml.document)

8.5. AI Chat API (Server-Sent Events)

POST /api/ai/chat
Headers: 
  - Authorization: Bearer <token>
  - Accept: text/event-stream
Body: { 
  context: "...", // выделенный текст из протокола
  instruction: "Сделай более формальным",
  meetingId: UUID
}
Response: SSE stream
  event: token
  data: {"text": "Новая"}
  
  event: token
  data: {"text": " формулировка"}
  
  event: done
  data: {"fullText": "..."}

Реализация на бэкенде:

// src/routes/ai.chat.ts
router.post('/chat', authMiddleware, async (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  
  const { context, instruction, meetingId } = req.body;
  
  const stream = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      { role: 'system', content: 'Ты помощник в редактировании протоколов...' },
      { role: 'user', content: `Текст: ${context}\n\nИнструкция: ${instruction}` }
    ],
    stream: true,
  });
  
  let fullText = '';
  for await (const chunk of stream) {
    const text = chunk.choices[0]?.delta?.content || '';
    fullText += text;
    res.write(`event: token\ndata: ${JSON.stringify({ text })}\n\n`);
  }
  
  res.write(`event: done\ndata: ${JSON.stringify({ fullText })}\n\n`);
  res.end();
});

Реализация на фронте:

// src/composables/useAiChat.ts
export function useAiChat() {
  const streamingText = ref('');
  const isLoading = ref(false);
  
  async function sendChat(context: string, instruction: string) {
    isLoading.value = true;
    streamingText.value = '';
    
    const response = await fetch('/api/ai/chat', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Accept': 'text/event-stream',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ context, instruction }),
    });
    
    const reader = response.body!.getReader();
    const decoder = new TextDecoder();
    
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      
      const chunk = decoder.decode(value);
      const lines = chunk.split('\n');
      
      for (const line of lines) {
        if (line.startsWith('data: ')) {
          const data = JSON.parse(line.slice(6));
          if (data.text) {
            streamingText.value += data.text;
          }
        }
      }
    }
    
    isLoading.value = false;
  }
  
  return { streamingText, isLoading, sendChat };
}

8.6. Prompts API

GET /api/prompts
Response: { data: [{ key, name, systemPrompt, userPromptTemplate }] }

PUT /api/prompts/:key
Body: { systemPrompt, userPromptTemplate, aiParameters }
Response: { data: { prompt } }

POST /api/prompts/:key/test
Body: { testTranscript: "..." }
Response: { data: { result: "..." } }

8.7. Settings API

GET /api/settings/embeddings
Response: { data: { mode, availableProviders, currentDimension } }

PUT /api/settings/embeddings
Body: { mode: 'openai' | 'local' }
Response: { data: { success: true, requiresReindex: true } }

POST /api/settings/embeddings/reindex
Response: { data: { taskId, status: 'processing' } }

9. РАБОТА С ИИ: ПРОМПТЫ И RAG

9.1. Структура промптов

Все промпты хранятся в таблице prompt_templates и редактируются через админку. Это позволяет настраивать поведение ИИ без изменения кода.

Базовые промпты:

gen_summary (Универсальное резюме)

System: Ты — ассистент проектного офиса. Твоя задача — создать краткое, 
информативное резюме встречи на основе транскрибации. Резюме должно быть 
понятно человеку, который не присутствовал на встрече.

User: Транскрибация встречи:
{{transcript}}

Создай резюме в 3-5 предложениях, выдели:
1. Основную тему обсуждения
2. Ключевые решения
3. Главные проблемы или риски

gen_standard_protocol (Протокол обычной встречи)

System: Ты — секретарь проектного офиса. Создай структурированный протокол 
встречи в формате Markdown. Будь точен, формален, не добавляй информацию, 
которой нет в транскрибации.

User: Транскрибация встречи:
{{transcript}}

Сформируй протокол со следующей структурой:

# Протокол встречи

## Повестка дня
- Пункт 1
- Пункт 2

## Решили
1. Решение 1
2. Решение 2

## Перечень мероприятий
| № | Мероприятие | Срок | Ответственный |
|---|-------------|------|---------------|
| 1 | ...         | ...  | ...           |

## Проблемные и нерешенные вопросы
- Вопрос 1
- Вопрос 2

ВАЖНО:
- Если в транскрибации нет четкого срока или ответственного — укажи "уточнить"
- Не выдумывай информацию, которой нет в тексте
- Используй деловой стиль

gen_op_council_protocol (Протокол оперативного совета)

System: Ты — секретарь оперативного совета проекта. Особенность протокола 
оперсовета — наличие блока "Результаты предыдущих мероприятий". Сравни 
текущую транскрибацию с предыдущими протоколами и определи статус каждого 
мероприятия.

User: Текущая транскрибация:
{{transcript}}

Предыдущие мероприятия (из прошлых протоколов):
{{previous_action_items}}

Сформируй протокол:

# Протокол оперативного совета

## Повестка дня
...

## Результаты предыдущих мероприятий
| Мероприятие | Срок | Ответственный | Статус | Комментарий |
|-------------|------|---------------|--------|-------------|
| ...         | ...  | ...           | ✅/⏳/❌ | ...        |

Статусы:
- ✅ Выполнено
- ⏳ В работе / Просрочено
- ❌ Не выполнено / Отменено

## Решили
...

## Перечень мероприятий
| № | Мероприятие | Срок | Ответственный |
|---|-------------|------|---------------|
| 1 | ...         | ...  | ...           |

## Проблемные и нерешенные вопросы
...

gen_steering_protocol (Протокол управляющего совета)

System: Ты — секретарь управляющего совета проекта. Протокол предназначен 
для высшего руководства. Будь лаконичен, фокусируйся на стратегических 
вопросах, рисках и ключевых решениях.

User: Текущая транскрибация:
{{transcript}}

Текущий статус проекта (от ИИ):
{{project_ai_status}}

Открытые вопросы из предыдущих встреч:
{{unresolved_issues}}

Сформируй протокол:

# Протокол управляющего совета

## Повестка дня
...

## Текущий статус проекта
(краткое резюме на основе AI-статуса)

## Проблемы и открытые вопросы
(агрегированный список из unresolved_issues + новые из транскрибации)

## Решили
(стратегические решения)

## Перечень мероприятий
| № | Мероприятие | Срок | Ответственный |
|---|-------------|------|---------------|

## Проблемные и нерешенные вопросы
(только критичные, требующие эскалации)

gen_project_status (AI-статус проекта)

System: Ты — аналитик проектного офиса. На основе всех протоколов и 
мероприятий проекта сформулируй краткий актуальный статус (3-4 предложения).

User: Проект: {{project_name}}
Все протоколы и мероприятия:
{{all_meetings_summary}}

Сформируй статус, включающий:
1. Текущий фокус проекта
2. Ключевые достижения
3. Главные риски
4. Следующие шаги

9.2. RAG-логика (Retrieval-Augmented Generation)

Перед генерацией протокола бэкенд собирает контекст:

// src/services/protocol.generator.ts
export class ProtocolGenerator {
  async generate(meetingId: string, type: ProtocolType) {
    const meeting = await db.query.meetings.findFirst({
      where: eq(meetings.id, meetingId),
      with: { project: true }
    });
    
    let context = {
      transcript: meeting.rawTranscript,
      previousActionItems: [],
      unresolvedIssues: [],
      projectAiStatus: meeting.project.aiStatus,
    };
    
    if (type === 'op_council' || type === 'steering_council') {
      // Получаем предыдущие встречи этого проекта
      const previousMeetings = await db.query.meetings.findMany({
        where: and(
          eq(meetings.projectId, meeting.projectId),
          lt(meetings.meetingDate, meeting.meetingDate),
          eq(meetings.protocolStatus, 'approved')
        ),
        orderBy: desc(meetings.meetingDate),
        limit: 5, // последние 5 встреч
      });
      
      // Извлекаем мероприятия из protocol_data
      context.previousActionItems = previousMeetings.flatMap(m => 
        m.protocolData?.action_items || []
      );
      
      // Извлекаем нерешенные вопросы
      context.unresolvedIssues = previousMeetings.flatMap(m => 
        m.protocolData?.unresolved_issues || []
      );
    }
    
    // Считаем токены и обрезаем контекст, если нужно
    const tokenCount = countTokens(JSON.stringify(context));
    if (tokenCount > 100000) {
      context = this.truncateContext(context, 100000);
    }
    
    // Загружаем шаблон промпта
    const template = await db.query.promptTemplates.findFirst({
      where: eq(promptTemplates.key, `gen_${type}_protocol`)
    });
    
    // Заполняем плейсхолдеры
    const userPrompt = this.fillTemplate(template.userPromptTemplate, context);
    
    // Вызываем LLM
    const response = await openai.chat.completions.create({
      model: 'gpt-4o-mini',
      messages: [
        { role: 'system', content: template.systemPrompt },
        { role: 'user', content: userPrompt }
      ],
      temperature: template.aiParameters?.temperature || 0.3,
      max_tokens: template.aiParameters?.max_tokens || 4000,
    });
    
    const markdown = response.choices[0].message.content;
    
    // Парсим Markdown в структурированные данные
    const protocolData = this.parseProtocolMarkdown(markdown);
    
    return { markdown, protocolData };
  }
  
  private parseProtocolMarkdown(markdown: string): ProtocolData {
    // Парсим таблицы, списки и заголовки
    // Используем библиотеку marked или собственный парсер
    // ...
    return {
      agenda: [],
      decisions: [],
      action_items: [],
      unresolved_issues: [],
    };
  }
}

9.3. Обработка ошибок ИИ

// src/utils/aiErrorHandler.ts
export async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3
): Promise<T> {
  let lastError: Error;
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      
      if (error.status === 429) {
        // Rate limit — ждем и повторяем
        await sleep(Math.pow(2, i) * 1000);
        continue;
      }
      
      if (error.status >= 500) {
        // Server error — повторяем
        await sleep(1000);
        continue;
      }
      
      // Client error (400, 401) — не повторяем
      throw error;
    }
  }
  
  throw lastError;
}

10. ИНТЕГРАЦИЯ С ВКС

10.1. Архитектура адаптеров

Используем паттерн Strategy для поддержки разных провайдеров ВКС.

// src/integrations/vcs/base.ts
export abstract class VCSProvider {
  abstract name: string;
  abstract getTranscript(meetingId: string): Promise<VCSTranscript>;
  abstract testConnection(): Promise<boolean>;
}

export interface VCSTranscript {
  text: string;
  metadata: {
    duration?: number;
    participants?: string[];
    recordingUrl?: string;
  };
}

// src/integrations/vcs/yandex.ts
export class YandexTelemostProvider extends VCSProvider {
  name = 'yandex';
  
  constructor(private apiKey: string) {}
  
  async getTranscript(meetingId: string): Promise<VCSTranscript> {
    // API Яндекс.Телемост
    const response = await fetch(
      `https://api.telemost.yandex.net/v1/meetings/${meetingId}/transcript`,
      {
        headers: { 'Authorization': `Bearer ${this.apiKey}` }
      }
    );
    
    const data = await response.json();
    
    return {
      text: data.transcript.text,
      metadata: {
        duration: data.duration,
        participants: data.participants.map(p => p.name),
        recordingUrl: data.recording_url,
      }
    };
  }
  
  async testConnection(): Promise<boolean> {
    try {
      await fetch('https://api.telemost.yandex.net/v1/status', {
        headers: { 'Authorization': `Bearer ${this.apiKey}` }
      });
      return true;
    } catch {
      return false;
    }
  }
}

// src/integrations/vcs/registry.ts
export class VCSRegistry {
  private providers = new Map<string, VCSProvider>();
  
  register(provider: VCSProvider) {
    this.providers.set(provider.name, provider);
  }
  
  get(name: string): VCSProvider | undefined {
    return this.providers.get(name);
  }
  
  list(): string[] {
    return Array.from(this.providers.keys());
  }
}

10.2. Использование в API

// src/routes/meetings.ts
router.post('/:id/import-from-vcs', async (req, res) => {
  const { id } = req.params;
  const { vcsProvider, vcsMeetingId } = req.body;
  
  const provider = vcsRegistry.get(vcsProvider);
  if (!provider) {
    return res.status(400).json({ error: { code: 'UNKNOWN_PROVIDER', message: '...' } });
  }
  
  const transcript = await provider.getTranscript(vcsMeetingId);
  
  // Сохраняем в meeting
  await db.update(meetings)
    .set({
      rawTranscript: transcript.text,
      sourceType: 'vcs_integration',
      sourceMetadata: transcript.metadata,
    })
    .where(eq(meetings.id, id));
  
  res.json({ data: { success: true } });
});

11. БЕЗОПАСНОСТЬ

11.1. Аутентификация и авторизация

// src/middleware/auth.ts
export function authMiddleware(roles?: UserRole[]) {
  return (req, res, next) => {
    const token = req.headers.authorization?.replace('Bearer ', '');
    
    if (!token) {
      return res.status(401).json({ error: { code: 'UNAUTHORIZED', message: '...' } });
    }
    
    try {
      const payload = jwt.verify(token, process.env.JWT_SECRET) as JWTPayload;
      req.user = payload;
      
      if (roles && !roles.includes(payload.role)) {
        return res.status(403).json({ error: { code: 'FORBIDDEN', message: '...' } });
      }
      
      next();
    } catch {
      return res.status(401).json({ error: { code: 'INVALID_TOKEN', message: '...' } });
    }
  };
}

// Роли:
// - admin: полный доступ, управление настройками и пользователями
// - pmo: создание/редактирование проектов и встреч, утверждение протоколов
// - viewer: только просмотр

11.2. Валидация входных данных

// src/validators/meeting.validator.ts
import { z } from 'zod';

export const createMeetingSchema = z.object({
  projectId: z.string().uuid(),
  meetingDate: z.string().date(),
  location: z.string().max(200).optional(),
  type: z.enum(['standard', 'op_council', 'steering_council']),
  sourceType: z.enum(['audio', 'text', 'vcs_integration']),
  rawTranscript: z.string().optional(),
});

export function validate(schema: z.ZodSchema) {
  return (req, res, next) => {
    try {
      req.body = schema.parse(req.body);
      next();
    } catch (error) {
      if (error instanceof z.ZodError) {
        return res.status(400).json({
          error: {
            code: 'VALIDATION_ERROR',
            message: 'Invalid input',
            details: error.errors
          }
        });
      }
      next(error);
    }
  };
}

11.3. Защита от инъекций и XSS

  • SQL-инъекции: Используем Drizzle ORM с параметризованными запросами (никакого raw SQL с конкатенацией)
  • XSS: Все пользовательские данные экранируются на фронте (Vue автоматически экранирует интерполяции)
  • Markdown: Используем DOMPurify для санитизации HTML, сгенерированного из Markdown
  • File uploads: Ограничение по типу (только .mp3, .wav, .txt, .eml) и размеру (до 100MB)

11.4. Rate Limiting

// src/middleware/rateLimit.ts
import rateLimit from 'express-rate-limit';

export const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 минут
  max: 100, // 100 запросов с одного IP
  message: { error: { code: 'RATE_LIMIT', message: 'Too many requests' } }
});

export const aiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 минута
  max: 10, // 10 AI-запросов в минуту (дорогая операция)
});

11.5. Логирование и аудит

Все критичные действия логируются в таблицу audit_log:

  • Создание/удаление проектов
  • Утверждение протоколов
  • Изменение настроек промптов
  • Попытки несанкционированного доступа

12. ПЛАН РАЗРАБОТКИ (ROADMAP)

Этап 1: Фундамент (2 недели)

Неделя 1:

  • Настройка проекта (monorepo: backend + frontend)
  • Docker Compose (Postgres + pgvector + backend + frontend)
  • Drizzle ORM + миграции (все таблицы)
  • Auth (login, JWT, middleware)
  • CRUD для Projects и Meetings (API + фронт)
  • Базовый UI: таблицы, формы, роутинг

Неделя 2:

  • Карточка проекта (вкладки, статистика)
  • Карточка встречи (вкладка "Исходник")
  • Markdown редактор (md-editor-v3)
  • Экспорт в DOCX (базовый, без ИИ)
  • Интеграция с одной ВКС (Яндекс.Телемост)

Этап 2: ИИ и генерация (2 недели)

Неделя 3:

  • Интеграция OpenAI SDK
  • Транскрибация аудио (синхронно через Whisper API)
  • Генерация универсального резюме
  • Генерация протокола обычной встречи
  • AI-чат (SSE стриминг)

Неделя 4:

  • Генерация протоколов опер. и управляющего советов (RAG)
  • AI-статус проекта
  • Диаграмма Ганта (Mermaid.js)
  • Управление промптами через админку

Этап 3: Векторный поиск и полировка (1 неделя)

Неделя 5:

  • Модуль векторизации (OpenAI + локальная модель)
  • Векторный поиск по встречам
  • UI/UX полировка (анимации, dark mode, адаптив)
  • Обработка ошибок, тосты, лоадеры
  • Документация (README, API docs)

Этап 4: Тестирование и релиз (1 неделя)

Неделя 6:

  • E2E тесты (Playwright)
  • Нагрузочное тестирование
  • Security audit
  • Deployment на prod
  • Обучение пользователей

Итого: 6 недель до MVP


13. DOCKER COMPOSE (ФИНАЛЬНАЯ КОНФИГУРАЦИЯ)

# docker-compose.yml
version: '3.8'

services:
  postgres:
    image: pgvector/pgvector:pg15
    environment:
      POSTGRES_DB: pmo_ai
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    environment:
      - NODE_ENV=production
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_NAME=pmo_ai
      - DB_USER=${DB_USER}
      - DB_PASSWORD=${DB_PASSWORD}
      - JWT_SECRET=${JWT_SECRET}
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - OPENAI_BASE_URL=${OPENAI_BASE_URL}
      - EMBEDDING_MODE=${EMBEDDING_MODE:-openai}
    volumes:
      - ./models:/app/models # для локальных моделей
      - uploads:/app/uploads
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_healthy

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
      args:
        - VITE_API_URL=/api
    ports:
      - "80:80"
    depends_on:
      - backend

volumes:
  postgres_data:
  uploads:

14. ЗАКЛЮЧЕНИЕ

Мы спроектировали современную, масштабируемую архитектуру для PMO AI Assistant, которая:

Решает бизнес-задачу — автоматизация 80% рутинной работы проектного офиса
Использует лучшие практики 2026 — PostgreSQL + pgvector, Vue 3 Composition API, SSE для стриминга
Гибкая к изменениям — JSONB "подушки безопасности", редактируемые промпты
Два режима векторизации — облачный (OpenAI) и локальный (transformers) для работы в закрытом контуре
Современный UI — Bento Grid, dark mode, плавные анимации, AI-native дизайн
Безопасная — JWT auth, RBAC, rate limiting, аудит
Готова к масштабированию — модульная архитектура, легко добавить новые ВКС, новые типы протоколов

Следующие шаги:

  1. Утвердить ТЗ с заказчиком
  2. Настроить репозиторий и CI/CD
  3. Начать Этап 1 (Фундамент)