Table of Contents
- Дейл (Deal) — Этап 6: Сервисы telegram/ai/ml (отдельные процессы) + Discovery + gRPC-ингресс Implementation Plan
- Global Constraints
- Зафиксированные решения (Rulings этапа)
- Задачи
- Task 1: .proto-контракты telegram/ai/ml + спецификация
- Task 2: Каркас telegram-service (sln, host gRPC, health, service-token)
- Task 3: Каркас ml-service (sln, host gRPC, health)
- Task 4: Каркас ai-service (sln, host gRPC, health)
- Task 5: ml-service — движок инкрементальной модели (per-tenant, SQLite)
- Task 6: ml-service — gRPC-сервис поверх пула
- Task 7: ai-service — LLM-фасад (OpenAI-совместимые + Anthropic)
- Task 8: ai-service — gRPC AiService
- Task 9: telegram-service — сессии, подключение, QR, статус
- Task 10: telegram-service — диалоги, мониторинг, backfill, поток в core
- Task 11: telegram-service — discovery-операции (search/info/read/join)
- Task 12: core — gRPC-ингресс telegram (PushMessage/SyncDialogs/ReportStatus)
- Task 13: core — модуль Telegram (таблицы, DTO, порт, DialogsService)
- Task 14: core — эндпоинты /api/tg (каналы, статус, QR) + SSE; замена boot-заглушки
- Task 15: core — ai-интеграция: контекст запроса, GrpcAiClassifier/GrpcAiTools, маппер, usage
- Task 16: core — ml-интеграция: GrpcMlClient + MlOutboxFlushScheduler
- Task 17: core — Discovery: таблицы, порт, сервисы задач/кандидатов/чёрного списка/лога
- Task 18: core — Discovery-воркер (5 с): поиск/оценка/авто-join, бан-гард
- Task 19: core — эндпоинты /api/discovery + generate-keywords; curl-приёмка
- Task 20: compose-dev, сквозная интеграция и финал этапа
- Self-Review
Перенесено из репозитория (
docs/superpowers/plans/2026-09-05-deal-stage6-services.md). Актуальная версия — здесь, в вики.
Дейл (Deal) — Этап 6: Сервисы telegram/ai/ml (отдельные процессы) + Discovery + gRPC-ингресс Implementation Plan
Исторический документ этапа 6. Актуальное состояние —
docs/superpowers/STATUS.mdиdocs/technical/Техническая-документация-Дейл.md.
Goal: Подключить к модульному монолиту src/core реальные автономные сервисы telegram/ai/ml как отдельные
процессы (свои sln/контейнеры), общаясь по gRPC (.proto в src/contracts/), и оживить вкладки Vue-фронта
«Каналы» (ChannelsView) и Discovery 1:1-контрактом /api: telegram-вкладка заменяет boot-заглушку
GET /api/tg/status реальным статусом/QR-входом/списком диалогов/мониторингом/«Перечитать»; Discovery —
полноценный модуль ядра (задачи поиска, кандидаты с оценкой по каскаду фильтров, чёрный список, авто-вступление
с квотами, лог). Пайплайн и канбан начинают получать настоящие сообщения (входящий gRPC → EnqueueAsync),
настоящие ИИ-классификацию/фильтр и ML-предсказания/обучение — за конфиг-флагами, с Local-заглушками как
фолбэком, когда сервис недоступен/выключен.
Architecture: сервисы — самодостаточные процессы (namespace Deal.Telegram/Deal.Ml/Deal.Ai): telegram
исполняет только команды ядра (сессии по тенантам 1:1, анти-бан, mark-as-read; ни БД-бизнеса, ни настроек), ml
держит пул инкрементальных моделей per-tenant с сохраняемыми весами (онлайн-обучение без дата-сайентиста — 1:1
с проверенным python mlservice/model.py, не ONNX), ai — фасад LLM-провайдеров без БД: core передаёт заполненные
промпты и конфиг провайдера в теле каждого запроса, сервис возвращает JSON-ответ модели + оценку токенов.
В ядре: новый модуль Deal.Modules.Telegram (владелец tenant-таблиц Dialogs/TgMessages, каталог каналов и
статус) с портом-гейтом ITelegramGateway, gRPC-сервер ингресса в Deal.Api (PushMessage → IngestService,
SyncDialogs, StatusReport → SSE); новые модульные части Discovery (таблицы/сервисы/воркер 5 с/оценка/анти-бан);
gRPC-адаптеры в Deal.Infrastructure заменяют Local-заглушки за флагом Services:{Ml,Ai,Telegram}:UseLocal.
Tech Stack: .NET 10 (Grpc.Tools/Google.Protobuf/Grpc.AspNetCore), WTelegramClient (NuGet), Net.Codecrete.QrCodeGenerator
(SVG QR), Microsoft.Data.Sqlite (веса моделей), HttpClient (OpenAI-совместимые + Anthropic), существующие порты
Contracts. Docker: сервисы добавляются в deploy/compose.dev.yml.
Spec: docs/architecture/2026-09-05-deal-architecture-design.md §6.2–7 (L145–187: gRPC-контракты, сервисы, пул
моделей, учёт токенов, mTLS+service-token); docs/api/api-map.md §3.3/3.7/3.8 (L123–142, L187–217), §2 SSE (L27–43),
§4.6/4.8/4.9/4.10 (настройки, каналы/discovery, статус), «кривые места» п.4/п.8/п.9 (L390–400); roadmap этапа 6
(L82–91); ТЗ §4.2/4.3/4.9, §5, §8; референс-семантика прототипа: backend/app/services/telegram.py (целиком),
services/{discovery,discovery_worker,discovery_eval,ai,suggest,ml_client,ban_guard}.py, routers/{tg_routes, discovery_routes,ml_routes}.py, mlservice/model.py, backend/app/{db.py,constants.py,config.py,main.py}; фронт
ChannelsView.vue/DiscoveryView.vue/store.js/api.js; образцы планов этапов 1–5.
Global Constraints
- Проект НЕ git; фиксация — отчёты задач
task-N-report.mdиprogress.mdв.superpowers/sdd/deal-stage6-services/. - .NET 10 SDK; каждая sln собирается 0 warnings/0 errors (
TreatWarningsAsErrors); dev-Postgresdeal-postgres(:5433); curl-приёмка core :5080 (scripts/build.sh/scripts/test.sh— собирают/тестируют толькоsrc/core). - Код-стайл этапов 1–5: 1 тип = 1 файл; XML-doc на public; комментарии на русском; без регионов; без магических
чисел (именованные константы); времена
DateTimeOffset(UTC), наружу epoch-ms; JSON camelCase;{detail}-ошибки. - Сервисы — отдельные sln (
src/{telegram-service,ml-service,ai-service}), ничего общего с core, кроме.protoи NuGet; ни один сервис не ходит в БД тенантов и не знает домен. Core — единственное место с БД и бизнес-логикой. - Vue-фронт,
backend/,mlservice/(python), корневойdocker-compose.ymlНЕ трогаем. - Сервисы подключаются флагами: по умолчанию dev = Local-заглушки (этапы 2–5), реальные сервисы —
UseLocal=false. - Строки ошибок/тостов/причин 1:1 с прототипом (см. задачи): «Telegram не подключён», «Сначала сохраните Telegram api_id и api_hash в настройках», «QR не активен — начните вход по QR», «Неверный код», «Код истёк — запросите новый», «Неверный облачный пароль», «Telegram подключён, сессия сохранена», «Telegram отключён», «Уже вступили в этот источник», «Уже вступили — удалите источник из каналов», «Задача не найдена», «Кандидат не найден» и т.д.
- НЕ выполнять автоматических сетевых подключений к Telegram/LLM в тестах: приёмка сервисов — unit + in-proc gRPC с фейками; живые проверки Telegram помечены «ручная проверка» (нужны api_id/api_hash/QR).
- Новые NuGet в сервисах:
Grpc.AspNetCore,Grpc.Tools,Google.Protobuf,WTelegramClient,Net.Codecrete.QrCodeGenerator,Microsoft.Data.Sqlite; в core:Grpc.AspNetCore,Grpc.Tools,Google.Protobuf,Microsoft.Extensions.Http(есть).
Зафиксированные решения (Rulings этапа)
Сокращения путей: TG= src/telegram-service/, ML= src/ml-service/, AI= src/ai-service/, PR= src/contracts/,
C= src/core/Deal.Contracts/, I= src/core/Deal.Infrastructure/, A= src/core/Deal.Api/, PL= src/core/Deal.Modules.Pipeline/,
KB= src/core/Deal.Modules.Kanban/, ST= src/core/Deal.Modules.Settings/, TM= src/core/Deal.Modules.Telegram/,
DC= src/core/Deal.Modules.Discovery/, T= src/core/tests/Deal.Tests.Unit/, DEP= deploy/compose.dev.yml.
- Ruling 1 (а) — контракты
.proto, кодогенерация, metadata. Три файла:PR/telegram.proto,PR/ai.proto,PR/ml.proto(пакетыdeal.telegram.v1/deal.ai.v1/deal.ml.v1,option csharp_namespaceDeal.Grpc.Telegram/Deal.Grpc.Ai/Deal.Grpc.Ml). Каждый RPC несёт обязательные gRPC-metadata:tenant-id(строка) иservice-token; серверный interceptor (общий шаблон в каждом процессе) проверяетservice-tokenпротив envDEAL_SERVICE_TOKEN(общий в compose; отказ —UNAUTHENTICATED). Каждый сервис проверяет принадлежность по своей модели (сессия/модель тенанта есть — иначеNOT_FOUND/FAILED_PRECONDITION), полю не доверяет. Ошибки домена —INVALID_ARGUMENT/NOT_FOUND/UNAVAILABLEсdetail= текст причины 1:1; FloodWait →RESOURCE_EXHAUSTEDс кодомflood. Кодогенерация — Grpc.Tools: каждый процесс компилирует только свои.protoчерез<Protobuf Include="..\..\contracts\X.proto" GrpcServices="Both" Link="Protos/X.proto"/>(генерация client+server в одном проходе; неиспользуемая сторона игнорируется): telegram-service — telegram.proto, ml/ai-сервисы — свои; core (Deal.Api и Deal.Infrastructure по месту использования) — все три (telegram: сервер Ingress + клиент-гейт; ai/ml: клиенты). Контракты — единственный «язык» между процессами (дизайн-док L145–152). - Ruling 2 (безопасность dev/prod). Dev (этап 6): gRPC без mTLS — plaintext в локальной сети/хосте
(
localhost/compose-сеть) + обязательный service-token вторым фактором. mTLS-сертификаты, их генерация и prod-compose — этап 7 (roadmap L95–97: «compose-prod … безопасность (mTLS…)»); код интерцепторов один и тот же, включение TLS в этап 7 не меняет контракты. Обоснование: 4 процесса + генерация/ротация сертификатов в dev — высокая трудоёмкость без защиты реальных данных; service-token закрывает сценарий «случайный процесс в сети». - Ruling 3 (б) — telegram-service: библиотека и сессии. Библиотека — WTelegramClient (де-факто стандарт
.NET, активная поддержка, API-уровень MTProto; TeleSharp/TLSharp заброшены). Один клиент на тенанта
(
tenantId → WTelegram.Client, 1:1; команды исполняются только на сессии своего тенанта; нет сессии → отказ). Хранение сессий — файлыdata/sessions/<tenantId>.session(session_pathname WTelegramClient; volume в compose). Шифрование at-rest: файл сессии оборачивается AES-GCM (существующий AesGcmSecretCipher-паттерн этапа 2; ключ — envDEAL_TELEGRAM_SESSION_KEY, 32 байта base64): сервис держит расшифрованный файл только в памяти процесса (temp-файл под личным каталогом процесса) и перешифровывает при сохранении/остановке. api_id/api_hash — НЕ env, а настройкаtgKeysтенанта (Settings, шифруется AES-GCM с этапа 2; api-map §4.6 L337); core расшифровывает и передаёт в теле запросов подключения. Внутренний анти-бан сервиса (паузы между сетевыми операциями одной сессии): backfill 1.5–3 с/сообщение и 3–6 с/диалог, поиск 2–4 с (константы telegram.py L35–36, ban_guard.search_pause L78–80); mark-as-read сразу после приёма/чтения. Внешний анти-бан (суточная квота авто-вступлений, паузы 50–70 с, flood-день, стоп-кран) — владение core (воркер Discovery), счётчики в tenant-БД. - Ruling 4 (в) — ml-service: алгоритм и сохраняемость. НЕ ONNX и НЕ ML.NET: переносим инкрементальную
наивно-байесовскую модель по терминам 1:1 с
mlservice/model.py(tokenize L78–87, upsert L105–131, predict L184–293, adaptive margin L42–55, самооценка eval L296–322, status/reset L325–354). Обоснование: (1) python-прототип уже даёт работающее онлайн-обучение на русском тексте без дата-сайентиста, порт-контракт Deal (MlPredictResultDto/status) спроектирован 1:1 под его ответы; (2) ONNX Runtime не умеет онлайн-обучение (нужен экспорт/переобучение вне процесса), ML.NET — не для инкрементального обучения; (3) сохраняемость весов = три таблицы. Хранилище — SQLite-файл на тенантаdata/ml/<tenantId>.sqlite(Microsoft.Data.Sqlite), таблицыclasses(label,n,updated_at)/terms(label,term,count)/eval_log(created_at,expected,predicted,correct)1:1 db-схемы model.py L64–75; запись — транзакциями, batch-вставка терминов (executemany-эквивалент). Пул:ConcurrentDictionary<tenantId, TenantModel>, модель лениво грузится по первому обращению, у каждой — свой lock (predict/learn сериализованы на тенанта). Перенос «мозгов» между инстансами (экспорт/импорт, дизайн-док L176) — по решению владельца НЕ делаем; сохранение между рестартами обязательно (файлы). Пороги: MIN_TOTAL 20, MIN_WINNER 6, MIN_WINNER_SPAM 4, MIN_HITS 2, MARGIN 0.9; адаптивный отрыв 0.35/0.5/0.7 после 400/150/60 примеров; классы типаt:hire/t:order(MIN_TYPE_WINNER 4); веса сигналов 1.0 (пользователь), 0.4 (ИИ), 0.6 (правила) — константы ml_client.py L26–28. - Ruling 5 (г) — ai-service: устройство и контракт с core. ai-service без БД: core передаёт в теле
каждого запроса (1) заполненные промпты (
fill_promptL63–77: подстановка{domain}/{keywords}из настроек тенанта делает core), (2) конфиг активного провайдера (id/base/model/apiKey/api_style — расшифрованный core изaiConfigs), (3) текст. Методы:Filter(промпт aiFilterPrompt, текст) →{pass,reason};Classify(system_prompt = aiPrompt+cardPrompt, user-контекст «Доски + примеры разметки + Сообщение» — собирает core) →{ok,json}— json-строка извлечённого ответа модели (типовая схема ответа задаётся промптом, python держит его сырым dict; строгий маппинг json→AiParsedLeadDtoделает core, 1:1 normalize_stack/clean_budget/ build_contacts/python_store_lead);GenerateKeywords(фикс. промпт L36–47 routes + описание) →{keywords}(очистка_clean_keywordsв core);EvaluateFit(текст + description + keywords задачи, промпт discovery_eval L50–54) →{fit,reason}. Вызовы LLM: OpenAI-совместимыеPOST {base}/chat/completions(Bearer), AnthropicPOST {base}/v1/messages(x-api-key+anthropic-version); temperature 0.2; таймауты 90 с (openai) / 60 с (anthropic); retrymax_retries=2с паузами 0.8/2 с; извлечение JSON из markdown-обёрток (extract_json L175–183); ошибки провайдера наружу какUNAVAILABLEс текстом «ИИ (имя) не ответил корректно — повторите попытку через несколько секунд». Учёт токенов: ответ несётusage{prompt/completion/total}— берётся из usage API-ответа провайдера, при отсутствии оценивается по символам (≈chars/4); core копит в tenant-KVaiTokenUsage(этап 7 — лимиты/бюджеты). Выключатели aiEnabled/aiFilterEnabled читает core (как в воркере этапа 4) — сервис их не знает. - Ruling 6 (д) — core-интеграция ML/AI: флаги, адаптеры, судьба MlOutbox. Секция конфигурации
Services:Ml|Ai→{UseLocal: bool (default true), Endpoint: string}(envSERVICES__ML__USELOCAL=false,SERVICES__ML__ENDPOINT=http://localhost:5103). ВAddDealIntegrationsрегистрируются gRPC-адаптеры (GrpcMlClient: IMlClient,GrpcAiClassifier: IAiClassifier,GrpcAiTools: IAiTools— новый порт, Ruling 9), когдаUseLocal=false, иначе текущие Local-* (фолбэк). Никакой логики переключения в рантайме — выбор на старте. Судьба MlOutbox: PushAsync ВСЕГДА пишет в MlOutbox (этап 3), новый фоновыйMlOutboxFlushScheduler(10 с, per-tenant цикл, эталон PipelineWorkerScheduler) выгружает по 10 строк (ORDER BY created_at), батч ≤100/цикл, вml.proto TrainBatch; удаляет строки только после успеха; при недоступности сервиса строки остаются (python L56–82).ResetAsync: сервис Reset +ClearOutboxAsync(1:1 reset_model L110–124). Кэш статуса сервиса 15 с (python L30–31, refresh_status) →reachableв/api/ml/status; недоступен — Predict → «не уверен», Status → кэш. Счётчики/выключатели/SSE воркера не меняются (Ruling 5 этапа 4; исключения порта воркер уже ловит). Входящий gRPC telegram: сервер в Deal.Api (отдельный порт) — см. Ruling 7. - Ruling 7 (д/ж) — Telegram-ингресс и каталог каналов. Новый чистый модуль
TMDeal.Modules.Telegram— владелец tenant-таблиц (миграцияTenantTelegramконтекста TenantDbContext):Dialogs(Id string PK, Name/Handle/Kind/Hue, Monitor bool, LastText/LastAt, Backfilled bool, UpdatedAt; 1:1 db.py L76–86) иTgMessages(Idm_<dialog>_<msg>PK, DialogId, Text, MsgAt, LeadId nullable; L67–74). ПортITelegramStore+ DTO (диалог §4.8 L349, сообщение превью L351) +DialogsService:List,SetMonitor(первое включение → фон Backfill),SetMonitorAll(1:1 L548–567),SyncFromTelegram(entries)— авто-мониторинг новых поautoMonitorNew, обновление имени/типа, удаление отсутствующих (1:1_persist_dialogsL468–503),MarkBackfilled,SavePreview. Порт-гейтC/Integrations/ITelegramGateway.cs(команды наружу):StatusAsync,StartQrAsync,StartPhoneAsync,SendCodeAsync,SendPasswordAsync,LogoutAsync,RefreshDialogsAsync(→entries),SetMonitorAsync(id, enabled),SetMonitorAllAsync,BackfillAsync(id, force),ReadRecentAsync(id, limit)(превью),SearchAsync,InfoAsync,ReadForEvalAsync(id, limit),JoinAsync(username),LeaveAsync(id); недоступность сервиса → исключение → ветки эндпоинтов как «не подключён». Входящий gRPC в core (серверA/Telegram/TelegramIngressService.cs, RPCPushMessage/SyncDialogs/ReportStatus): kestrel-порт :5082 (envGRPC_INGRESS_PORT), Http2; интерцептор service-token; tenantId из metadata → собственный scope сITenantContext.SetTenant(доверенный источник, не сессия);PushMessage(dialogId/msgId/text/канальные поля/hue/msgAt — hue считает сервис по DIALOG_HUES-палитре) →PipelineIngestService.EnqueueAsync(тот же контракт, что demo-ingest, L7–59) + пишет превью в TgMessages;SyncDialogs→DialogsService.SyncFromTelegram, ответ = актуальный список monitored id (сервис держит зеркало мониторинга в памяти);ReportStatus{phase,connected,listener,account,error,qrUrl}→ KVtgAccount/tgStatus(внутренние ключи SettingsKeys) + из Api-слоя SSEsystem_statusи тосты «Telegram подключён, сессия сохранена»/«Telegram отключён» при переходах фаз (python L178–207). Сервис сам фильтрует события по своему зеркалу monitored (обновляется ответом SyncDialogs и командой SetMonitor) — как python_monitored. - Ruling 8 (ж) — /api/tg и статус. Снимается boot-заглушка
BootStubEndpoints(остаётся в коде до Task 14). Эндпоинты 1:1 api-map §3.3 (13 шт., фронт): статус/start-phone/start-qr/send-code/send-password/logout/qr-image/ dialogs/refresh/monitor-all/backfill-all/{id}/monitor/{id}/backfill(сервер-only)/preview.GET /api/tg/status(§4.9): live-поля (phase/connected/listener/error/qrUrl) из gateway (сервис недоступен → idle-форма), account из KV tgAccount, monitored = count(Dialogs WHERE Monitor), keysSet из настроек.GET /qr-image— SVG через Net.Codecrete.QrCodeGenerator (SVG-first, без внешних зависимостей; 404 «QR не активен — начните вход по QR»). Публикации SSE system_status/toast из Api-слоя (Ruling 5 этапа 3); фронт-флоу 1:1 (store.js L1419–1508). - Ruling 9 (д/е) — Discovery: модуль, таблицы, порт ИИ-инструментов. Новый модуль
DC(чистый) — владелец таблиц (миграцияTenantDiscovery):DiscTasks/DiscCandidates/DiscBlacklist/DiscLog1:1 db.py L136–196 (+idx L177/196; json-колонки marks/topics/keywords text). DTO §4.8 L353–355;IDiscoveryStore; сервисыDiscoveryTasksService/DiscoveryCandidatesService/DiscoveryBlacklistService/DiscoveryLogService+DiscoveryPlanGuard— 1:1 discovery.py: create (имя; plan 1..discJoinLimit; бюджет активных задач, L234–282), patch (рост plan с бюджетом), delete (с кандидатами и логом), start (пустые ключи → 400 «Нет ключевых слов для поиска — добавьте их в задачу»; reset прогресса для done/failed), pause, advance_search, кандидаты (add с исключениями «уже мониторится»/«чёрный список»/«кандидат есть», L409–453; set_candidate; mark_joined/rejected L497–567; blacklist), лог. Новый портC/Integrations/IAiTools.cs:GenerateKeywordsAsync(description)→{ok, keywords, error},EvaluateFitAsync(text, description, keywords)→{fit, reason}— локальные реализации на этапе 6 не нужны (Disco-воркер сам падает в эвристику при сбое/aiEnabled=false, python L187–194); порт реализуется gRPC-адаптеромGrpcAiToolsза тем же флагомServices:Ai:UseLocal=false. - Ruling 10 (е) — Discovery: воркер, оценка, анти-бан.
DiscoveryWorkerScheduler(5 с, per-tenant, эталон PipelineWorkerScheduler) +DiscoveryWorkerService.TickAsync— одно действие за тик, порядок шагов 1:1 discovery_worker.tick L444–484: (1) план достигнут → done+лог; (2) поиск — следующий ключ задачи через gateway.Search (личные чаты/боты пропускаются, kind→channel/group/forum); (3) оценка первогоnewкандидата: info (kind/forum/участники; minSubscribers → skip), чтение выборки (ReadForEval; история недоступна → метки «канал: история недоступна»/«закрытая группа (история скрыта) — вступите сами»; язык ru → skip при «не русский», иначе метка; <3 сообщений → метка «мало сообщений»), фит: ML-спам (Predict через IMlClient, только mlEnabled) → не подходит; ИИ (IAiTools.EvaluateFit, если aiEnabled) → иначе эвристика по ключам; форумы — по темам (group_by_topic; passed — есть проходная тема); вердикт —total>=3 && ratio*100>=threshold(passed L229–237). (4) авто-вступление первогоreviewпри autoJoin: повторная проверка «не состоим» → пауза discJoinDelayMin..Max (core) → join; FloodWait/ошибка → лог flood/error, join_failures (3 → delete); успех → mark_joined(auto), +в Dialogs (монитор on), +фоновый Backfill, −чёрный список. Лимит: авто-вступления за сутки по DiscLog event='join_auto' (UTC) < discJoinLimit; discFloodDay (внутренний KV, ключ SettingsKeys DiscFloodDay — новый) и discPaused стопят сетевые шаги. Метки/поля кандидата и fitRatio 1:1 (L154–173, marks L85–89). - Ruling 11 (е) — эндпоинты Discovery. 1:1 api-map §3.8 (13 шт.): tasks CRUD+start/pause+generate-keywords,
candidates(статус-фильтр), join/reject (ручные, вне квот; ошибки 400 «Уже вступили…»), blacklist, log.
generate-keywords: aiEnabled/ключ-недоступность →
{keywords:[], error}HTTP 200 (мягкие ошибки, api-map L209, «кривое место» п.7), успех —_clean_keywords-фильтр в core (≤30, ≤60 симв., дедуп). Счётчики/статусы задач и кандидатов — как discovery.py. - Ruling 12 (з) — compose и окружение dev. В
DEPдобавляются сервисыtelegram-service/ai-service/ml-service: build изsrc/<svc>/Deal.*.sln(Dockerfile в корне сервиса), порты 5101/5102/5103 на host, volumesdeal_tg_sessions(/data/sessions),deal_ml_data(/data/ml), общий envDEAL_SERVICE_TOKEN; healthcheck — gRPC health (встроенный Grpc.HealthCheck, порт health на том же endpoint). core dev запускается из хоста и ходит наlocalhost:5101..5103(SERVICES__*__ENDPOINT), сервисы ходят в core-ингресс черезSERVICES__CORE__INGRESS=http://host.docker.internal:5082(env). Порядок подъёма не критичен: Local-фолбэки переживают отсутствие сервисов; сквозная приёмка — при поднятых процессах. - Ruling 13 (и/к) — события/безопасность. Новых типов SSE нет: используются system_status/toast (telegram),
существующие new_lead (после карточки — уже в PipelineWorkerScheduler). Аудит команд сервиса
(tenantId, действие, диалог, результат)— структурированные логи Serilog на каждом RPC (этап 7 — аудит-поток); rate-лимиты gRPC-ингресса — этап 7. Ключи/секреты не логируются;DEAL_ENCRYPTION_KEY/DEAL_SERVICE_TOKEN/DEAL_TELEGRAM_SESSION_KEY— только env.
Задачи
Отчёты — task-N-report.md в .superpowers/sdd/deal-stage6-services/. Пути сокращены по Rulings.
Task 1: .proto-контракты telegram/ai/ml + спецификация
Files: Create: PR/telegram.proto, PR/ai.proto, PR/ml.proto, PR/README.md (сервисы/RPC/messages/поля,
metadata tenant-id+service-token, коды ошибок, deadline-рекомендации). telegram.proto: TelegramService
GetStatus/StartQr/StartPhone/SendCode/SendPassword/Logout/RefreshDialogs(→entries[])/SetMonitor/SetMonitorAll/
Backfill/ReadRecent/Search/GetInfo/ReadForEval/Join/Leave + IngressService PushMessage/SyncDialogs/ReportStatus
(контракты Rulings 7). ai.proto: AiService Filter/Classify/GenerateKeywords/EvaluateFit (Ruling 5; usage в каждом
reply). ml.proto: MlService Predict/Status/Reset/TrainBatch (поля 1:1 с MlPredictResultDto/status: classes map,
eval{count,correct,accuracy}, take/label/scores/hits/ready/margin/terms/type).
Источники: Rulings 1/3/5/7; IMlClient/IAiClassifier + Models/*.cs (core Contracts, формы DTO); mlservice/model.py predict/status; ai.py filter_incoming/classify; telegram.py методы (имена L134–873).
Acceptance: файлы + README со схемой каждого RPC (поля/messages/коды) согласованы; контракты валидируются
компиляцией в Task 2–4 (кодогенерация — первый прогон здесь невозможен без csproj). Отчёт: task-1-report.md.
Task 2: Каркас telegram-service (sln, host gRPC, health, service-token)
Files: Create: TG/Deal.Telegram.sln, TG/Deal.Telegram/Deal.Telegram.csproj (link telegram.proto, Server),
TG/Deal.Telegram/Program.cs (Kestrel :5101 Http2; AddGrpc+HealthChecks; env PORT/GRPC_PORT),
TG/Deal.Telegram/ServiceTokenInterceptor.cs, TG/Deal.Telegram/TelegramServiceImpl.cs (заглушки: методы →
UNIMPLEMENTED), TG/Deal.Telegram/Dockerfile, TG/Deal.Telegram.Tests/ (хост поднимается, health OK, запрос без
токена → UNAUTHENTICATED), DEP — запись telegram-service.
Источники: Rulings 1/2/12; эталон gRPC-сервера — настройка AddGrpc/HealthChecks (документация Grpc.AspNetCore).
Acceptance: dotnet build Deal.Telegram.sln 0/0 (доказывает кодогенерацию telegram.proto); юнит-тесты: health
ready; интерцептор отклоняет пустой/неверный токен. Отчёт: task-2-report.md.
Task 3: Каркас ml-service (sln, host gRPC, health)
Files: Create: ML/Deal.Ml.sln, ML/Deal.Ml/Deal.Ml.csproj (link ml.proto Server), ML/Deal.Ml/Program.cs
(Kestrel :5103, env GRPC_PORT), ML/Deal.Ml/ServiceTokenInterceptor.cs, ML/Deal.Ml/MlServiceImpl.cs (заглушки),
ML/Deal.Ml/Dockerfile, ML/Deal.Ml.Tests/ (health; token), запись ml-service в DEP.
Источники: Rulings 1/2/12; Task 2 (эталон).
Acceptance: build 0/0 (кодогенерация ml.proto); тесты health/token PASS. Отчёт: task-3-report.md.
Task 4: Каркас ai-service (sln, host gRPC, health)
Files: Create: AI/Deal.Ai.sln, AI/Deal.Ai/Deal.Ai.csproj (link ai.proto Server), AI/Deal.Ai/Program.cs
(Kestrel :5102, env GRPC_PORT), AI/Deal.Ai/ServiceTokenInterceptor.cs, AI/Deal.Ai/AiServiceImpl.cs (заглушки),
AI/Deal.Ai/Dockerfile, AI/Deal.Ai.Tests/ (health; token), запись ai-service в DEP.
Источники: Rulings 1/2/12; Task 2.
Acceptance: build 0/0 (кодогенерация ai.proto); тесты PASS. Отчёт: task-4-report.md.
Task 5: ml-service — движок инкрементальной модели (per-tenant, SQLite)
Files: Create: ML/Deal.Ml/Model/ModelConstants.cs (пороги Ruling 4), ML/Deal.Ml/Model/MlTokenizer.cs
(снятие ссылок regex + токены [a-zа-яё0-9@+.#]+, len≥3 и «~prefix» len≥6 — 1:1 L78–87),
ML/Deal.Ml/Model/OnlineNaiveBayes.cs (upsert/learn/batch/predict/status/reset/_maybe_eval, математика L184–323:
score термина w<1→1.0 иначе 1+(w−1)/(w+1); prior n/total; best=score+3·prior; adaptive margin; type-решение),
ML/Deal.Ml/Storage/MlDb.cs (Microsoft.Data.Sqlite; EnsureSchema/Tables), ML/Deal.Ml/Model/TenantModel.cs +
ModelPool.cs (lazy-load по тенанту, lock на модель), ML/Deal.Ml/Model/ModelState.cs (состояние: классы/термины/
eval-окно, JSON). Тесты ML/Deal.Ml.Tests/: tokenize; learn→predict спам/колонка; ready-пороги (20/6/4/2);
адаптивный margin; delta<0 «разучивание»; eval-окно (50/200); перезапуск пула сохраняет веса (2-й инстанс на тот
же файл).
Источники: mlservice/model.py целиком; Ruling 4; референс predict-математики L184–293.
Acceptance: build 0/0; тесты PASS (обучение/предсказание на русских примерах: «нужен middle python…» →
колонка/тип; «резюме…» → spam после обучения). Отчёт: task-5-report.md.
Task 6: ml-service — gRPC-сервис поверх пула
Files: Modify: ML/Deal.Ml/MlServiceImpl.cs — Predict/Status/Reset/TrainBatch; tenantId metadata → ModelPool
(модели нет — она создаётся лениво: для Predict отсутствие опыта даёт «не готов» — не ошибка; Ruling 4);
TrainBatch = learn_batch (1 транзакция) → число примеров; Reset — reset модели + пересоздание файла (очистка);
Status — ready/classes/learned/eval 1:1. Тесты: in-proc gRPC (GrpcChannel к тестовому хосту): train → predict;
train-батч из 3; reset обнуляет; неверный service-token → UNAUTHENTICATED.
Источники: mlservice/server.py (эталон форм ответов), model.py status/reset; Rulings 1/4/6.
Acceptance: build 0/0; in-proc gRPC-тесты PASS. Отчёт: task-6-report.md.
Task 7: ai-service — LLM-фасад (OpenAI-совместимые + Anthropic)
Files: Create: AI/Deal.Ai/Llm/LlmConfig.cs (provider: id/name/base/model/key/apiStyle/local), AI/Deal.Ai/Llm/ LlmHttpClient.cs (HttpClientFactory; OpenAI POST {base}/chat/completions Bearer temperature 0.2 max_tokens 8000;
Anthropic POST {base}/v1/messages x-api-key+version; таймауты 90/60 с), AI/Deal.Ai/Llm/LlmRetryPolicy.cs (2
ретрая: 0.8 с/2 с — ai.py L96–117), AI/Deal.Ai/Llm/JsonExtractor.cs (extract_json L175–183),
AI/Deal.Ai/Llm/TokenEstimator.cs (usage провайдера или chars/4), AI/Deal.Ai/Llm/ProviderCaller.cs (ошибки →
AiException с кодом). Тесты: фейковый HttpMessageHandler: OpenAI-ответ; Anthropic-ответ; markdown-обёртка;
usage из ответа и оценка; 3 неудачи → исключение с текстом L115–117; таймаут.
Источники: ai.py _call_openai/_call_anthropic/chat_json/extract_json (L80–183); Ruling 5.
Acceptance: build 0/0; unit-тесты PASS (без сети). Отчёт: task-7-report.md.
Task 8: ai-service — gRPC AiService
Files: Modify: AI/Deal.Ai/AiServiceImpl.cs — Filter (chat_json по фильтр-промпту → pass/reason; при ok=false
из модели — pass:true,skipped? нет: воркер шлёт только при aiFilterEnabled; ошибка → UNAVAILABLE), Classify (json →
reply{ok,json}), GenerateKeywords (промпт Ruling 5 → keywords), EvaluateFit (промпт discovery_eval → fit/reason);
каждый reply + usage. Тесты in-proc: все 4 метода с фейковым провайдером; недоступный провайдер → UNAVAILABLE.
Источники: ai.py L188–258; discovery_routes L36–47/189–211; discovery_eval L50–54/153–194; Rulings 1/5.
Acceptance: build 0/0; in-proc тесты PASS. Отчёт: task-8-report.md.
Task 9: telegram-service — сессии, подключение, QR, статус
Files: Create: TG/Deal.Telegram/Sessions/TgOptions.cs (session dir, DEAL_TELEGRAM_SESSION_KEY), TG/Deal.Telegram/ Sessions/SessionFileCipher.cs (AES-GCM обёртка файла), TG/Deal.Telegram/Sessions/TenantSession.cs (id тенанта,
клиент WTelegramClient, состояние), TG/Deal.Telegram/Sessions/SessionFarm.cs (пул 1 акк/тенант, auto_resume на
старте — авторизованная сессия → ready, L209–222), TG/Deal.Telegram/Telegram/ClientFactory.cs (конфиг:
api_id/api_hash из запроса; session_pathname; внутренние паузы). Реализация методов: StartQr/StartPhone/SendCode/
SendPassword/Logout/GetStatus (фазы idle|phone|code|password|qr|ready, account, qrUrl; heartbeat/авто-возобновление
фоновым циклом 30 с). Тесты: cipher roundtrip; farm: tenant-изоляция (нет сессии → отказ); фазовые переходы на
fake-клиенте (абстракция ISessionClient); ручная проверка QR — отдельно.
Источники: telegram.py L82–222, L286–329; config.py L31 (SESSIONS_DIR); Rulings 1/3.
Acceptance: build 0/0; unit PASS. ⚠ Ручная проверка: реальный QR-вход/код/2FA с кредов (api_id/api_hash),
auto_resume после рестарта контейнера. Отчёт: task-9-report.md.
Task 10: telegram-service — диалоги, мониторинг, backfill, поток в core
Files: Create: TG/Deal.Telegram/Dialogs/DialogCatalog.cs (зеркало monitored-набора тенанта: SetMonitor/
SetMonitorAll/актуализация ответом SyncDialogs), TG/Deal.Telegram/Dialogs/RealtimeListener.cs (NewMessage →
фильтр по зеркалу → PushMessage в core; mark-as-read sendReadAcknowledge; сохранение last_text? нет — только пуш),
TG/Deal.Telegram/Dialogs/BackfillService.cs (последние 10 с паузами 1.5–3 с/сообщение и 3–6 с/диалог; read-ack;
реверс-порядок от старых к новым; force; L331–390), TG/Deal.Telegram/Dialogs/RealtimeSweep.cs (30 с: догон
непрочитанных по unread_count, паузы, read-ack, L392–456), TG/Deal.Telegram/Core/CoreIngressClient.cs (gRPC-клиент
к SERVICES__CORE__INGRESS; PushMessage/SyncDialogs; сбой — лог, упущенное догоняет sweep). Реализация RPC
RefreshDialogs/SetMonitor/SetMonitorAll/Backfill/ReadRecent (превью, свежие из TG). Тесты: фильтр мониторинга;
backfill-паузы (fake clock); PushMessage-клиент к in-proc fake-серверу ингресса.
Источники: telegram.py L244–283, L331–456, L505–620; Rulings 3/7.
Acceptance: build 0/0; unit/in-proc PASS (без реальной сети). ⚠ Ручная проверка: refresh/подписка/backfill
живого аккаунта. Отчёт: task-10-report.md.
Task 11: telegram-service — discovery-операции (search/info/read/join)
Files: Create: TG/Deal.Telegram/Discovery/DiscoveryOps.cs — Search (contacts.SearchRequest, пауза 2–4 с,
кэш entities, выходные id подписанные, kind «канал»/«группа»/«чат», L624–664), GetInfo (participants via
GetFullChannel/GetFullChat, is_forum, L666–716), ReadForEval (обычная лента; форумы — темы GetForumTopics + на
тему get_messages(reply_to), per-topic 3..10, cap 5 тем; ошибки → ok:false no_history; L718–816), Join (по
username, FloodWait → RpcException RESOURCE_EXHAUSTED + код flood, L818–839), Leave (L841–848). Тесты: нормализация
kind/username; формат ответов (fake-слой TL не трогаем — тесты на чистых мапперах ответов).
Источники: telegram.py L622–873; ban_guard.search_pause; Rulings 3/7.
Acceptance: build 0/0; unit PASS (мапперы/валидация). ⚠ Ручная проверка: поиск/чтение/join живого аккаунта.
Отчёт: task-11-report.md.
Task 12: core — gRPC-ингресс telegram (PushMessage/SyncDialogs/ReportStatus)
Files: Modify: A/Program.cs (второй Kestrel-listen :5082, Http2, GRPC_INGRESS_PORT; AddGrpc; AddAuthentication
не нужен — интерцептор), A/Infrastructure/ не трогаем. Create: A/Telegram/IngressServiceTokenInterceptor.cs,
A/Telegram/TelegramIngressService.cs (Grpc Deal.Grpc.Telegram.IngressServiceBase): PushMessage → scope с
SetTenant(metadata tenant-id) → PipelineIngestService.EnqueueAsync (+ ITelegramStore.SavePreview) → reply
{accepted/duplicate}; SyncDialogs → DialogsService.SyncFromTelegram → reply{monitoredIds}; ReportStatus → KV
tgStatus/tgAccount + публикация (через DI Api-слоя) SSE system_status/тостов на переходах фаз. Create:
A/Telegram/TelegramIngressAuth.md? нет. Тесты (in-proc WebApplicationFactory+gRPC-канал): PushMessage кладёт
строку очереди тенанта (эмуляция входящего сообщения — сквозная проверка без Telegram); неверный токен → отказ;
PushMessage для несуществующего тенанта не падает (нет схемы → ошибка ловится, reply not-accepted).
Источники: Rulings 7/13; PipelineIngestService L7–59; паттерн scope/SetTenant — PipelineWorkerScheduler L169–213.
Acceptance: build 0/0; тесты PASS (см. выше). Отчёт: task-12-report.md.
Task 13: core — модуль Telegram (таблицы, DTO, порт, DialogsService)
Files: Create: TM/.../TelegramModuleMarker.cs, I/Persistence/Entities/{DialogEntity,TgMessageEntity}.cs +
конфигурации (JSON не нужен; индексы DialogId/MsgAt), TM/Application/Models/{TelegramDialogDto,TelegramMessageDto,TgStatusDto}.cs,
TM/Application/ITelegramStore.cs, TM/Application/DialogsService.cs (List/SetMonitor/SetMonitorAll/SyncFromTelegram/
MarkBackfilled/SavePreview — Ruling 7), TM/Application/TelegramModuleRegistrar.cs, I/Persistence/Repositories/ TelegramStore.cs, C/Integrations/ITelegramGateway.cs (Ruling 7). Modify: I/Persistence/TenantDbContext.cs —
DbSet Dialogs/TgMessages + ApplyConfiguration. EF: миграция TenantTelegram для TenantDbContext
(dotnet ef migrations add TenantTelegram --context TenantDbContext --output-dir Migrations/TenantDb --project src/core/Deal.Infrastructure --startup-project src/core/Deal.Api; старт Api применяет к дефолтной схеме).
csproj-ссылки: TM → ST (настройки) + Contracts; реестр AddTelegramModule() в Api. Тесты: SyncFromTelegram (новый+autoMonitorNew/обновление/удаление отсутствующих),
SetMonitor-семантика; порт-контракт гейта.
Источники: db.py L67–86; telegram.py _persist_dialogs/list_dialogs/set_monitor* L468–581; api-map §4.8 L349–351;
Rulings 7/8.
Acceptance: build 0/0; миграция применяется к дефолтной схеме; тесты PASS. Отчёт: task-13-report.md.
Task 14: core — эндпоинты /api/tg (каналы, статус, QR) + SSE; замена boot-заглушки
Files: Modify: A/Program.cs (+MapTelegramEndpoints), A/Endpoints/BootStubEndpoints.cs (удаляется вызов
MapBootStubEndpoints, файл — delete). Create: A/Endpoints/TelegramEndpoints.cs (13 шт. api-map §3.3; тела
запросов — record'ы; ошибки гейта → {detail} 400; refresh → upsert через DialogsService и ответ {ok,count} или
{ok:false, reason:"not-connected", count:0}; monitor/backfill по Ruling 8 с фоновым backfill-спуском при первом
включении), A/Endpoints/QrImageEndpoint.cs (SVG Net.Codecrete; 404 «QR не активен — начните вход по QR»),
A/Telegram/TgStatusService.cs (сборка §4.9: гейт+KV+monitored count+keysSet), события SSE/toast при ReportStatus
(из Task 12). Tests: юнит-тесты TgStatusService (сервис недоступен → idle-форма); curl-приёмка эндпоинтов со
стаб-гейтом (фейк-реализация ITelegramGateway в тестах, не Local).
Источники: api-map §3.3/§4.9; tg_routes.py целиком (тексты и статусы); store.js L1352–1508 (фронт-флоу); Rulings 7/8.
Acceptance: build 0/0; юнит+curl: GET /api/tg/status (idle без гейта), dialogs, monitor, backfill-all, preview,
start-qr (фейк) → phase/qrUrl; 401 без куки. Отчёт: task-14-report.md.
Task 15: core — ai-интеграция: контекст запроса, GrpcAiClassifier/GrpcAiTools, маппер, usage
Files: Modify: C/Integrations/IAiClassifier.cs — контракт остаётся, НО Classify/Filter переходят на
запросные record'ы: ClassifyAsync(AiClassifyRequest, ct), FilterAsync(AiFilterRequest, ct) (в C/Integrations/
Models/: AiClassifyRequest{Text, SystemPrompt, UserContext}, AiFilterRequest{Text, SystemPrompt}); сигнатуры
LocalAiClassifier адаптируются (строит запрос сам: Filter — skipped; Classify — локальный разбор, Ruling 5 этапа 4).
Modify: PL/Application/PipelineWorkerService.cs — call-site'ы фильтра/классификации переходят на новые сигнатуры
через AiClassifyContextBuilder (Логика веток/выключателей/обучения ML не меняется — Ruling 5 этапа 4/6).
Create: PL/Application/AiClassifyContextBuilder.cs (fill_prompt 1:1 ai.py L63–77; доски non-suggested с правилами/
ключами — python L226–243; примеры разметки по CardMoves/learning-истории, ≤8, L201–215), PL/Application/ AiRawLeadMapper.cs (json-ответ модели → AiParsedLeadDto 1:1 python: title ≤140, стек normalize, бюджет
BudgetNormalizer=clean_budget L316–339, контакты ContactsQualifier=build_contacts L389–421, типы/spam/board;
доску решает CardComposer BoardAccepts — как сейчас), I/Integrations/GrpcAiClassifier.cs,
I/Integrations/GrpcAiTools.cs (IAiTools: GenerateKeywords/EvaluateFit; usage→KV aiTokenUsage — SettingsKeys
новый внутренний ключ), I/Integrations/LocalAiTools.cs (для UseLocal: методы не поддерживаются → исключение/
пустой результат — воркер Discovery сам выбирает эвристику), регистрация в AddDealIntegrations по флагу
Services:Ai (Ruling 6). Tests: контекст-билдер (промпты/доски/примеры); маппер json→DTO (бюджет «2к»/валюты/
контакты); адаптеры (in-proc gRPC ai-service); Local-фолбэк.
Источники: ai.py L61–258; pipeline.py _store_lead L433–514; CardComposer; Rulings 5/6.
Acceptance: build 0/0; тесты PASS; воркер с GrpcAiClassifier (UseLocal=false) проходит фильтр/классификацию
против in-proc ai-service. Отчёт: task-15-report.md.
Task 16: core — ml-интеграция: GrpcMlClient + MlOutboxFlushScheduler
Files: Create: I/Integrations/GrpcMlClient.cs (IMlClient: Predict/Status/Reset/PushAsync — Push остаётся
записью в MlOutbox через IMlLearningStore как LocalMlClient; Predict → gRPC, сбой → NotReadyPrediction;
Status → service-статус + кэш 15 с (reachable), статистика из KV/таблиц; Reset → gRPC Reset + ClearOutbox),
A/Hosting/MlOutboxFlushScheduler.cs (10 с per-tenant; по 10 строк, ≤100 за цикл, TrainBatch; delete после успеха;
эталон PipelineWorkerScheduler). Регистрация по флагу Services:Ml (Ruling 6). Modify: I/Integrations/ LocalMlClient.cs — не трогаем (фолбэк); ST/Application/SettingsKeys.cs — внутренние ключи AiTokenUsage/
DiscFloodDay/TgStatus/TgAccount. Tests: flush (фейк-gRPC): 25 строк → 3 батча, строки удалены, сбой → строки
остались; reset; reachable false при недоступности; predict-fallback.
Источники: ml_client.py L30–31/56–135; Rulings 4/6; LocalMlClient (эталон Push/Status).
Acceptance: build 0/0; тесты PASS. Отчёт: task-16-report.md.
Task 17: core — Discovery: таблицы, порт, сервисы задач/кандидатов/чёрного списка/лога
Files: Create: DC/Application/Models/*.cs (§4.8 DTO: задача L353, кандидат L355, чёрный список, лог),
DC/Application/DiscoveryIdPrefixes.cs (dt_/dl_), DC/Application/IDiscoveryStore.cs, DC/Application/ DiscoveryTasksService.cs (create/patch/delete/start/pause/advance/bump, план-бюджет 1:1 L234–381),
DC/Application/DiscoveryCandidatesService.cs (add с исключениями, set_candidate, mark_joined/mark_rejected,
delete; метки/топики JSON), DC/Application/DiscoveryBlacklistService.cs, DC/Application/DiscoveryLogService.cs,
DC/Application/DiscoveryModuleRegistrar.cs; миграция TenantDiscovery (TenantDbContext — DbSet DiscTasks/DiscCandidates/DiscBlacklist/DiscLog +
ApplyConfiguration; команда как в Task 13); I/Persistence/Repositories/DiscoveryStore.cs; csproj DC → ST + Contracts. Tests: валидации (имя/бюджет/план), start без ключей, исключения
add_candidate, mark_joined→joined/autoJoined/счётчики, blacklist-перезапись rejected.
Источники: discovery.py (создание/кандидаты/чёрный список/лог L234–608), db.py L136–196, api-map §3.8/§4.8; Rulings 9/10.
Acceptance: build 0/0; миграция применяется; тесты PASS. Отчёт: task-17-report.md.
Task 18: core — Discovery-воркер (5 с): поиск/оценка/авто-join, бан-гард
Files: Create: DC/Application/DiscoveryBanGuard.cs (лимит дня по DiscLog join_auto за UTC-сутки; discFloodDay;
discPaused; wait-пауза из настроек 1:1 ban_guard.py), DC/Application/DiscoveryLangDetector.cs (detect_lang_ru
L62–83), DC/Application/DiscoveryEvaluator.cs (фит: короткие → нет; ML-спам при mlEnabled (IMlClient.Predict);
ИИ EvaluateFit при aiEnabled (IAiTools), сбой → эвристика; форумы по темам — group_by_topic L96–117 + passed
L229–237), DC/Application/DiscoveryWorkerService.cs (шаги 1–4 tick L444–484 через ITelegramGateway; маркеры и
логи 1:1; join_failures=3→delete), A/Hosting/DiscoveryWorkerScheduler.cs (5 с, per-tenant, эталон
PipelineWorkerScheduler). Тесты: бан-гард (лимит/флуд/пауза), оценка (язык/фит/порог/форумы/метки), воркер-шаги с
фейковым гейтом (search→candidate; eval→review; join с паузами; план выполнен → done).
Источники: discovery_worker.py целиком; discovery_eval.py целиком; ban_guard.py целиком; Rulings 9/10.
Acceptance: build 0/0; тесты PASS. Отчёт: task-18-report.md.
Task 19: core — эндпоинты /api/discovery + generate-keywords; curl-приёмка
Files: Create: A/Endpoints/DiscoveryEndpoints.cs — 13 эндпоинтов api-map §3.8: tasks (list/create/patch/delete/
start/pause), generate-keywords (мягкая ошибка HTTP 200 {keywords:[], error}; _clean_keywords в core),
candidates(фильтр), join (ручной: валидация статуса, Join→add в Dialogs monitor→фон backfill→mark_joined(auto:false)
→remove_blacklist, ошибки 400 с текстом), reject (→blacklist reason «отклонено вручную»), blacklist list/delete,
log. Curl-приёмка discovery: создание задачи → start (после добавления ключей) → симуляция работы воркера
(фейк-гейт в тестовом host) → кандидаты new/review → reject → blacklist → лог; 404/400 ветки.
Источники: discovery_routes.py целиком; store.js L2171–2370; Rulings 9/11.
Acceptance: build 0/0; curl PASS (или тестовая приёмка) по сценарию выше. Отчёт: task-19-report.md.
Task 20: compose-dev, сквозная интеграция и финал этапа
DEP: сервисы из Task 2–4 доводятся (healthcheck gRPC, volumes, envDEAL_SERVICE_TOKEN, ingress env);docker compose -f deploy/compose.dev.yml configвалиден; локальный подъём всех процессов (ручной шаг — docker).- Сквозная эмуляция (без реального Telegram/LLM): подняты core+3 сервиса (
SERVICES__*__USELOCAL=false); gRPC-вызов PushMessage в core (клиент-эмулятор, скриптscripts/grpc-emit.ps1/.shна grpcurl или тест-проект) → очередь → admin/tick → карточка (new_lead); ml: TrainBatch →/api/ml/statusпоказывает ready; ai: фильтр/классификация через фейковый OpenAI-сервер? НЕТ — ai-сервис без ключа отдаёт UNAVAILABLE, воркер падает в локальный разбор (проверяем); затемSERVICES__AI__USELOCAL=true— фолбэк жив. - Обновить
docs/technical/Техническая-документация-Дейл.md(сервисы/порты/gRPC-контракты, каналы-вкладка, Discovery, флаги, ml-модель и веса) и roadmap (этап 6 → «Выполнено», ограничения этапа 7). - Полный прогон:
scripts/build.sh+scripts/test.sh(620 + новые PASS), build каждой sln 0/0. - Отчёт
task-20-report.md+ финальная строкаprogress.md.
Источники: Rulings 2/12/13; compose.dev.yml (эталон minio-записи); паттерны отчётов этапов 1–5.
Acceptance: см. пункты выше; любые живые проверки Telegram/LLM — ⚠ ручные, по возможности, с кредами.
Self-Review
- Spec coverage: прото-контракты (а) — Task 1 + Rulings 1/3/5/7; каркасы сервисов — Task 2–4; ml-алгоритм/ сохраняемость — Task 5/6 (Ruling 4); ai-фасад/промпты/таймауты/токены — Task 7/8/15 (Ruling 5); core gRPC-клиенты за флагом и судьба MlOutbox — Task 15/16 (Ruling 6); входящий telegram-gRPC→EnqueueAsync — Task 12 (Ruling 7); замена Local-заглушек с фолбэком — Task 15/16/20; Discovery (таблицы/воркер/оценка/чёрный список/квоты/история/ генерация ключей/эндпоинты) — Task 17/18/19 (Rulings 9–11); каналы-эндпоинты и QR/статус/марк-as-рид/мониторинг/ «Перечитать»/ключи/авто-мониторинг — Task 10/13/14 (Rulings 3/7/8); SSE system_status/toast/new_lead — Task 12/14 (Ruling 13); compose-dev — Task 2–4/20; безопасность dev (service-token, mTLS-решение) — Rulings 1/2, Task 2–4/12. Roadmap-скоуп (L82–91) покрыт; ТЗ §4/§5/§8 — через api-map/референсы выше.
- Placeholder scan: TODO/«добавьте обработку» нет; «ручная проверка» — явно помеченные живые проверки с кредами (задачи 9/10/11/20), авто-приёмка — эмуляция ингресса и фейки. Onnx/TeleSharp альтернативы не оставлены «на потом» — зафиксированы решения (Rulings 3/4). IColumnSuggester (LocalColumnSuggester) сознательно НЕ заменяется gRPC (эвристика читает карточки тенанта в ядре; ai-service участвует только через IAiTools GenerateKeywords — Kanban-suggest остаётся локальным, api-map L120–121 без изменений) — это решение, не TODO.
- Type consistency: имена контрактов и методы: IAiClassifier переходит на запросные record'ы (Task 15) — воркер Pipeline (Ruling 5 этапа 4) вызывает ClassifyAsync/FilterAsync; адаптеры Local/Grpc реализуют один порт; IMlClient не меняет сигнатур (Predict/Status/Reset/Push) — GrpcMlClient/LocalMlClient взаимозаменяемы; новые внутренние SettingsKeys (AiTokenUsage/DiscFloodDay/TgStatus/TgAccount) добавляются в ST-каталог как внутренние; ITelegramGateway (Task 13) реализуется клиентом Task 12–14 и потребляется эндпоинтами/воркером Discovery (Task
- — единый список методов Ruling 7;
QueuedMessage(контракт ингресса) тот же, что у demo-ingest;MlPredictResultDto/status-поля 1:1 с ml.proto (Task 1/6). Циклов ссылок нет: TM→ST+Contracts; DC→ST+Contracts; TM/DC не знают друг о друге; Api оркестрирует.
- Вне scope этапа 6: mTLS-сертификаты и prod-compose (этап 7); лимиты/бюджеты токенов (учёт уже есть); оператор/админка/аудит-поток; экспорт/импорт ML-моделей (решение владельца); мультиаккаунтность на тенанта; события pipeline_stats/boards_changed/leads_reclassified (фронт не слушает); reclassify ИИ-переклассификации на реальном ИИ (контракт-заглушка остаётся; реальный вызов — вместе с операторским контуром этапа 7); шифрование сессий и их бэкап-интеграция (сессии шифруются файлово, но ротация ключей/бэкап-политика — этап 7).