projects/pr_office.md

1724 lines
66 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ТЕХНИЧЕСКОЕ ЗАДАНИЕ: 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` (Пользователи)
```sql
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` (Проекты)
```sql
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` (Встречи)
```sql
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` (Шаблоны промптов)
```sql
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` (Журнал действий)
```sql
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`
```sql
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)
**Цветовая схема:**
```css
/* 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`, который имеет две реализации. Это позволяет переключать режимы через настройки без изменения бизнес-логики.
```typescript
// 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)
**Реализация:**
```typescript
// 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
- Готовы пожертвовать немного качества
**Реализация:**
```typescript
// 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. **Предзагрузка моделей:**
```bash
# Скрипт для предзагрузки модели в 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. Переключение режимов через настройки
```typescript
// 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": "..."}
```
**Реализация на бэкенде:**
```typescript
// 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();
});
```
**Реализация на фронте:**
```typescript
// 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)
Перед генерацией протокола бэкенд собирает контекст:
```typescript
// 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. Обработка ошибок ИИ
```typescript
// 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** для поддержки разных провайдеров ВКС.
```typescript
// 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
```typescript
// 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. Аутентификация и авторизация
```typescript
// 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. Валидация входных данных
```typescript
// 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
```typescript
// 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 (ФИНАЛЬНАЯ КОНФИГУРАЦИЯ)
```yaml
# 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 (Фундамент)