Deal — единая кодовая база
ci / build-test (push) Canceled after 0s

SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/
Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue,
контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер),
Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог).

Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0,
тесты 1340/130/52/38/9 зелёные.
This commit is contained in:
Rustam Khalimov
2026-09-11 23:56:47 +03:00
commit 27c7831910
1383 changed files with 158436 additions and 0 deletions
@@ -0,0 +1,86 @@
# Открытые вопросы: вложения источников (media → Storage) и просмотр исходника
Дата: 2026-09-11. Статус: решения владельца получены (см. §0).
## 0. Решения владельца (2026-09-11)
- **А) Медиа-посты пропускаем.** Сообщения без текста (только медиа/вложение) в систему не попадают.
Извлечение вложений Telegram и выгрузка их в Storage не делаются. Generic-контракт по-прежнему умеет
нести `DataRef` — этим смогут пользоваться другие источники (файл/диск/таблица) и ручные вложения карточки.
- **Б) Проверка без живого Telegram** — реализуем с юнит-тестами на фейковой сессии/фейковом Storage,
без реального API.
- **В)** Объяснение термина — в §1.5. **Решение: делаем.** Реализован remote-просмотр: `TelegramService.ReadSource`,
`ITelegramGateway.ReadSourceAsync`, `TelegramSourceContentProvider` (Kind=telegram) в ядре,
`GET /api/cards/{id}/source` и кнопка «Обновить из источника» в подробной карточке.
## 1.5. Что такое «remote-просмотр исходника»
Карточка хранит **ссылку на источник** (`SourceRef`) и **содержимое** (`SourceContent`). Содержимое попадает
в карточку в момент приёма. «Просмотр исходника» — это возможность по кнопке догрузить/показать **оригинальное
сообщение у источника** (то, что было в канале/письме/строке), если контент в карточке устарел или урезан.
Сейчас содержимое уже отдаётся в `CardDto.content` и через `GET /api/cards/{id}/source`. Для локальных
источников этого достаточно. Для **внешних** источников (например Telegram) данные лежат не в ядре, а в
сервисе-владельце; чтобы их догрузить, ядру нужен провайдер `ISourceContentProvider` для `kind`, который
ходит по gRPC к сервису-владельцу (условный RPC `ReadSource(dialogId, msgId)`) и возвращает исходный текст/медиа.
Это и есть «remote-просмотр» — расширение extension-point, которое не требуется до появления реальной
необходимости (напр. если карточки хранят урезанный текст или нужно открыть живой первоисточник).
## 1. Что уже готово (не требует решений)
- Единый контракт источника несёт вложения: `SourceContent.Data: IReadOnlyList<DataRef>`
ссылки на объекты Storage-сервиса (`DataRef.Id/Ref/Kind/MimeType/...`).
- Контракт входящего потока (`src/contracts/sources.proto`, `PushSource`) передаёт
`DataRefProto`/`ContactRefProto` — источник может прислать вложения сразу со ссылками.
- Storage-сервис (`src/storage-service/Deal.Storage`, `storage.proto`) умеет `Upload/Download/Stat/Delete`,
сам определяет `kind`/`mimeType`/размеры (контент-снифинг), бэкенд — MinIO.
- Ядро хранит `SourceContent` карточки (в т.ч. `Data`) и отдаёт его в `CardDto.content` и через
`GET /api/cards/{id}/source` (extension-point `ISourceContentProvider` + `SourceContentResolver`).
- Фронт рендерит вложения: `SourceContentView.vue` (image/video/audio/document/archive по `kind`),
ссылки и контакты — списками.
## 2. Проблемная часть (требует живого Telegram)
Извлечение и выгрузка медиа из Telegram не проверяемы офлайн:
1. **Медиа-сообщения сейчас отбрасываются.** `TlMessageMapper.ToMessage`
(`src/telegram-service/Deal.Telegram/Telegram/TlMessageMapper.cs`) принимает только `Message`
с непустым `message` (текстом). Посты с одним вложением и подписью (`media` + `caption`) не попадают
в поток вообще. Нужно: определять `Message.media`, читать `caption`, тип/размеры/длительность.
2. **Скачивание и выгрузка.** Требуется `client.DownloadMedia(...)` (WTelegram) → поток →
`StorageService.Upload(meta + data)``DataRefProto`. В telegram-сервисе нет gRPC-клиента Storage
и соответствующей конфигурации в compose (endpoint/токен). Проверить можно только с реальным
Telegram-аккаунтом и живым MinIO.
3. **Подпись без текста.** Даже если вложение извлечено, в посте может не быть текста: нужен ли такой
пост «карточкой» (сейчас `PipelineIngestService` пропускает записи без `Content.Text`)? Предлагается
принимать запись, если есть текст **или** вложения/ссылки/контакты, а классификацию медиа-онли
строить по подписи (`caption`) и метаданным. Требуется подтверждение продуктовой логики.
4. **Просмотр исходника из другого контура.** «Открыть исходник» для remote-источников (Telegram — это
лишь один из них) требует провайдера `ISourceContentProvider`, который ходит по gRPC к сервису-владельцу
источника (новый RPC, например `ReadSource(dialogId, msgId)`), возвращая текст/медиа. Это тоже
живой Telegram.
## 3. Предлагаемый план (после подтверждения)
1. `TelegramMessage` расширить моделью `TelegramAttachment` (caption, fileName, mimeType, size, width,
height, durationSec, `Task<Stream> Open(cancellationToken)`), заполнять в `TlMessageMapper` из
`Message.media`/`Document`/`Photo`.
2. В telegram-сервисе добавить `StorageClient` (gRPC, `Deal.Grpc.Storage`) + `SourceAttachmentUploader`:
загрузка каждого вложения → `DataRefProto`.
3. `DialogProtoMapper.ToSourceRequest(message, dataRefs)` — прокинуть `content.data` и `caption`.
4. `PipelineIngestService`: принимать запись при непустом тексте **или** непустых `Data`/`Links`/`Contacts`
(нужно продуктовое решение по п.2.3).
5. `ISourceContentProvider` для `kind="telegram"` — gRPC-провайдер к telegram-сервису (RPC `ReadSource`).
6. Настройки: endpoint/токен Storage в `deploy/compose.dev.yml`/`compose.prod.yml` для telegram-сервиса.
## 4. Что нужно от владельца
- А) Делать ли медиа-сообщения без текста карточками (по подписи/метаданным), или пропускать?
- Б) Для проверки вложений нужны живые Telegram api_id/api_hash и работающий MinIO — будет ли прогон
на вашей стороне, или реализуем «слепо» с юнит-тестами на фейковой сессии и фейковом Storage?
- В) Нужен ли remote-просмотр исходника (`ReadSource`) в этом объёме, или достаточно того, что
содержимое хранится в карточке?
Пока эти пункты не закрыты, они вынесены в `backlog.md` (`TD-STORE-ATTACH`, `TD-SOURCE-PROVIDER`),
а generic-часть (контракт, Storage-сервис, хранение, API, рендер) реализована.