66 KiB
ТЕХНИЧЕСКОЕ ЗАДАНИЕ: 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)
Назначение: Детальная информация о проекте, история встреч, визуализация.
Вкладки:
- Обзор:
- Карточка проекта (все поля, редактируемые)
- AI-статус (большой блок, кнопка "Обновить статус")
- Диаграмма Ганта (Mermaid.js, интерактивная)
- Статистика: всего встреч, активных мероприятий, просроченных
- Встречи:
- Таблица встреч этого проекта (отфильтрованная)
- Кнопка "+ Новая встреча"
- Мероприятия:
- Сводная таблица всех мероприятий по проекту
- Фильтры: статус, ответственный, срок
- Экспорт в Excel
Таблица встреч (/meetings)
Назначение: Управление всеми встречами, семантический поиск.
Элементы:
- Таблица с колонками:
- Дата
- Проект (ссылка)
- Тип встречи (badge)
- Место
- Ответственный
- Краткое резюме (первые 100 символов)
- Статус протокола
- Панель фильтров:
- Векторный поиск (текстовое поле: "О чем были встречи?")
- Фильтр по проекту
- Фильтр по дате (range picker)
- Фильтр по ответственному
- Фильтр по типу встречи
- Кнопка "+ Новая встреча"
Карточка встречи (/meetings/:id)
Назначение: Основная рабочая область — загрузка исходников, генерация и редактирование протокола.
Вкладки:
- Исходник:
- Если аудио: плеер + кнопка "Транскрибировать" (показывает прогресс)
- Если текст: редактор транскрибации (textarea с подсветкой синтаксиса)
- Кнопка "Сохранить исходник"
- Резюме:
- Блок с универсальным резюме (генерируется автоматически)
- Кнопка "✏️ Редактировать" (переключает в режим редактирования)
- Кнопка "Перегенерировать"
- Протокол:
- Три кнопки генерации (с иконками):
- 📋 Протокол встречи
- ⚡ Протокол оперативного совета
- 👔 Протокол управляющего совета
- Markdown редактор (md-editor-v3):
- Split view (редактор + превью)
- Панель инструментов (жирный, списки, таблицы)
- Возможность вставки таблиц (для перечня мероприятий)
- AI-чат (справа, collapsible):
- Выделяешь текст в протоколе → пишешь в чат: "Сделай более формальным"
- ИИ стримит ответ, можно принять или отклонить
- Кнопки действий:
- 💾 Сохранить черновик
- ✅ Утвердить протокол (триггерит обновление статуса проекта)
- 📥 Экспорт в DOCX
- Три кнопки генерации (с иконками):
Настройки (/settings)
Вкладки:
- Проекты: Управление справочником проектов (CRUD)
- Пользователи: Управление доступами (роли: admin, pmo, viewer)
- Промпты: Редактирование системных промптов для генерации протоколов
- Интеграции: Настройка API-ключей для ВКС (MTS Link, Bitrix24, Яндекс.Телемост)
- Векторизация: Выбор режима (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), запуск pm2frontendраздаётся из dist через express static
- Переменные окружения:
.envфайл (ключи API, настройки БД)
4.4. Deployment
- Frontend собирается через Vite в папку
dist - раздаётся из dist через express static
- PostgreSQL работает в отдельном контейнере с volume для данных. настраивается через .env
5. МОДЕЛЬ ДАННЫХ (PostgreSQL)
5.1. Принципы проектирования
- Реляционное ядро — поля, по которым часто фильтруем/сортируем/делаем JOIN
- JSONB для гибкости — "подушки безопасности" для будущих изменений
- Векторные поля — для семантического поиска
- Аудит —
created_at,updated_atво всех таблицах - 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;
}
}
}
Важные нюансы локальной модели:
-
Выбор модели:
- all-MiniLM-L6-v2 (384 dim) — быстрая, легкая (~80MB), хорошее качество для английского
- paraphrase-multilingual-MiniLM-L12-v2 (384 dim) — мультиязычная, хорошо работает с русским
- sbert_large_nlu_ru (1024 dim) — специализированная русская модель, лучшее качество для русского, но тяжелее (~1.3GB)
-
Предзагрузка моделей:
# Скрипт для предзагрузки модели в docker-образ # scripts/download-models.js const { downloadFile } = require('@xenova/transformers'); async function downloadModels() { await downloadFile('Xenova/paraphrase-multilingual-MiniLM-L12-v2'); } downloadModels(); -
Производительность:
- CPU: ~50-100ms на один текст (зависит от длины)
- RAM: ~500MB-1.5GB (зависит от модели)
- Для batch из 100 текстов: ~5-10 секунд
-
Переменная размерность:
- При смене модели нужно пересоздать все эмбеддинги в БД
- Поле
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, аудит
✅ Готова к масштабированию — модульная архитектура, легко добавить новые ВКС, новые типы протоколов
Следующие шаги:
- Утвердить ТЗ с заказчиком
- Настроить репозиторий и CI/CD
- Начать Этап 1 (Фундамент)