Реализованы сервисы ВМ2 - проверка сообщений и синхронизация с Б24 (деплой еще без перевода в боевой режим)

This commit is contained in:
mi
2026-08-13 18:52:42 +03:00
parent 5100ba9fc3
commit 99605b1c77
144 changed files with 15295 additions and 1120 deletions
@@ -23,7 +23,7 @@
| Направление | `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 | **Нет** outbound moderation (доверенный канал); MIME/size/antivirus policy модуля | App DB + WS / polling |
| **O→C. Оператор → клиент** | `company` | Bitrix24 webhook → `bitrix-local-app` → inbox API | **Нет** Message Safety/AV; только MIME/size, residual risk принят | App DB + WS / polling |
Правила:
@@ -65,7 +65,7 @@
- Push / deep link в чат (модель может быть push-ready позже).
- Админ-модерация очереди `needs_review` (enum зарезервирован, в MVP не создаётся).
- CRM Contact sync в hot path чата (`bitrix-sync` **не** участвует в Open Lines delivery).
- Реальная антивирус/LLM-модерация: в MVP допускается stub `message-safety` с фиксированными правилами теста; канонический контракт вердиктов — arch-02 (`200/203/403`).
- До production cutover допускается только явно маркированный stub v1; target Message Safety v2 имеет `200/202/403`, local-only URL checks и file scan.
---
@@ -162,12 +162,13 @@
|---|---|
| `pending` | В quarantine, проверка не завершена |
| `clean` | Allow, файл в S3-data (после promote) |
| `bypassed` | Forced allow в emergency MOCK; файл перенесён, но не проверялся |
| `infected` | Deny |
| `failed` | Ошибка инфраструктуры проверки |
`direction`: `client_upload` \| `company_inbound`.
Клиентский файл до allow живёт **только** в S3-quarantine. Постоянных access keys у клиента нет — только короткий presigned PUT/GET.
Клиентский файл до allow живёт **только** в versioned S3-quarantine. Presigned PUT подписывает `If-None-Match: *` и checksum; один object key нельзя перезаписать. Постоянных access keys у клиента нет.
### 5.5. Идентификаторы
@@ -229,12 +230,12 @@
### 6.3. Исходящий файл
1. `POST .../attachments/init` → presigned PUT в **S3-quarantine**.
2. Frontend грузит байты **напрямую в S3** (не через api-backend).
3. `POST .../attachments/{id}/complete` + checksum → HeadObject, metadata, `scan_status=pending`.
1. `POST .../attachments/init` → presigned PUT в versioned **S3-quarantine** со signed `If-None-Match: *`, checksum и `Content-Type`.
2. Frontend грузит байты напрямую; повторная запись key получает `412`.
3. `POST .../attachments/{id}/complete` + checksum → фиксация authoritative `version_id + ETag + checksum`, `scan_status=pending`.
4. `POST .../messages` с `content_kind=file`, `attachment_id`, `checksum`.
5. Safety (файл); при `203 pending` api-backend sync-poll `task_id` внутри того же HTTP-запроса клиента.
6. Allow → promote quarantine → S3-data attachments → delivery Open Lines (`message.files` signed URL, `message.text` пустой).
5. Safety (файл); при `202 pending` api-backend sync-poll `Location` внутри того же HTTP-запроса клиента.
6. Allow → conditional promote сохранённой S3 version (source ETag/checksum match) → S3-data attachments → delivery Open Lines.
7. Deny → quarantine delete, `blocked`/`rejected`.
Пока идёт poll safety, **это** клиентское соединение ждёт; параллельные запросы других клиентов не блокируются.
@@ -266,6 +267,8 @@
Лимиты 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`.
@@ -293,7 +296,7 @@ Inbox `dialog.closed` → `Dialog.status=closed`. Frontend получает `dia
|---|---|---|---|---|
| `200 allow` | `allowed` | `delivered` после успешной отправки; иначе `failed` | да (после allow) | `201` или `503`/`504` |
| `403 deny` | `blocked` | `rejected` | нет | `422 message_blocked` |
| `203 pending` → затем allow/deny | как финал | как финал | только после allow | финальный код после poll |
| `202 pending` → затем allow/deny | как финал | как финал | только после allow | финальный код после poll |
| timeout / circuit open | по политике модуля | `failed` | нет | `503`/`504` |
Клиенту **не** отдаётся промежуточный `processing` как успешный ответ `POST .../messages`.
@@ -370,8 +373,8 @@ Inbox `dialog.closed` → `Dialog.status=closed`. Frontend получает `dia
| Метод и путь | Кто → кто | Назначение |
|---|---|---|
| `POST /internal/safety/v1/messages/check` | api-backend → message-safety | Проверка |
| `GET /internal/safety/v1/messages/tasks/{task_id}` | api-backend → message-safety | Poll вердикта |
| `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 | Входящие события |
@@ -441,6 +444,8 @@ Partial unique: один active dialog на `user_id`.
| `content_kind` | `text` \| `file` |
| `text` | непустой для text; `''` для file |
| `safety_status` / `delivery_status` | §5.3 |
| `safety_processing_mode` | `standard | mock`, internal/audit only |
| `safety_config_version` | версия active Message Safety config, internal/audit only |
| `external_message_id` | Bitrix id для inbound |
| `client_idempotency_key` | опционально/связка с Idempotency-Key |
| `occurred_at` | |
@@ -452,7 +457,7 @@ Partial unique: один active dialog на `user_id`.
### 10.3. `message_attachments`
Поля: `id`, `dialog_id`, `message_id NULL` до привязки, `owner_user_id`, `direction`, имена/MIME/size/checksum, `scan_status`, storage keys (working + quarantine), timestamps, common fields.
Поля: `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`.
@@ -480,7 +485,7 @@ attachments/... # проверенные вложения чата (clie
| Процесс | Владелец | Назначение |
|---|---|---|
| Safety recovery worker | api-backend | Доводит `203 pending` после обрыва клиентского HTTP |
| 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 |
@@ -551,6 +556,8 @@ Application-код **не** обходит outbox «в обход» для по
Этот документ собирает бизнес-смысл обмена сообщениями для аналитики и смежных фич; детальные алгоритмы — в 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. Критерии приёмки
@@ -570,6 +577,16 @@ Application-код **не** обходит outbox «в обход» для по
13. Кнопка «Оператор» берёт номер только из `operator.call.phone`.
14. Популярный вопрос не имеет отдельного API — только text message.
15. Ownership: чужие dialog/attachment → `404`.
16. Любой Safety deny создаёт ровно одну company-реплику с mnemonic `safety.chat.blocked`; исходный blocked text редактируется, internal `rule_id` не виден клиенту.
17. В 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.
17. Semantic prompt-injection RU/EN возвращает allow + monitor audit; active content, URL policy и malware остаются hard deny.
18. `files=unavailable` не ломает text-only чат; `links=unavailable` блокирует только text с URL; Redis Safety outage не выключает core.
19. File async проходит `202` внутри api-backend до sticky final; public pending клиенту не возвращается.
20. EICAR, malformed/polyglot/encrypted/active PDF дают deny; dependency timeout даёт `503`, а не blocked.
21. Link pipeline не выполняет HTTP fetch; NXDOMAIN разрешяется с monitor, private/metadata IP блокируется.
22. S3 overwrite получает `412`; wrong version/ETag и conditional promote mismatch запрещают delivery.
23. Load acceptance module-05 §15.4 проходит: 10 text/s, 2 file/s, 5 slots, ≤100 pending; availability SLO в MVP не задаётся.
24. VM2 cutover/rollback gates module-10 пройдены; v1 stub не считается production control.
---
@@ -591,6 +608,10 @@ Application-код **не** обходит outbox «в обход» для по
| 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. Открытые вопросы
@@ -598,9 +619,8 @@ Application-код **не** обходит outbox «в обход» для по
|---|---|---|---|
| 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 |
| Q3 | Политика показа blocked-сообщения в ленте (полный текст vs redacted) | Минимизация PII в хранении blocked (M8 module-01) + нейтральный UI | DB + frontend |
| Q4 | Создание нового dialog сразу после `closed` — всегда разрешено или по бизнес-правилу «сессия поддержки» | MVP: разрешить, пока соблюдён unique active | Продукт / поддержка |
| Q5 | Stub `message-safety` с `400` на GET task vs канон arch-02 `403` | Для prod — только канон `200/203/403`; stub не расширяет публичный контракт | module-05 / contract tests |
| 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 |