Skip to content

AI-помощник ​

Помощник подключается к внешней языковой модели по ключу, который администратор вводит в настройках. По ключу система сама определяет провайдера (DeepSeek, OpenAI или Anthropic) и список доступных моделей. Ключ хранится только зашифрованным и никогда не возвращается в API.

Этап 4 (реквизиты из файла, поиск по смыслу, голосовой ввод) — разделы «Реквизиты из файла», «Поиск по смыслу» и «Голосовой ввод».

Этап 3 (утренняя сводка руководителям, вкладка «Расход», вопрос помощнику из поиска ⌘K, аудит каждого вызова) — разделы «Утренняя сводка», «Расход» и «Вопрос из поиска».

Этап 1 (панель «Помощник», беседы, ответ потоком, инструменты на чтение) — раздел «Панель помощника» ниже.

Реализовано, этап 0 ТЗ (ai-assistant-spec.md): пакет internal/ai (провайдеры, определение по ключу, справочник моделей, шифрование), модуль internal/modules/assistant (настройки, проверка подключения, журнал расхода), миграция 0157 (таблицы ai_settings, ai_usage, код ai.manage), экран «Администрирование → AI-помощник» во фронтенде. Кнопки помощника в карточках — раздел «Действия в карточках».


Схема ​

Админ: "Вставляю ключ, жму «Проверить подключение»"
    ↓
Система: "Ключ sk-ant-… → Anthropic; иначе GET /models у DeepSeek, затем у OpenAI"
    ↓
Провайдер: "200 — вот модели" | "401 — ключ не мой"
    ↓
Система: "Короткий запрос к модели без рассуждения: «Ответь одним словом: готов»"
    ↓
Админ видит: провайдер, модели с контекстом и ценами, время ответа, ответ модели
    ↓
Админ: "Сохранить" → ключ проверяется ещё раз, шифруется AES-GCM, пишется в ai_settings
    ↓
Аудит: ai.settings_updated (категория security) — какие поля изменились, без значения ключа

Хранение ключа ​

ЧтоГде
Ключ провайдераai_settings.api_key_enc — AES-256-GCM, первый байт — версия схемы, затем nonce и шифротекст
Мастер-ключпеременная окружения APP_SECRETS_KEY (не короче 32 символов), только на сервере
Подсказка для интерфейсаai_settings.key_hint — префикс и 4 последних символа, например sk-…4d49

Без APP_SECRETS_KEY ключ проверить можно, сохранить нельзя (409 ai_secrets_not_configured). Если мастер-ключ сменили, сохранённый ключ перестаёт читаться: key_readable=false, помощник выключается, страница просит ввести ключ заново. Дамп базы без мастер-ключа ключ провайдера не раскрывает.

Ключ не попадает в ответы API, журнал аудита и логи: адаптеры провайдеров заменяют его подсказкой в текстах ошибок (redact).


Определение провайдера ​

Ключ или настройкаКандидаты
Провайдер выбран явнотолько он
sk-ant-…Anthropic
Задан свой адресDeepSeek, если в адресе есть deepseek; Anthropic — если anthropic; иначе OpenAI-совместимый шлюз
sk-proj-…, sk-svcacct-…OpenAI
Прочие sk-…DeepSeek, затем OpenAI

Кандидат подтверждается ответом GET /models. Отказ в доступе (401/403) переходит к следующему кандидату. Сетевой сбой, лимит частоты или нехватка средств важнее отказа: такая ошибка возвращается как итог, потому что ключ, возможно, верный.

Свой адрес провайдера принимается только с https://. http:// разрешён для этой же машины (localhost, 127.0.0.1, host.docker.internal) — локальный шлюз или тестовый двойник.


Модели ​

Список моделей всегда берётся у провайдера. DeepSeek и Anthropic отдают контекст и лимит ответа, OpenAI — только идентификаторы (не чат-модели вроде эмбеддингов отсекаются). Справочник internal/ai/catalog.go дополняет известные модели ценами за 1 млн токенов, которых провайдеры через API не публикуют.

