42 KiB
Бизнес-постановка: Обмен сообщениями (Chat / Dialog)
Статус: v1 — консолидация принятых решений из HAN_chat_specification (arch-00…arch-05, module-01, module-05, module-06); открытые вопросы зафиксированы в §17
Продукт: HAN Chat (клиентское приложение + api-backend + message-safety + bitrix-local-app / Open Lines)
Источники: архитектура HAN_chat_specification; макет Figma (не канон — только визуализация; при расхождении приоритет у этого ТЗ и arch-документов)
Связанный backlog: непрочитанные сообщения на кнопке «Чат» (синхронизация между устройствами); UX ошибки отложенного сообщения после входа
Смежно: пользователь (auth/bootstrap), уведомления (send_chat_message, кнопки Чат/Оператор), популярные вопросы, Message Safety
Нормативная часть — §1–§16. §17 — ненормативный журнал решений и открытых вопросов; при расхождении с §1–§16 приоритет у §1–§16. При расхождении этого документа с arch/module после их обновления — приоритет у arch/module до синхронизации.
1. Цель
Дать клиенту канал диалога с оператором компании: отправка текста и файлов из приложения в Bitrix24 Open Lines и получение ответов оператора в реальном времени — с проверкой исходящих сообщений (Message Safety), идемпотентностью, ownership и деградацией без потери уже принятых данных.
Чат — канал «клиент ↔ оператор». Уведомления — канал «компания → клиент» вне чата (см. notification-requirements.md). Непрочитанные ответы оператора не моделируются как уведомления; индикатор непрочитанного чата — отдельная фича (§3.2, §17.2).
2. Два направления обмена
| Направление | sender_type |
Кто инициирует | Проверка Message Safety | Доставка |
|---|---|---|---|---|
| C→O. Клиент → оператор | client |
Frontend (JWT) | Обязательна до Open Lines | api-backend → bitrix-local-app → Open Lines |
| O→C. Оператор → клиент | company |
Bitrix24 webhook → bitrix-local-app → inbox API |
Нет Message Safety/AV; только MIME/size, residual risk принят | App DB + WS / polling |
Правила:
- Чат доступен только авторизованному клиенту (JWT +
bootstrap). Гость инициирует auth; текст сохраняется локально и отправляется после входа (§6.4). - У пользователя не более одного активного диалога (
open|waiting_for_company|waiting_for_client). dialog_idприложения равенexternal_chat_idдля Open Lines.- MVP: одно исходящее сообщение — либо текст, либо ровно один файл (
content_kind), не оба сразу. - Клиент на
POST .../messagesполучает только финальный результат (или ошибку инфраструктуры), не промежуточное «обрабатывается». - Источник истины ленты — App DB; WS — at-most-once best effort; после reconnect — REST reconcile.
3. Границы релиза
3.1. В scope
- Один активный диалог на пользователя; ленивое создание перед первым сообщением.
- Исходящие:
content_kindtext|file; входящие: текст и файлы оператора. - Orchestration Message Safety: sync check + sync-wait poll
task_id+ checkpointsafety_tasks+ recovery. - Presigned upload клиента в S3-quarantine → complete → safety → promote в S3-data attachments.
- Durable outbox App → Open Lines; idempotent delivery по
message_id. - Inbox Open Lines → App:
message.new,dialog.closed; idempotency по(external_chat_id, bitrix_message_id)/event_id. - Realtime
WS /api/v1/realtime+ polling fallback истории сообщений. - Популярные вопросы как обычная отправка текста после auth.
- Общий механизм отложенного сообщения на frontend (ручной ввод, популярный вопрос, CTA уведомлений
send_chat_message). - Кнопки UI «Чат» и «Оператор» (
tel:←operator.call.phone) — смежно с уведомлениями. - Нормализация текста входящих из Bitrix (удаление служебной разметки отправителя) — уже закрытый дефект backlog.
- Ownership, rate limits,
Idempotency-Key(TTL 24 ч) на create dialog / send message. - Константы
chat.attachments.*,rate_limit.message_send.*,operator.call.phone.
3.2. Вне scope
- Смешанное сообщение «текст + файл(ы)» (post-MVP, отдельная версия API).
- Несколько вложений в одном исходящем сообщении.
- UI-раздел «История чатов / список диалогов» как продуктовый экран — deprecated для MVP frontend (один чат с компанией); backend
GET /api/v1/dialogsне удаляется. - Индикатор непрочитанных сообщений чата и sync между устройствами (
Dialog.client_last_opened_atи аналоги) — backlog п.23–24; не путать с бейджем уведомлений. - Typing indicators, реакции, редактирование/удаление сообщений клиентом, ответы на конкретное сообщение (quote/reply).
- Голосовые / видеосообщения, стикеры, произвольные форматы сверх
chat.attachments.*. - Push / deep link в чат (модель может быть push-ready позже).
- Админ-модерация очереди
needs_review(enum зарезервирован, в MVP не создаётся). - CRM Contact sync в hot path чата (
bitrix-syncне участвует в Open Lines delivery). - До production cutover допускается только явно маркированный stub v1; target Message Safety v2 имеет
200/202/403, local-only URL checks и file scan.
4. UI
| Место | Поведение |
|---|---|
| Главная: поле ввода | Отправка текста; без auth → согласия → OTP → bootstrap; затем немедленный переход в чат и отложенная отправка |
| Главная: популярные вопросы | Тап = автоотправка текста вопроса (тот же поток, что ручной ввод) |
| Кнопка «Чат» | Открывает текущий активный диалог или создаёт его при отсутствии |
| Кнопка «Оператор» | tel: на operator.call.phone (не чат-сообщение) |
| Экран чата | Лента сообщений клиента и компании; статусы доставки по DTO |
| Вложения | Выбор файла → init → PUT в quarantine → complete → send content_kind=file |
| Гость в Центре уведомлений / чате | Auth-gate; после входа — ЛК |
Визуал — по Figma. Figma не канон статусов доставки и состава API.
5. Бизнес-модель
5.1. Сущности
| Сущность | Схема | Назначение |
|---|---|---|
Dialog |
han_app |
Диалог клиента с Open Lines |
Message |
han_app |
Сообщение в диалоге |
MessageAttachment |
han_app |
Вложение (client upload или company inbound) |
safety_tasks |
han_app |
Checkpoint sync-wait Message Safety (техническая) |
delivery_outbox |
han_app |
Durable намерение доставки в Open Lines |
openlines_inbox_receipts |
han_app |
Idempotency приёма событий local app |
dialog_sessions |
bitrix_local |
Маппинг чата у bitrix-local-app |
popular_questions |
han_app |
Справочник текстов быстрых вопросов (не сообщения) |
5.2. Dialog.status
| Значение | Смысл |
|---|---|
open |
Диалог создан, сообщений ещё нет |
waiting_for_company |
Последнее значимое — исходящее от клиента; ждём оператора |
waiting_for_client |
Последнее значимое — входящее от оператора; ждём клиента |
closed |
Закрыт в Open Lines (dialog.closed / ONIMCONNECTORDIALOGFINISH) |
Переходы:
| Событие | Новый статус |
|---|---|
POST /dialogs (новый) |
open |
| Успешная доставка исходящего клиента в Open Lines | waiting_for_company |
| Сохранено входящее от оператора | waiting_for_client |
Inbox dialog.closed |
closed |
Из closed обратный переход запрещён. Новый активный диалог после закрытия — снова через POST /dialogs, когда продукт это разрешит; инвариант «один active» сохраняется.
5.3. Сообщение: вид и статусы
content_kind (исходящее MVP):
content_kind |
Тело запроса | Message.text |
Вложения |
|---|---|---|---|
text |
непустой text |
текст | 0 |
file |
attachment_id + checksum |
пустая строка | ровно 1 |
Запрещено: текст + файл → 400 mixed_content_not_allowed; пустое → 400 empty_message; >1 вложение → 400 too_many_attachments.
sender_type: client | company.
safety_status:
| Значение | Смысл |
|---|---|
pending |
Проверка не завершена |
allowed |
Финальный allow |
blocked |
Финальный deny |
needs_review |
Зарезервирован, в MVP не создаётся |
Входящие company всегда safety_status=allowed.
delivery_status:
| Значение | Смысл | Клиенту как финал POST .../messages? |
|---|---|---|
accepted |
Принято API, safety/доставка ещё не финализированы | нет (промежуточный) |
processing |
Sync-wait safety | нет |
delivered |
Allow + ушло в Open Lines (или входящее сохранено) | да |
rejected |
Deny Message Safety | да (422 message_blocked) |
failed |
Инфраструктурная ошибка (Bitrix/S3/timeout), не safety-deny | да (503/504) |
Инварианты: rejected ↔ blocked; delivered → allowed.
5.4. Вложение
scan_status |
Смысл |
|---|---|
pending |
В quarantine, проверка не завершена |
clean |
Allow, файл в S3-data (после promote) |
bypassed |
Forced allow в emergency MOCK; файл перенесён, но не проверялся |
infected |
Deny |
failed |
Ошибка инфраструктуры проверки |
direction: client_upload | company_inbound.
Клиентский файл до allow живёт только в versioned S3-quarantine. Presigned PUT подписывает If-None-Match: * и checksum; один object key нельзя перезаписать. Постоянных access keys у клиента нет.
5.5. Идентификаторы
| Имя | Назначение |
|---|---|
dialog_id |
UUID диалога = external_chat_id Open Lines |
message_id |
UUID сообщения; ключ idempotent delivery |
attachment_id |
UUID вложения |
bitrix_message_id |
ID сообщения в Bitrix для inbox dedup |
task_id |
Async-проверка Message Safety |
Idempotency-Key |
Клиентский ключ create dialog / send (Redis + durable fallback), TTL 24 ч |
5.6. Константы (app_settings)
| Ключ | Смысл | Seed |
|---|---|---|
chat.message.max_length |
Макс. длина нормализованного текста; public config для счётчика n/max |
4000 |
chat.attachments.allowed_extensions |
Разрешённые расширения | jpg,jpeg,png,webp,heic,heif,pdf |
chat.attachments.allowed_mime_types |
Разрешённые MIME | image/jpeg, image/png, …, application/pdf |
chat.attachments.disallowed_extensions |
Явный deny-list | svg,doc,docx,xls,xlsx,csv |
chat.attachments.max_size_mb |
Макс. размер | 5 |
chat.attachments.storage |
Провайдер | selectel_s3 |
chat.attachments.upload_mode |
Режим | presigned_put |
chat.attachments.safety_scan_required |
Safety обязателен | true |
chat.attachments.presigned_upload_ttl_seconds |
TTL upload URL | 600 |
rate_limit.message_send.per_user |
Лимит отправок | 30/minute |
rate_limit.message_send.per_dialog |
Лимит на диалог | 20/minute |
rate_limit.download_url.per_user |
Лимит download-url | 60/hour |
operator.call.phone |
Телефон кнопки «Оператор» | +74999591007 |
Те же chat.attachments.* переиспользуются уведомлениями для документов клиента.
5.7. Популярные вопросы
- Справочник
popular_questionsотдаётся public content API. - Отдельного backend-flow нет: после auth текст уходит как обычное
content_kind=text. - Без auth — тот же механизм отложенного сообщения, что у ручного ввода.
6. Поведение UI и сценарии
6.1. Создание / открытие диалога
- Frontend перед первым
POST .../messagesвызываетPOST /api/v1/dialogsсIdempotency-Key. - Нет active →
201,status=open. - Есть active →
200, возвращается существующий (новый не создаётся). - Кнопка «Чат» открывает этот диалог (или создаёт при отсутствии).
6.2. Исходящее текстовое сообщение
POST /dialogs(если нетdialog_id).POST .../messagesсcontent_kind=text,Idempotency-Key.- Rate limits (nginx + app).
- Message Safety (текст, ссылки).
- Allow → outbox → Open Lines →
delivery_status=delivered,Dialog→waiting_for_company. - Deny →
422 message_blocked, в Open Lines не уходит; backend сохраняет в истории отдельнуюcompany-реплику с бизнес-текстом для сообщения или документа. - Dependency failure →
503/504, при уже созданном Message —delivery_status=failed.
6.3. Исходящий файл
POST .../attachments/init→ presigned PUT в versioned S3-quarantine со signedIf-None-Match: *, checksum иContent-Type.- Frontend грузит байты напрямую; повторная запись key получает
412. POST .../attachments/{id}/complete+ checksum → фиксация authoritativeversion_id + ETag + checksum,scan_status=pending.POST .../messagesсcontent_kind=file,attachment_id,checksum.- Safety (файл); при
202 pendingapi-backend sync-pollLocationвнутри того же HTTP-запроса клиента. - Allow → conditional promote сохранённой S3 version (source ETag/checksum match) → S3-data attachments → delivery Open Lines.
- Deny → quarantine delete,
blocked/rejected.
Пока идёт poll safety, это клиентское соединение ждёт; параллельные запросы других клиентов не блокируются.
6.4. Отложенное сообщение (гость → после auth)
Общий frontend-механизм для:
- ручного ввода;
- популярного вопроса;
- CTA уведомлений с
send_chat_message(после входа в контуре G).
Порядок:
- Клиент инициирует отправку без JWT → согласия → OTP →
bootstrap→session-start. - Экран авторизации завершается после успешного bootstrap, даже если последующая отправка в чат упадёт.
- Затем
POST /dialogs→POST .../messagesс сохранённым текстом. - Ошибка Bitrix/safety показывается в контексте чата, не как «не удалось завершить вход» (backlog п.21).
6.5. Входящее от оператора
- Bitrix24
ONIMCONNECTOR*→bitrix-local-app(inbox, retry, DLQ). - Forward в
POST /internal/openlines/v1/inbox(message.new). - api-backend: ownership по
external_chat_id, save Messagecompany/allowed/delivered, файлы → S3-data +MessageAttachment. - Текст очищается от служебной разметки отправителя Bitrix (BBCode-префиксы имени и т.п.) — клиент видит чистый текст ответа.
Dialog.status→waiting_for_client.- Publish WS
message.new; при недоступности WS — клиент подтянет через polling. - Local app ack delivery в Bitrix после успешного apply / duplicate-ack.
Лимиты MIME/size для файлов оператора в MVP — те же chat.attachments.*.
Файл оператора считается данными доверенного Bitrix24-channel: он не загружается в quarantine и не проходит Message Safety/ClamAV. Выполняются только MIME/size validation, безопасная выдача download response и audit. Остаточный malware-риск для MVP принят явно.
6.6. Закрытие диалога
Inbox dialog.closed → Dialog.status=closed. Frontend получает dialog.status по WS или при следующем GET. Новый активный — только новым POST /dialogs.
6.7. Realtime и fallback
WS /api/v1/realtime+ JWT.- После
connected—subscribeсdialog_ids(и опциональноnotifications). - События чата:
message.new,message.status,dialog.status. - Reconnect: backoff 1s…30s; повтор
subscribe. - WS недоступен > 30s → polling
GET .../messages?after=<cursor>(и notifications counter при подписке). - Ping/pong ~30s.
6.8. Скачивание вложения
GET .../attachments/{id}/download-url → короткий presigned GET + audit attachment.download_url_issued. URL в логи/audit не пишется. Ownership обязателен.
7. Матрицы поведения
7.1. Исходящее: safety → delivery
| Вердикт safety | safety_status |
delivery_status (финал клиенту) |
Open Lines | HTTP клиенту |
|---|---|---|---|---|
200 allow |
allowed |
delivered после успешной отправки; иначе failed |
да (после allow) | 201 или 503/504 |
403 deny |
blocked |
rejected |
нет | 422 message_blocked |
202 pending → затем allow/deny |
как финал | как финал | только после allow | финальный код после poll |
| timeout / circuit open | по политике модуля | failed |
нет | 503/504 |
Клиенту не отдаётся промежуточный processing как успешный ответ POST .../messages.
7.2. Идемпотентность клиента
| Ситуация | Результат |
|---|---|
Тот же Idempotency-Key + то же тело |
Тот же HTTP-ответ, без повторного side-effect |
| Тот же ключ + другое тело | 409 idempotency_key_reused |
| Нет ключа на обязательном endpoint | 400 validation_error |
| TTL | 24 часа (Redis); durable fallback в idempotency_records |
Доставка в Open Lines идемпотентна по message_id. Inbox message.new — unique (external_chat_id, bitrix_message_id).
7.3. Деградация зависимостей
| Зависимость | Влияние на чат |
|---|---|
| Redis DB0 | Write fail-closed; read ограниченно деградирует |
| Redis DB1 (realtime) | REST работает; WS/publish деградирует → polling |
message-safety |
Отправка недоступна; чтение истории работает |
bitrix-local-app / Bitrix |
После allow сообщение может стать failed; recovery по outbox |
| S3 | Файловые операции недоступны; текстовый чат продолжает работать |
| Open Lines в readiness | Может быть degraded; не обязано валить весь API |
7.4. Ownership и ошибки доступа
| Ситуация | Код |
|---|---|
Чужой dialog_id / attachment_id / message |
404 (существование не раскрывается) |
| Нет/невалиден JWT | 401 |
Диалог closed, политика запрещает send |
по контракту модуля (409/422 — уточнить в OpenAPI при публикации) |
| Safety deny | 422 message_blocked |
8. Идентификация и корреляция
- Публичные id — UUID.
dialog_idгенерирует приложение при create и передаётся в Open Lines какexternal_chat_id.- При первой доставке
bitrix-local-appсоздаёт/обновляетdialog_sessions. X-Request-ID,traceparent, опциональноX-Ux-Session-Id— для логов/audit; не auth.- Connector Bitrix:
han_mobile_app, Open Line id по env/глоссарию.
9. API
Общие конвенции — arch-02: /api/v1/* JWT, /api/v1/public/*, /internal/{mnemonic}/v1/*; JSON snake_case; даты RFC 3339 UTC; cursors opaque.
9.1. Клиентские (JWT)
| Метод и путь | Назначение |
|---|---|
POST /api/v1/dialogs |
Find-or-create active dialog (Idempotency-Key) |
GET /api/v1/dialogs |
Список/история диалогов (API есть; UI MVP — один чат) |
GET /api/v1/dialogs/{dialog_id} |
Карточка диалога |
GET /api/v1/dialogs/{dialog_id}/messages?after=&limit= |
История / polling |
POST /api/v1/dialogs/{dialog_id}/messages |
Отправка (Idempotency-Key + safety) |
POST .../attachments/init |
Presigned PUT quarantine |
POST .../attachments/{id}/complete |
Подтверждение upload |
GET .../attachments/{id}/download-url |
Presigned GET + audit |
WS /api/v1/realtime |
События чата (и опционально уведомлений) |
9.2. Public
| Метод и путь | Назначение |
|---|---|
GET /api/v1/public/content |
Тексты + popular_questions |
GET /api/v1/public/settings |
В т.ч. публичные флаги/лимиты UI при is_public |
9.3. Internal
| Метод и путь | Кто → кто | Назначение |
|---|---|---|
POST /internal/safety/v2/messages/check |
api-backend → message-safety | Проверка |
GET /internal/safety/v2/messages/tasks/{task_id} |
api-backend → message-safety | Poll вердикта; pending = 202 |
POST /internal/openlines/v1/messages |
api-backend → bitrix-local-app | Исходящая доставка |
GET /internal/openlines/v1/dialogs/{external_chat_id} |
api-backend → bitrix-local-app | Reconciliation |
POST /internal/openlines/v1/inbox |
bitrix-local-app → api-backend | Входящие события |
9.4. Ошибки домена чата
| Код | HTTP | Когда |
|---|---|---|
validation_error |
400 | Нет Idempotency-Key, невалидное тело |
empty_message |
400 | Нет текста и вложения |
mixed_content_not_allowed |
400 | Текст и файл вместе |
too_many_attachments |
400 | >1 вложение |
attachment_not_completed |
400 | Send до complete |
attachment_checksum_mismatch |
400 | Checksum не совпал |
unauthorized |
401 | JWT |
message_blocked |
422 | Safety deny |
idempotency_key_reused |
409 | Ключ с другим телом |
resource_state_conflict |
409 | Конфликт состояния (напр. complete с другим checksum) |
rate_limit_exceeded |
429 | Лимит |
dependency_unavailable |
503 | Circuit / недоступна зависимость |
dependency_timeout |
504 | Timeout зависимости |
9.5. DTO MessageResponse (REST и WS)
{
"message_id": "uuid",
"dialog_id": "uuid",
"sender_type": "client",
"content_kind": "text",
"text": "Здравствуйте",
"attachments": [],
"safety_status": "allowed",
"delivery_status": "delivered",
"created_at": "2026-07-09T12:00:00Z"
}
Сортировка сообщений: created_at ASC (append в ленте). Диалоги: updated_at DESC.
10. Модель данных (схема han_app)
Общие правила — module-01 §9.1 / arch-05.
10.1. dialogs
| Поле | Описание |
|---|---|
id |
= dialog_id = external_chat_id |
user_id |
FK владельца |
status |
enum §5.2 |
last_message_at |
|
closed_at |
при closed |
| common fields | обязательны |
Partial unique: один active dialog на user_id.
10.2. messages
| Поле | Описание |
|---|---|
id |
message_id |
dialog_id |
FK |
sender_type |
client | company |
content_kind |
text | file |
text |
непустой для text; '' для file |
safety_status / delivery_status |
§5.3 |
safety_processing_mode |
`standard |
safety_config_version |
версия active Message Safety config, internal/audit only |
external_message_id |
Bitrix id для inbound |
client_idempotency_key |
опционально/связка с Idempotency-Key |
occurred_at |
|
| common fields |
Индексы: лента (dialog_id, created_at, id); recovery по delivery_status; unique inbound (dialog_id, external_message_id).
Решение: исходящее создаётся до завершения safety со статусами pending/accepted, чтобы safety_tasks имел FK (crash checkpoint).
10.3. message_attachments
Поля: id, dialog_id, message_id NULL до привязки, owner_user_id, direction, имена/MIME/size/checksum, scan_status, storage keys, quarantine_version_id, quarantine_etag, timestamps, common fields.
MVP: unique partial — не более одного active attachment на message_id.
10.4. Технические таблицы
| Таблица | Назначение | Физическая очистка |
|---|---|---|
safety_tasks |
Poll/recovery Message Safety | да, по retention |
delivery_outbox |
App → Open Lines | по статусам/retention модуля |
openlines_inbox_receipts |
Dedup inbox | по политике модуля |
idempotency_records |
Durable fallback Idempotency-Key | по expires_at |
10.5. Схема ключей S3 (чат)
quarantine/... # клиентский upload до вердикта
attachments/... # проверенные вложения чата (client + company)
Бакет documents (han-chat-documents) — для документов профиля/уведомлений, не hot path чата Open Lines.
11. Фоновые процессы
| Процесс | Владелец | Назначение |
|---|---|---|
| Safety recovery worker | api-backend | Доводит 202 pending после обрыва клиентского HTTP |
| Delivery outbox worker | api-backend | Retry доставки в local app / Open Lines |
| Quarantine orphan cleanup | api-backend / ops | Удаляет просроченные объекты без active task/attachment |
| Inbox retry / DLQ | bitrix-local-app | Надёжность webhook → API |
| Realtime publish | api-backend + Redis DB1 | Fan-out WS; при сбое — polling |
Application-код не обходит outbox «в обход» для повторной доставки без idempotency.
12. Audit и observability
event_type |
Когда |
|---|---|
dialog.created |
Новый диалог |
message.submitted |
Принято к обработке |
message.blocked |
Safety deny |
message.delivered |
Успех Open Lines / inbound saved |
message.failed |
Инфраструктурный fail |
attachment.upload_initialized / completed / promoted / rejected |
Lifecycle файла |
attachment.download_url_issued |
Presigned GET |
openlines.inbox_applied |
Входящее применено |
Метрики: latency send, safety poll duration/timeout, outbox depth/age/DLQ, inbox duplicate/apply, WS reconnect/drop, rate limit rejects. Нельзя использовать user_id/dialog_id как metric labels.
В логах нет: тел сообщений, tokens, presigned URL, Bitrix download URL, полного PII.
13. Хранение
Dialog/Message/MessageAttachment— прикладные строки, soft-delete, физическое удаление запрещено.- Закрытые диалоги и их сообщения остаются в БД (история API); UI MVP показывает текущий чат.
- Quarantine и технические checkpoint — исключения с retention.
- S3-data attachments живут с записью вложения; lifecycle бакетов — ops-политика.
14. Смежные сервисы
| Компонент | Роль в чате |
|---|---|
| Frontend | UI чата, отложенное сообщение, upload, WS/polling, кнопки Чат/Оператор |
| api-backend | Dialogs/messages/attachments, safety orchestration, outbox, inbox apply, realtime |
| message-safety | Вердикт allow/deny/pending по исходящим |
| bitrix-local-app | Connector Open Lines, исходящие/входящие, dialog_sessions |
| Bitrix24 Open Lines | Рабочее место оператора |
| S3 | Quarantine + attachments |
| Redis | Rate limit, idempotency cache, realtime coordination |
| nginx | /api/* + WS upgrade; proxy_read_timeout ≥ safety poll budget + запас |
| bitrix-sync | Не в hot path чата |
Порядок работ (если поднимать домен с нуля): (1) dialogs + messages text path + idempotency → (2) safety orchestration → (3) Open Lines out + inbox → (4) attachments → (5) WS + polling → (6) recovery/outbox hardening.
15. Влияние на arch-документы
| Документ | Содержание по чату |
|---|---|
arch-00-glossary.md |
Dialog, Message, статусы, Open Lines terms |
arch-01-system-architecture.md |
Потоки C→O и O→C, создание диалога |
arch-02-api-contracts.md |
REST/WS/safety/openlines контракты |
arch-03-docker-compose-blueprint.md |
Сервисы, WS location, timeouts |
arch-04-settings-and-content.md |
chat.attachments.*, rate limits, operator phone |
module-01-api-backend.md |
Алгоритмы, таблицы, workers |
module-05-message-safety.md |
Stub/production safety |
module-06-bitrix-local-app.md |
Connector / inbox / out |
Этот документ собирает бизнес-смысл обмена сообщениями для аналитики и смежных фич; детальные алгоритмы — в module-спеках.
Для Safety: Product Owner принимает generic UX; Safety Service Owner — v2 contract/cutover; Rule Pack Owner — rules/corpus; Security Owner — monitor→deny и risks; Operations Owner — VM2/incident/restore. Назначения ролей фиксируются в release checklist.
16. Критерии приёмки
- Без JWT отправить сообщение нельзя; после auth отложенный текст/популярный вопрос уходит штатным
POST /dialogs→POST .../messages. - Не более одного active dialog; повторный
POST /dialogsвозвращает существующий. - Text и file взаимоисключающи; mixed/empty/too many → соответствующие
400. POST .../messagesвозвращает только финальный статус; deny →422 message_blockedбез доставки в Bitrix.- Allow → сообщение видно оператору в Open Lines;
Dialog→waiting_for_company. - Ответ оператора появляется в клиенте через WS или polling;
Dialog→waiting_for_client; текст без служебной разметки имени из Bitrix. dialog.closedзакрывает диалог; изclosedнельзя вернуться тем же id.- Файлы: quarantine → safety → promote; deny чистит quarantine; download только presigned + audit.
- Идемпотентность create/send соблюдается 24 ч; повтор delivery/inbox не плодит дубли.
- При падении WS > 30s клиент уходит в polling и не теряет сообщения, уже лежащие в App DB.
- Недоступность safety блокирует send, но не чтение истории; недоступность S3 не ломает text-only чат.
- Ошибка отправки после успешного bootstrap не выглядит как ошибка входа.
- Кнопка «Оператор» берёт номер только из
operator.call.phone. - Популярный вопрос не имеет отдельного API — только text message.
- Ownership: чужие dialog/attachment →
404. - Любой Safety deny создаёт ровно одну company-реплику с mnemonic
safety.chat.blocked; исходный blocked text редактируется, internalrule_idне виден клиенту. - В emergency MOCK normal checks не выполняются: отдельные fixed policy для text/file дают только sync allow/deny. Mode не виден клиенту; mock file allow хранится как
scan_status=bypassed, а включение доступноdeployтолько через root-owned helper/restart и не имеет auto-expiry. - Semantic prompt-injection RU/EN возвращает allow + monitor audit; active content, URL policy и malware остаются hard deny.
files=unavailableне ломает text-only чат;links=unavailableблокирует только text с URL; Redis Safety outage не выключает core.- File async проходит
202внутри api-backend до sticky final; public pending клиенту не возвращается. - EICAR, malformed/polyglot/encrypted/active PDF дают deny; dependency timeout даёт
503, а не blocked. - Link pipeline не выполняет HTTP fetch; NXDOMAIN разрешяется с monitor, private/metadata IP блокируется.
- S3 overwrite получает
412; wrong version/ETag и conditional promote mismatch запрещают delivery. - Load acceptance module-05 §15.4 проходит: 10 text/s, 2 file/s, 5 slots, ≤100 pending; availability SLO в MVP не задаётся.
- VM2 cutover/rollback gates module-10 пройдены; v1 stub не считается production control.
17. Журнал решений и открытых вопросов (ненормативно)
17.1. Принятые решения
| # | Решение | Раздел / источник |
|---|---|---|
| D1 | Чат ≠ уведомления; непрочитанный чат не есть Notification | §1, notification-requirements |
| D2 | Один active dialog; dialog_id = external_chat_id |
§2, arch-01 |
| D3 | MVP content: text XOR file | §5.3, arch-02 |
| D4 | Клиент ждёт финальный вердикт на одном HTTP; poll safety внутри api-backend | §6.2–§6.3 |
| D5 | Исходящие проходят Message Safety; входящие оператора — доверенный канал | §2, arch-02 |
| D6 | Durable outbox + idempotent delivery; inbox dedup | §6, module-01 |
| D7 | WS обязателен как primary realtime; polling — fallback | §6.7 |
| D8 | UI истории диалогов deprecated; API списка сохраняется | §3.2 |
| D9 | Отложенное сообщение — общий frontend-механизм | §6.4 |
| D10 | Популярный вопрос = обычный text send | §5.7 |
| D11 | Presigned upload напрямую в S3; api-backend не проксирует байты | §6.3 |
| D12 | chat.attachments.* — единые лимиты для чата (и reuse уведомлениями) |
§5.6 |
| D13 | Generic Safety deny: company-реплика safety.chat.blocked, blocked text redacted, rule не раскрывается |
§6.2, module-01 M8 |
| D14 | Emergency MOCK: независимые forced text/file allow/deny, root-owned helper для deploy, без auto-expiry |
module-05 §2.3, module-10 |
| D14 | Semantic rules monitor-only; NXDOMAIN monitor allow | module-05 |
| D15 | Performance acceptance без availability SLO: 10 text/s, 2 file/s, 5 slots, 100 pending | module-05 §15.4 |
17.2. Открытые вопросы
| # | Вопрос | Предложение | Влияние |
|---|---|---|---|
| Q1 | Индикатор непрочитанных сообщений чата + sync между устройствами (backlog 23–24) | Поле вроде Dialog.client_last_opened_at / last_read_message_id + WS/REST counter; не тип уведомления message |
Новая мини-постановка |
| Q2 | Точный HTTP-код send в уже closed dialog |
Зафиксировать в OpenAPI (409 resource_state_conflict или 422) |
Клиентский UX |
| Q4 | Создание нового dialog сразу после closed — всегда разрешено или по бизнес-правилу «сессия поддержки» |
MVP: разрешить, пока соблюдён unique active | Продукт / поддержка |
| Q5 | Stub v1 расходится с target v2 | Для production — только 200/202/403 и terminal failed 503; stub изолирован adapter-ом до cutover |
module-05 / contract tests |
| Q6 | Смешанный content text+files | Отдельный API version post-MVP | arch-02 breaking |
| Q7 | Unread badge на кнопке «Чат» vs бейдж колокольчика уведомлений | Развести визуально и в данных | UI + Q1 |
18. Связь со смежными доменами
| Домен | Связь |
|---|---|
| Пользователь | Чат только после JWT + bootstrap; отложенное сообщение после входа |
| Уведомления | CTA send_chat_message пишет в чат; кнопки Чат/Оператор на главной; тип message в уведомлениях запрещён |
| Документы профиля | Другой бакет/реестр; не заменяют вложения чата |
CRM (bitrix-sync) |
Contact map по телефону параллельно; не доставляет сообщения Open Lines |
Владелец ленты и статусов доставки — api-backend + App DB; рабочее место оператора — Bitrix24 Open Lines через bitrix-local-app.