Модели выбираются по назначению:

НазначениеДля чегоПо умолчанию
model_chatдиалог в панели и действия в карточкахDeepSeek: deepseek-flash; Anthropic: claude-haiku-4-5
model_heavyаналитика, черновики ТЗ и сметыDeepSeek: deepseek-v4-pro; Anthropic: claude-opus-5-5

При смене ключа на ключ другого провайдера выбранные модели сбрасываются на значения по умолчанию нового провайдера.


Сбои провайдера ​

КлассHTTP провайдераТекст на странице
auth401, 403Провайдер не принял ключ
balance402На счёте провайдера недостаточно средств
rate_limit429Провайдер ограничил частоту запросов
model404Провайдер не нашёл модель
bad_requestпрочие 4xxПровайдер отклонил запрос
unavailable5xx, сеть, таймаутПровайдер не отвечает

«Проверить подключение» возвращает сбой в теле ответа (ok=false, error_code, error_message) — страница показывает его как состояние, без тоста. Сохранение с нерабочим ключом отклоняется ошибкой формы (ai_key_rejected на поле api_key), и ключ не сохраняется.

Операции с провайдером из админки ограничены 12 секундами — меньше WriteTimeout сервера (15 с).

Сбои rate_limit и unavailable при работе помощника (панель, действия, сводка, распознавание, поиск) повторяются дважды — через 1 и 3 секунды (assistant/guard.go). Поток ответа повторяется, только пока сотрудник не увидел ни одного слова; ключ, баланс и отказ в запросе не повторяются.

Защита от утечек ​

Суммы и контакты клиентов уходят модели по политике данных — по решению 2026-09-30 обе передачи включены. Секреты не уходят никогда: всё, что получает провайдер, — вопрос сотрудника, результаты инструментов, история беседы, текст документа — проходит через assistant.Redact, и найденное заменяется на «[скрыто]»:

  • пароли, ПИН- и CVV-коды, коды из СМС, токены и ключи с меткой («пароль: …», token=…); без двоеточия или знака равенства текст не трогается — «пароль администратора» остаётся словами;
  • ключи и токены по виду: sk-… (OpenAI, DeepSeek, Anthropic), JWT, Bearer …, GitHub, Slack, AWS, Google, токен Telegram-бота, блок закрытого ключа;
  • номера банковских карт — 16–19 цифр, проходящие проверку по Луну;
  • паспорт РФ («паспорт 45 10 123456», «серия … №»), СНИЛС с контрольным числом или с меткой.

Реквизиты (ИНН, КПП, ОГРН, счета, БИК), телефоны, почта и суммы не вырезаются — это рабочие данные. Сколько фрагментов вырезано, пишется в журнал сервера без самих значений. Изображения (фото и сканы для распознавания) уходят как есть: в них вырезать нечего без распознавания, а сотрудник выбирает файл сам.


API ​

Метод и путьПравоЧто делает
GET /api/v1/ai/statusлюбой сотрудниквключён ли помощник и какие функции открыты
GET /api/v1/ai/settingsai.manageнастройки без ключа
PUT /api/v1/ai/settingsai.manageсохранить целиком; api_key — только при смене, clear_key — удалить ключ
POST /api/v1/ai/settings/testai.manageпроверить ключ из формы или сохранённый
GET /api/v1/ai/usageai.manageрасход за период по дням, сотрудникам или функциям
GET /api/v1/ai/usage/summaryai.manageитоги расхода за период
POST /api/v1/ai/digest/send-meai.manageсобрать утреннюю сводку себе прямо сейчас
POST /api/v1/ai/extract/requisitesсотрудникреквизиты из файла (multipart file)
GET /api/v1/ai/search?q=сотрудникпоиск по смыслу для ⌘K

ai.manage выдан OWNER (через system.global_admin) и OPS (миграция 0157). В /org/me — capability ai_manage; по ней фронтенд показывает пункт меню и открывает маршрут /admin/ai.

Включить помощника можно только с сохранённым читаемым ключом и выбранной моделью (ai_enable_without_key).


Функции, лимиты, данные ​

  • Функции (features): панель помощника, задачи, проекты, чаты, табель, утренняя сводка, реквизиты из файла (documents), поиск по смыслу (search), «Опишите словами» на формах (forms, помощник в формах). Выключенная функция скрыта у всех.
  • Утренняя сводка (digest): час отправки 5–12 по времени компании, слать ли менеджерам активных проектов. Письмом сводка не отправляется — только в RMS.
  • Лимиты (limits): токенов в сутки на компанию и на сотрудника (0 — без ограничения), ожидание ответа 10–300 секунд.
  • Данные (data_policy): передавать ли суммы и бюджеты, контакты клиентов (по умолчанию — да). Срока хранения бесед нет: история хранится, пока сотрудник её не удалит (поле history_days прежней версии интерфейса принимается и не используется).

Журнал расхода ​

Каждый вызов модели — строка ai_usage: пользователь, функция (admin_test для проверки подключения), провайдер, модель, токены запроса, из кэша и ответа, время, итог и класс ошибки. По журналу считаются дневные лимиты и вкладка «Расход».

Каждый вызов модели пишет в аудит событие ai.request (категория business): функция, провайдер, модель, токены, время, итог, для панели — номер беседы. Текст вопроса и ответа в аудит не попадает. Проверка подключения пишет своё событие ai.connection_tested.


Панель помощника ​

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

Кнопка-искра в шапке и клавиши ⌘J / Ctrl+J открывают правую панель. Кнопка видна, только когда помощник включён и функция «Панель „Помощник“» не выключена (GET /ai/status). Помощник знает, какая страница открыта: на карточке проекта «этот проект» — он, в карточке задачи — эта задача, в чате — этот чат.

Сотрудник: "Что с этим проектом, есть риски?"  (страница /projects/29)
    ↓
POST /ai/conversations/:id/messages {content, page}
    ↓
Сервер: помощник включён? лимит не исчерпан? — иначе обычный JSON 409
    ↓
Модель (deepseek-flash, без рассуждения) → вызывает get_project {project_id: 29}
    ↓
Инструмент: GET /api/v1/projects/29 и /projects/29/tasks — через этот же роутер,
            с токеном сотрудника → его права, его охват, деньги по матрице
    ↓
Ответ сжат: служебные поля выброшены, деньги и контакты — по политике данных
    ↓
Модель пишет ответ → event token … event done → ответ сохранён в беседе

Почему помощник видит ровно то, что сотрудник ​

Инструменты не ходят в базу. Они вызывают собственный HTTP API приложения внутри процесса (HandlerAPI) с заголовком Authorization исходного запроса. Поэтому действуют все те же проверки, что у интерфейса: соглашение о неразглашении, матрица прав, охват «свой», вырезание денежных полей по матрице. Инженер, спросивший о прибыли, получит от инструмента «нет доступа», и помощник так и ответит.

Поверх этого — политика данных из настроек: без share_money из результатов вырезаются бюджеты, выручка, затраты, суммы; без share_client_contacts — почта и телефоны.

Помощник только читает. Изменить что-либо (создать задачу, записать часы) он не может — таких инструментов нет.

Инструменты ​

ИнструментОткуда данныеЧто получает модель
searchGET /searchнайденные компании, контакты, проекты, задачи, сотрудники со ссылками
list_tasksGET /tasks (+ /projects для названий)задачи сотрудника или проекта; фильтр «просрочены / сегодня / неделя / без срока» считается в часовом поясе компании
get_taskGET /tasks/:id, /tasks/:id/checklist, /tasks/:id/subtasks, /tasks/:id/commentsкарточка с началом, чек-листом, подзадачами, связями «ждёт / следом» и 10 последних комментариев
list_projectsGET /projectsпроекты с поиском и статусом
get_projectGET /projects/:id, /projects/:id/tasksкарточка и сводка задач: всего, завершено, просрочено, без ответственного, без срока
list_chatsGET /chatsчаты с числом непрочитанных и последним сообщением
get_chat_messagesGET /chats/:id/messages, /tasks/:id/chat/messagesпоследние сообщения
my_timesheetGET /timesheets/myстатус табеля, часы по проектам, записи, корректировки
workloadGET /workloadзагрузка людей по дням: средний и пиковый процент, перегруженные дни, отсутствия, командировки, дни мероприятий; фильтры — период, роль, проект, имя (загрузка людей)
company_dashboardGET /dashboard/executiveпанель руководства, списки обрезаны до 8 строк

Статусы в результатах — подписями интерфейса («Планируется», «Сметим»). Результат одного инструмента — не больше 12 000 символов.

Ход беседы ​

  • За один вопрос модель может обратиться к инструментам до 5 раз; шестой шаг — без инструментов, чтобы ответ точно был.
  • В модель уходят вопросы и итоговые ответы беседы, без старых результатов инструментов. Пока несвёрнутых реплик не больше 20 — все; больше — начало беседы, кроме 10 последних реплик, сворачивается моделью «Диалог» в краткое содержание (ai_conversations.summary, до сообщения summary_upto, миграция 0162). Модель получает «Ранее в беседе…» и последние реплики; свёртка дополняется, когда снова наберётся 20. Не удалось свернуть — уходят 20 последних реплик. Свёртка — строка ai_usage с функцией history_summary. Весь текст истории — не больше ~150 тыс. знаков.
  • Системный промпт разделён: стабильная часть (правила, инструменты) одинакова для всех и кэшируется провайдером; изменчивая — дата, сотрудник, открытая страница — идёт после неё.
  • Каждое обращение к модели — строка ai_usage с функцией assistant. Дневные лимиты считаются по ней до начала ответа (409 ai_daily_limit_org / ai_daily_limit_user).
  • Ожидание ответа модели — limits.request_timeout_sec на каждый шаг. WriteTimeout сервера (15 с) для потока снимается через http.ResponseController, заголовок X-Accel-Buffering: no отключает буферизацию в nginx-шлюзе.

Беседы ​

Таблицы ai_conversations и ai_messages (миграция 0158). Беседа принадлежит одному сотруднику, чужие не видит никто. Хранятся вопросы, ответы, вызовы инструментов и их результаты — по ним видно, на каких данных построен ответ. История хранится бессрочно, пока сотрудник не удалит беседу; удаление — настоящее (сообщения удаляются каскадом, в журнале расхода остаются только цифры), в интерфейсе — с подтверждением.

Метод и путьЧто делает
GET /api/v1/ai/conversationsмои беседы, свежие первыми
POST /api/v1/ai/conversationsначать беседу (context_type, context_id — страница)
GET /api/v1/ai/conversations/:id/messagesвопросы и ответы с подписями шагов
POST /api/v1/ai/conversations/:id/messagesвопрос; ответ — поток text/event-stream
DELETE /api/v1/ai/conversations/:idудалить беседу вместе с сообщениями

События потока: token {text} — кусок ответа; tool {name, label} — помощник смотрит данные («Открываю проект №29»); done {message} — ответ сохранён; error {code, message} — сбой провайдера.

Интерфейс ​

  • src/components/ai/AiAssistantPanel.vue — панель; useAiAssistant — общее состояние; src/api/ai/stream.ts — чтение потока через fetch с тем же токеном, что у apiFetch.
  • Ответ рендерит AppMarkdown — разбор Markdown в узлы Vue без v-html: HTML из ответа модели выводится текстом. Ссылки на пути приложения открываются роутером, внешние — в новой вкладке, прочие схемы — просто текст.
  • Отказ до начала ответа (выключен, лимит) показывается строкой в панели, вопрос возвращается в поле ввода. Кнопка «Остановить» обрывает поток; что успело прийти, сохраняется.
  • Открыть панель: кнопка в шапке, ⌘J / Ctrl+J и плавающая кнопка над кнопкой чата (кроме телефона и раздела «Чаты»).
  • e2e e2e/ai-assistant.spec.ts во фронте: администратор задаёт ключ заглушки провайдера (OpenAI-совместимый сервер на машине теста, адрес http://host.docker.internal:…), проверяет и включает помощника; сотрудник спрашивает — пароль из вопроса до провайдера не доходит; ключ удалён — кнопок нет. Тест перезаписывает ключ, поэтому запускается только с E2E_AI_MOCK=1.

Действия в карточках ​

Одношаговые действия (этап 2): кнопка в карточке — готовый результат через 1–3 секунды. В отличие от панели модель здесь не выбирает инструменты: данные собирает код — теми же инструментами, через тот же API от имени сотрудника, — а модель только формулирует ответ одним JSON-объектом по схеме. Результат — предложение: он показывается или подставляется в поля, сохраняет сотрудник сам.

ГдеКнопкаactionДанныеРезультатФункция в настройках
Карточка задачи (длинное обсуждение)«Кратко о задаче»task_summaryзадача, комментарии, чат задачисуть, что ждут от вас, открытые вопросыЗадачи
Новая задача«Сформулировать»task_formulateнабранный текст, проектназвание, описание с шагами, план часов — в поля формыЗадачи
Обзор проекта«Сводка помощника»project_summaryкарточка, сводка задач, чат проектагде проект, риски, ближайшие шагиПроекты (модель «Аналитика»)
Чат (от 10 непрочитанных)«Что я пропустил»chat_missedнепрочитанные и последние сообщенияглавное, решения, вопросы к вам, срокиЧаты
Поле ввода чата✦ → «Помочь с ответом» / «Дописать ответ»reply_draftпоследние сообщения, набранный черновиктекст в поле вводаЧаты
Поле ввода чата✦ → «Короче» / «Официальнее» / «Мягче»rephraseнабранный тексттекст в поле вводаЧаты
Новая версия ТЗ«Заполнить черновик»spec_draftбриф из полей, прошлая версия ТЗ, карточка, чат проектацель, формат и площадка, оборудование, монтаж, особые требования — в поля формыПроекты (модель «Аналитика»)
Смета по ТЗ«Предложить строки»estimate_rowsверсия ТЗ, карточка, уже заведённые строкиработы с часами по статьям — в черновик сметыПроекты (модель «Аналитика»)
Мой табель«Заполнить по задачам»timesheet_fillпустые рабочие дни месяца до сегодня, свои задачи, записи месяцазаписи часов с отметками; добавляет сотрудник обычным POST /time-entries/selfТабель
Корректировка табеля«Сформулировать»correction_reasonвыбранная работа, дата, часы, черновикпричина для проверяющего — в полеТабель
Неоплаченный счёт («Продажи», документы проекта)«Напомнить об оплате» / «Письмо о долге»invoice_reminderстрока реестра документов по счёту (номер, дата, итог, оплачено, остаток, просрочка — готовыми строками), руководитель клиентатема и текст письма; тон style: обычно / softer / formal; отправляет сотрудник из своей почтыПроекты
Обзор сданного или закрытого проекта«Итоги проекта»project_reportкарточка и задачи, план и факт (если суммы открыты) — готовыми строками, перерасходы, незакрытое (close-check), чат проектачто сделано, деньги, что пошло не так, выводы на следующий проект — черновик отчёта, «Скопировать»Проекты (модель «Аналитика»)
Любая форма«Опишите словами» + микрофон → «Заполнить»form_fillсхема полей формы, справочники формы, контекст страницы, слова сотрудниказначения полей с «почему» — подсвечены в форме (подробнее)Формы

В письме о долге и итогах проекта суммы, даты и проценты считает код и передаёт модели строками («1 200 000,50 ₽», «04.10.2026»): модель только переносит их в текст и не вычисляет разницы и доли. Оплаченный счёт — 400 ai_action_input; план и факт без права на суммы проекта в итоги не попадают.

«Заполнить по задачам» проверяет ответ модели кодом: только пустые рабочие дни, только задачи сотрудника, не больше 8 часов в день. Сданный месяц — 409 ai_timesheet_closed, заполнять нечего — 409 ai_timesheet_nothing_to_fill.

POST /api/v1/ai/actions/:action → {action, model, result, generated_at, cached}. Сводка проекта ложится в историю проекта (ai_project_summaries, миграция 0164; ai_action_cache из 0162 удалена): в обзоре проекта последняя сводка раскрыта с датой и автором, прежние свёрнуты списком с датами (GET /ai/projects/:id/summaries, до 20). Сводку видит каждый, кто видит проект; составленную с суммами — только те, кому суммы проекта открыты (сервер проверяет доступ запросом карточки проекта от имени сотрудника). Повторный запрос в течение часа отдаёт последнюю видимую сводку (cached = true), «Обновить» передаёт refresh = true и добавляет новую. Кнопки видны, только когда помощник включён и функция не выключена (GET /ai/status).

  • Ответ не по схеме — одна повторная попытка с напоминанием; снова не по схеме — 409 ai_action_bad_answer.
  • Нет доступа к данным — 403 ai_action_forbidden; функция выключена — 409 ai_feature_off; сбой провайдера — 409 ai_action_failed / ai_action_provider.
  • Ошибки показываются строкой рядом с кнопкой, без тостов.
  • Каждый вызов — строка ai_usage с именем действия в feature.

Предложения в панели ​

Помощник может предложить изменить задачу — назначить ответственного и/или перенести срок (инструмент propose_task_changes). Сервер ничего не меняет: в поток уходит событие proposal {id, task_id, project_id, task_title, assignee_id?, assignee_name?, due_date?, reason}, в ответе появляется карточка с кнопками «Применить» и «Не нужно». «Применить» отправляет обычный PATCH /tasks/:id с правами сотрудника — правила сроков и прав проверяет модуль задач, отказ показывается на карточке.

Предложение готовится, только если сотрудник может править задачу (can_edit); ответственный ищется в справочнике сотрудников. Предложения сохраняются вместе с ответом (ai_messages.proposals, миграция 0159) и видны при повторном открытии беседы.


Утренняя сводка ​

По будням в назначенный час (по умолчанию 8:00 по времени компании) руководство получает сводку рисков. Получатели — активные сотрудники с ролью OWNER, OPS, DIV_HEAD (и старыми admin, director, head_of_department), а если включено — менеджеры проектов в статусах «Подтверждён» и «В работе». Функция включается переключателем «Утренняя сводка» и по умолчанию выключена.

Факты собирает код, модель только пишет. Воркер работает в процессе API: ему нужен роутер, через который читаются данные. Для каждого получателя он выпускает короткоживущий токен доступа (только в памяти) и читает API от его имени — с его правами, охватом и вырезанием денег:

  • задачи в статусах «Планируется», «В работе», «На паузе» — просроченные, со сроком сегодня, без ответственного (по 200 на статус; больше — счётчики «не меньше»);
  • табели прошлого месяца — сданные ждут решения, черновики не сданы, корректировки ждут решения; только если матрица открывает получателю чужие табели;
  • открытые обязательства в статусах «Просрочено» и «Срок близко», без сумм.

Раздел, на который нет прав, пропускается. Рисков нет — сводка не отправляется (статус empty). Модели уходит JSON фактов, в ответ — короткий текст со ссылками на задачи. Сбой провайдера, пустой ответ или исчерпанный лимит — уходит тот же список фактов без модели.

Куда приходит. Сводка сохраняется беседой помощника (context_type = digest, заголовок «Сводка на 30 сентября»), уведомление ai.digest в колокольчике показывает счётчики и открывает беседу в панели по ссылке /?aiConversation=N — руководитель сразу может спросить подробности. Письмом сводка не отправляется (решение 2026-09-30).

Один раз в день. Таблица ai_digests (миграция 0160) — строка на получателя и день. Её занимает процесс, который отправляет (INSERT … ON CONFLICT DO NOTHING), поэтому несколько экземпляров API не задублируют сводку. Воркер проверяет раз в 5 минут и шлёт в окне четырёх часов после назначенного — после перезапуска сервера утром сводка всё равно уйдёт, но не вечером.

Проверка. «Прислать мне сейчас» в «Функциях и лимитах» (POST /ai/digest/send-me) собирает сводку администратору с его правами — можно посмотреть до включения рассылки. В учёт дня не идёт.

Расход пишется в ai_usage с функцией digest на получателя.


Расход ​

Вкладка «Расход» на странице «AI-помощник» (право ai.manage): период (этот месяц, прошлый, 30 или 90 дней), плитки — запросы и ошибки, токены и доля из кэша, оценка стоимости, сколько сотрудников обращались; таблица по функциям, сотрудникам или дням и выгрузка CSV.

Стоимость — оценка по справочнику цен моделей (internal/ai/catalog.go, доллары за 1 млн токенов): вход без кэша, вход из кэша и ответ считаются по своим ценам. Это прикидка, а не учётные деньги — в money.Money она не попадает. Модель без цены в справочнике даёт cost_complete = false, и интерфейс пишет «+» после суммы. Дни режутся по часовому поясу компании; период — не длиннее года.


Вопрос из поиска ​

В глобальном поиске (⌘K / Ctrl+K), если запрос похож на вопрос — заканчивается «?», начинается со «что», «как», «сколько», «покажи» и т. п. или длиннее четырёх слов, — первой строкой появляется «Спросить помощника: «…»». Она открывает панель и задаёт вопрос в новой беседе с контекстом текущей страницы. Строка видна, только когда панель помощника включена.


Реквизиты из файла ​

Кнопка «Распознать из файла» — на первом шаге «Новой компании» рядом с поиском по ИНН и в форме реквизитов нашей компании. Подходят фото и сканы (JPEG, PNG, WebP), PDF, DOCX и TXT до 10 МБ: карточка предприятия, договор, счёт, письмо.

Как читается файл (internal/modules/assistant/docread.go). Вид файла определяется по содержимому, не по расширению:

  • изображение уходит модели картинкой — нужна модель, которая видит изображения (Model.Vision): модель диалога, если видит, иначе первая такая у провайдера (deepseek-flash видит, deepseek-v4-pro — нет); таких нет — 409 ai_extract_no_vision;
  • PDF с текстовым слоем читается pdftotext; скан без текста — первые 3 страницы картинками через pdftoppm (150 dpi). Утилиты poppler стоят в образе сервера (Dockerfile, пакет poppler-utils); без них PDF не принимается (409 ai_extract_pdf_unavailable), остальное работает;
  • DOCX — текст абзацев из word/document.xml, без новых зависимостей;
  • текст модели — не длиннее 30 000 символов.

Модель только переписывает. Ей велено взять реквизиты одной организации — в договоре той стороны, что не RMS Group, — и не угадывать цифры. Дальше работает код (extract.go):

  • номера приводятся к цифрам (в КПП допустимы латинские буквы в 5–6 позиции);
  • проверки: ИНН (10 и 12 цифр) и ОГРН/ОГРНИП — по контрольным цифрам (internal/legalid), КПП — по формату, расчётный и корреспондентский счёт — по ключу с БИК (internal/bankaccount);
  • если на сервере подключена DaData (DADATA_API_KEY и DADATA_SECRET_KEY), компания сверяется с ЕГРЮЛ по ИНН, банк — по БИК. Пустые поля дополняются из реестра (from_registry), расхождения КПП, ОГРН и корр. счёта, недействующая компания, ИНН или БИК, которых нет в реестре, — предупреждения.

Результат — предложение (AI-P2): форма заполняется, ничего не сохраняется. Итог приходит уведомлением («прочитаны с изображения, сверены с ЕГРЮЛ»), ошибки и предупреждения подсвечивают сами поля формы — без плашек над ней. В «Новой компании» распознанный и прошедший проверку расчётный счёт добавляется к компании после сохранения (основным).

Расход — функция requisites_extract. Скан из трёх страниц у DeepSeek — около 3 секунд, текстовый PDF — около 1,5.


Разбор ТЗ клиента ​

«Новая версия ТЗ» → «Разбор ТЗ помощником» (функция «Проекты»). Менеджер выбирает файл — загруженный или уже лежащий в проекте — и/или вставляет текст письма клиента, нажимает «Разобрать ТЗ»:

  1. POST /api/v1/ai/specs/parse (multipart: file или file_id, text, project_id). Файл проекта сервер читает сам с правами сотрудника (files.ReadContent): ссылку хранилища браузер может не открыть. Документ читается тем же механизмом, что реквизиты: PDF с текстом, скан — первые 3 страницы картинками, DOCX, XLSX (листы строками, ячейки через табуляцию), фото. Текст документа разбирает модель «Аналитика», сканы — модель, которая видит изображения.
  2. Ответ — разбор для менеджера: summary, event (цель, формат, площадка, даты, гости, монтаж), program, equipment, staff, logistics, travel, requirements, risks и questions — конкретные вопросы клиенту, которые копируются одним текстом в письмо. Ответ модели разбирается терпимо: числа строкой («4 шт»), списки строкой — всё принимается.
  3. Сверка со складом: каталог оборудования читается через API с правами сотрудника, предварительно отбираются позиции с общими словами (до 300), модель «Диалог» сопоставляет позиции ТЗ с каталогом; код проверяет id и считает «есть N» / «не хватает N из M». Услуги (трансляция, запись) и чего нет на складе — «нет в каталоге». Сбой сверки разбор не ломает.
  4. Разбор заполняет поля формы (form: цель, формат и площадка, оснащение, монтаж, требования) и сохраняется в meta.ai_parse версии ТЗ — карточка «Разбор ТЗ» видна в версии и потом.
  5. Файл «ТЗ для команды» — POST /api/v1/ai/specs/brief (project_name, parse): DOCX без внешних библиотек (ZIP с WordprocessingML) — сводка, мероприятие, программа и оборудование таблицами со сверкой по складу, персонал, логистика, требования, риски, вопросы клиенту.

Расход — функция spec_parse в журнале: разбор и сверка — две строки. Проверено на ТЗ форума на 350 гостей в Word: разбор за 12–15 секунд, 13 позиций оборудования, 9 вопросов клиенту.

Поиск по смыслу ​

Эмбеддингов у DeepSeek нет (/embeddings отвечает 404), поэтому семантический поиск сделан без векторов (smartsearch.go). Модель превращает запрос в до 6 коротких поисковых фраз — основы слов без окончаний, синонимы и профессиональные термины («экран» → «LED», «светодиод»), другое написание, аббревиатуры, — а обычный /search ищет по каждой от имени сотрудника, с его правами. Результаты сливаются: выше найденное по большему числу фраз, до 5 в разделе. Запрос из одного-двух слов идёт первой фразой как есть.

В ⌘K раздел «Похоже по смыслу» появляется через 0,7 с после паузы в наборе (от 3 символов) и показывает только то, чего нет в прямых результатах. Пока модель думает — строка «Подбираю похожие слова…». Около 1 секунды и нескольких сотен токенов на запрос; расход — функция smart_search.


Голосовой ввод ​

Кнопка микрофона — в поле вопроса панели помощника и в поле сообщения чата. Распознавание речи — браузерное (Web Speech API, ru-RU): Chrome, Edge, Safari; сервер RMS и провайдер AI в этом не участвуют (Chrome отправляет звук в сервис Google). Где распознавания нет (Firefox), кнопки нет. Надиктованное дописывается к тексту поля и не отправляется само. Диктовка в чатах работает независимо от включения помощника.

Загружаем документацию…