Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!--
|
||||
Deal.Proto — общий проект кодогенерации .proto-контрактов этапа 6 (Ruling 1).
|
||||
|
||||
Компилирует все три контракта (telegram/ml/ai.proto) через Grpc.Tools с
|
||||
генерацией и клиентской, и серверной стороны (GrpcServices="Both"): сборка
|
||||
этого проекта — первый прогон кодогенерации и валидации .proto до создания
|
||||
сервисных проектов (Task 2–4). Сгенерированные типы живут в пространствах
|
||||
имён из option csharp_namespace (Deal.Grpc.Telegram/Deal.Grpc.Ml/Deal.Grpc.Ai)
|
||||
и переиспользуются core и сервисами через ProjectReference.
|
||||
|
||||
Сборка: 0 warnings / 0 errors (TreatWarningsAsErrors), как остальные sln.
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<AssemblyName>Deal.Proto</AssemblyName>
|
||||
<RootNamespace>Deal.Proto</RootNamespace>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Кодогенерация: protoc + плагин gRPC C# (только для сборки). -->
|
||||
<PackageReference Include="Grpc.Tools" Version="2.83.0" PrivateAssets="All" />
|
||||
<!-- Рантайм сгенерированных сообщений (MessageParser/ByteString и т.п.). -->
|
||||
<PackageReference Include="Google.Protobuf" Version="3.35.1" />
|
||||
<!-- Типы gRPC C#-стабов (Grpc.Core.*): нужны сгенерированному коду при
|
||||
компиляции; транспорты (Grpc.Net.Client/Grpc.AspNetCore) подключают
|
||||
проекты-потребители по месту (Ruling 1, список NuGet этапа). -->
|
||||
<PackageReference Include="Grpc.Core.Api" Version="2.83.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Контракты этапа 6; файлы лежат рядом с проектом (src/contracts). -->
|
||||
<Protobuf Include="telegram.proto" GrpcServices="Both" />
|
||||
<Protobuf Include="ml.proto" GrpcServices="Both" />
|
||||
<Protobuf Include="ai.proto" GrpcServices="Both" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,204 @@
|
||||
# Контракты gRPC этапа 6 (`src/contracts`)
|
||||
|
||||
Контракты между ядром Deal и сервисами telegram/ml/ai (отдельные процессы).
|
||||
Источники: дизайн-док §6.2 (L145–152), план «Дейл — Этап 6: Сервисы …»
|
||||
(Task 1, Rulings 1/3/5/7), api-map §3.3/§3.7/§3.8/§4.8/§4.9/§4.10, прототип
|
||||
`backend/app/services/*.py` и `mlservice/model.py`.
|
||||
|
||||
Контракты — **единственный «язык» между процессами** (Ruling 1): сервисы не
|
||||
делят с ядром ничего, кроме этих `.proto` и NuGet.
|
||||
|
||||
| Файл | Пакет | `csharp_namespace` | Сервисы |
|
||||
|---|---|---|---|
|
||||
| `telegram.proto` | `deal.telegram.v1` | `Deal.Grpc.Telegram` | `TelegramService`, `IngressService` |
|
||||
| `ml.proto` | `deal.ml.v1` | `Deal.Grpc.Ml` | `MlService` |
|
||||
| `ai.proto` | `deal.ai.v1` | `Deal.Grpc.Ai` | `AiService` |
|
||||
|
||||
## Кодогенерация
|
||||
|
||||
- `Deal.Proto.csproj` — общий classlib: компилирует все три `.proto` через
|
||||
`Grpc.Tools` (`<Protobuf Include=... GrpcServices="Both"/>`), т.е. генерирует
|
||||
и клиент, и сервер каждого сервиса в одном проходе (неиспользуемая сторона
|
||||
игнорируется потребителем). Сборка проекта = валидация `.proto` (protoc) и
|
||||
первый прогон кодогенерации (Task 1); Task 2–4 подключают файлы к сервисам
|
||||
`ProjectReference`-ом либо `<Protobuf Include="..\..\contracts\X.proto">`.
|
||||
- Пакеты: `Grpc.Tools` (PrivateAssets), `Google.Protobuf`, `Grpc.Core.Api`.
|
||||
Транспорты — по месту: `Grpc.AspNetCore` (серверы), `Grpc.Net.Client`/клиенты.
|
||||
- Сгенерированные типы: `obj/…/{Telegram,TelegramGrpc,Ml,MlGrpc,Ai,AiGrpc}.cs`,
|
||||
пространства имён из `option csharp_namespace`.
|
||||
- Проекты-потребители: core (клиенты ML/AI; telegram-клиент-гейт + сервер
|
||||
ингресса), telegram-service/ml-service/ai-service — добавляются в Task 2–4+.
|
||||
|
||||
## Metadata (все RPC, обязательны)
|
||||
|
||||
| Заголовок | Значение | Отказ |
|
||||
|---|---|---|
|
||||
| `tenant-id` | id тенанта (строка). **Единственный** источник принадлежности; полю в теле не доверяем | отсутствует → `UNAUTHENTICATED` (для ингресса core: `SetTenant` из metadata) |
|
||||
| `service-token` | общий токен сервисов (env `DEAL_SERVICE_TOKEN`, общий в compose) | пустой/неверный → `UNAUTHENTICATED` |
|
||||
|
||||
Интерцептор `service-token` — общий шаблон в каждом процессе (Ruling 1/2);
|
||||
каждый сервис дополнительно проверяет принадлежность по своей модели (сессия/
|
||||
модель тенанта есть, иначе `NOT_FOUND`/`FAILED_PRECONDITION`).
|
||||
|
||||
## Коды ошибок (общие)
|
||||
|
||||
Ошибки домена — gRPC-статусы, `detail` = текст причины 1:1 с прототипом
|
||||
(строки «Telegram не подключён», «Неверный код», «Код истёк — запросите новый»,
|
||||
«Неверный облачный пароль», «ИИ (имя) не ответил корректно — …» и т.д.):
|
||||
|
||||
| Статус | Когда |
|
||||
|---|---|
|
||||
| `INVALID_ARGUMENT` | невалидный ввод/код/пароль/username, пустой текст |
|
||||
| `NOT_FOUND` | диалог/сущность/сессия тенанта не найдены |
|
||||
| `FAILED_PRECONDITION` | операция невозможна в текущей фазе (нет сессии и т.п.) |
|
||||
| `RESOURCE_EXHAUSTED` | FloodWait Telegram (detail начинается с префикса `flood`) |
|
||||
| `UNAVAILABLE` | недоступен Telegram/LLM-провайдер/хранилище модели (безопасный повтор/фолбэк) |
|
||||
| `UNAUTHENTICATED` | неверный/отсутствующий `service-token` |
|
||||
|
||||
«Мягкие» сценарии НЕ являются ошибками RPC: Predict неготовой модели («не
|
||||
уверен»), ReadForEval без истории (`ok=false, error="no_history"`), сброс с
|
||||
`ok=false,error`, `filter` с решением pass/reason.
|
||||
|
||||
## Deadlines (клиент)
|
||||
|
||||
| Сфера | Рекомендация | Обоснование |
|
||||
|---|---|---|
|
||||
| telegram: GetStatus/SetMonitor/SetMonitorAll/Logout | 10 с | локальная сеть/статус |
|
||||
| telegram: StartPhone/StartQr/SendCode/SendPassword/Search/GetInfo/ReadRecent/ReadForEval/Join/Leave | 60 с | сетевые операции Telegram (паузы анти-бана 2–4 с поиск) |
|
||||
| telegram: RefreshDialogs/Backfill | 120 с | iter_dialogs 500; backfill 10 сообщ. × 1.5–3 с + 3–6 с/диалог |
|
||||
| ingress: PushMessage/SyncDialogs/ReportStatus | 10 с | локальная сеть; упущенное догоняет realtime-sweep |
|
||||
| ml: Predict | 5 с | локальная модель |
|
||||
| ml: Status/Reset | 10 с | локально |
|
||||
| ml: TrainBatch | 30 с | батч ≤100, 1 транзакция |
|
||||
| ai: все RPC | 120 с | провайдер 90/60 с + ретраи 0.8/2 с |
|
||||
|
||||
---
|
||||
|
||||
## `telegram.proto`
|
||||
|
||||
Два сервиса: команды ядра → сервис (`TelegramService`) и поток сервис → ядро
|
||||
(`IngressService`, gRPC-сервер в Deal.Api :5082, Ruling 7).
|
||||
|
||||
### Словари значений
|
||||
|
||||
- `phase`: `idle | phone | code | password | qr | ready` (status() L85).
|
||||
- `kind`: `channel | group | forum | chat` (канон контракта; 1:1 `_kind_of`
|
||||
L461–466: broadcast → channel, megagroup/gigagroup/group → group, остальное →
|
||||
chat; `forum` — отдельный флаг `is_forum` в GetInfo, в каталоге форум приходит
|
||||
как group). Discovery-коды `channel/group/forum` ядро получает из kind+is_forum.
|
||||
- `error`/`qr_url`/`account` — `optional` (presence): пустое = нет ошибки/URL.
|
||||
|
||||
### Маппинги на HTTP-контракт (заметка для T13/14, код не меняется)
|
||||
|
||||
Внутренний канон контракта `kind` — EN (`channel/group/forum/chat`). Граница
|
||||
HTTP-эндпоинтов (api-map §4.8 L349, замороженный контракт фронта) НЕ 1:1:
|
||||
- `GET /api/tg/dialogs` → `item.type` остаётся **русским** («канал»/«группа»/
|
||||
«чат»), как в прототипе (`refresh_dialogs`/`list_dialogs`): на границе
|
||||
эндпоинта каналов (T13/14) нужен обратный маппинг EN → RU
|
||||
(channel→«канал», group/forum→«группа», chat→«чат»; forum в списке диалогов
|
||||
не встречается — каталог приносит его как group).
|
||||
- Discovery: `candidate.type` (`kind`) — **EN** (`channel/group/forum`), как в
|
||||
прототипе (db.py L162–166, `_kind_code`); маппинг на границе НЕ нужен.
|
||||
|
||||
### TelegramService (ядро — клиент, сервис — сервер)
|
||||
|
||||
| RPC | Запрос | Ответ | Ошибки / примечания |
|
||||
|---|---|---|---|
|
||||
| `GetStatus` | `GetStatusRequest` (пуст) | `GetStatusReply{phase,connected,listener,account,error?,qr_url?}` | нет сессии → FAILED_PRECONDITION «Telegram не подключён». live-поля для `GET /api/tg/status`; monitored/keysSet ядро считает само (Ruling 8) |
|
||||
| `StartPhone` | `StartPhoneRequest{phone, api_id, api_hash}` | `StartPhoneReply{phase}` | ключи tgKeys передаёт ядро (Ruling 3); нет ключей → INVALID_ARGUMENT «Сначала сохраните…»; ответ фаза `code` |
|
||||
| `StartQr` | `StartQrRequest{api_id, api_hash}` | `StartQrReply{phase, qr_url}` | фаза `qr` + url; уже авторизован → `ready`, url пуст |
|
||||
| `SendCode` | `SendCodeRequest{code}` | `SendCodeReply{phase}` | «Неверный код»/«Код истёк…» → INVALID_ARGUMENT; 2FA → фаза `password` |
|
||||
| `SendPassword` | `SendPasswordRequest{password}` | `SendPasswordReply{phase}` | «Неверный облачный пароль» → INVALID_ARGUMENT; ответ `ready` |
|
||||
| `Logout` | `LogoutRequest` (пуст) | `LogoutReply{ok}` | отключение + удаление сессии тенанта |
|
||||
| `RefreshDialogs` | `RefreshDialogsRequest` (пуст) | `RefreshDialogsReply{entries: DialogEntry[]}` | каталог диалогов; применять ядру через Ingress.SyncDialogs-семантику (`SyncFromTelegram`) |
|
||||
| `SetMonitor` | `SetMonitorRequest{dialog_id, enabled}` | `SetMonitorReply{ok, enabled}` | зеркало monitored в сервисе; первый backfill запускает ядро |
|
||||
| `SetMonitorAll` | `SetMonitorAllRequest{enabled}` | `SetMonitorAllReply{ok, count, enabled}` | count = диалогов в каталоге |
|
||||
| `Backfill` | `BackfillRequest{dialog_id, force}` | `BackfillReply{processed}` | последние ~10 сообщений → поток PushMessage; паузы 1.5–3 с/сообщ. + mark-as-read; force = «Перечитать» |
|
||||
| `ReadRecent` | `ReadRecentRequest{dialog_id, limit 1..50}` | `ReadRecentReply{messages: PreviewMessage[]}` | свежие из TG; lead и фолбэк на БД — в ядре |
|
||||
| `Search` | `SearchRequest{query, limit}` | `SearchReply{results: DialogEntry[]}` | пауза анти-бана внутри сервиса; личные/ботов отсеивает ядро (Ruling 10) |
|
||||
| `GetInfo` | `GetInfoRequest{dialog_id}` | `GetInfoReply{info: ChannelInfo}` | ChannelInfo{id,name,username,kind,hue,participants?,is_forum}; сбои full_chat не роняют RPC |
|
||||
| `ReadForEval` | `ReadForEvalRequest{dialog_id, limit}` | `ReadForEvalReply{ok, error?, messages: EvalMessage[]}` | форумы — по темам; история скрыта → ok=false,error="no_history" (НЕ ошибка RPC) |
|
||||
| `Join` | `JoinRequest{username}` | `JoinReply{ok}` | FloodWait → RESOURCE_EXHAUSTED (`flood`); ручной join вне квот |
|
||||
| `Leave` | `LeaveRequest{dialog_id}` | `LeaveReply{ok}` | нет членства → NOT_FOUND |
|
||||
|
||||
Общие сообщения:
|
||||
- `DialogEntry{id,name,username,kind,hue}` — каталог/поиск (id подписанный:
|
||||
каналы `-100…`, группы `-…`, личные `+…`; hue — палитра DIALOG_HUES).
|
||||
- `ChannelInfo` = DialogEntry + `participants?` + `is_forum`.
|
||||
- `PreviewMessage{id(text), text, time(ms)}` — превью (api-map §4.8 L351; `id`
|
||||
строкой: int-сообщения TG и фолбэк `m_<dialog>_<msg>`).
|
||||
- `EvalMessage{id(int64), text, date_ms, topic_id?, topic_title?}` — выборка
|
||||
оценки кандидата (`_discovery_message_item`).
|
||||
|
||||
### IngressService (сервис — клиент, ядро — сервер в Deal.Api, порт `GRPC_INGRESS_PORT` :5082)
|
||||
|
||||
| RPC | Запрос | Ответ | Примечания |
|
||||
|---|---|---|---|
|
||||
| `PushMessage` | `PushMessageRequest{dialog_id, channel_name, channel_handle, channel_hue, msg_id?, text, msg_at?}` | `PushMessageReply{accepted, duplicate}` | 1:1 QueuedMessage/demo-ingest: EnqueueAsync + превью в TgMessages; дубль dialog+msgId → duplicate=true, очередь не растёт; нет msg_at → ядро подставит now; hue считает сервис |
|
||||
| `SyncDialogs` | `SyncDialogsRequest{entries: DialogEntry[]}` | `SyncDialogsReply{monitored_ids[]}` | ядро: SyncFromTelegram (autoMonitorNew/обновление/удаление); ответ — актуальный зеркальный список monitored сервиса |
|
||||
| `ReportStatus` | `ReportStatusRequest{phase,connected,listener,account,error?,qr_url?}` | `ReportStatusReply{ok}` | ядро: KV tgStatus/tgAccount + SSE system_status/тосты на переходах фаз |
|
||||
|
||||
---
|
||||
|
||||
## `ml.proto`
|
||||
|
||||
`MlService` — пул инкрементальных наивно-байесовских моделей per-tenant
|
||||
(файл `data/ml/<tenantId>.sqlite`). Поля 1:1 с `MlPredictResultDto`/
|
||||
`MlServiceStatusDto` и model.py.
|
||||
|
||||
| RPC | Запрос | Ответ | Примечания |
|
||||
|---|---|---|---|
|
||||
| `Predict` | `PredictRequest{text}` | `PredictReply{take,label?,scores: map<string,double>,hits,ready,margin?,terms[],type?}` | «не уверен» при неготовой модели/пустом тексте — не ошибка; scores ≤5 лучших (round 3); margin адаптивный 0.9/0.7/0.5/0.35; terms ≤8; type — t:hire/t:order |
|
||||
| `Status` | `StatusRequest` (пуст) | `StatusReply{ready,classes: map<string,double>,learned,eval: ModelEval}` | classes «label → вес» round 2; модель создаётся лениво |
|
||||
| `Reset` | `ResetRequest` (пуст) | `ResetReply{ok, error?}` | очистка classes/terms/eval_log + пересоздание файла; ok=false — мягкая ошибка (ядро чистит ml_outbox только при ok) |
|
||||
| `TrainBatch` | `TrainBatchRequest{items: TrainExample[]}` | `TrainBatchReply{learned}` | 1 транзакция + пакетные вставки терминов (= learn_batch); learned = применено примеров |
|
||||
|
||||
Сообщения:
|
||||
- `TypeDecision{take,label("hire"|"order"),value("t:hire"|"t:order"),margin}` —
|
||||
решение о типе заявки.
|
||||
- `ModelEval{count,correct,accuracy}` — окно самооценки (EVAL_WINDOW последних
|
||||
подтверждённых решений).
|
||||
- `TrainExample{text,label,delta}` — строка обучения (ml_outbox): delta 1.0
|
||||
пользователь / −1.0 снять / 0.4–0.6 ИИ-правила.
|
||||
|
||||
Пороги и константы (Ruling 4): MIN_TOTAL 20, MIN_WINNER 6, MIN_WINNER_SPAM 4,
|
||||
MIN_HITS 2, MARGIN 0.9, типы `t:*` с MIN_TYPE_WINNER 4 — живут в ml-service
|
||||
(реализация Task 5/6), в контракт не входят.
|
||||
|
||||
---
|
||||
|
||||
## `ai.proto`
|
||||
|
||||
`AiService` — фасад LLM-провайдеров без БД (Ruling 5). Ядро передаёт в теле
|
||||
каждого запроса заполненные промпты/контекст + конфиг активного провайдера
|
||||
(`provider_config`); сервис возвращает ответ модели + оценку токенов.
|
||||
|
||||
| RPC | Запрос | Ответ | Примечания |
|
||||
|---|---|---|---|
|
||||
| `Filter` | `FilterRequest{prompt, text, provider_config}` | `FilterReply{pass, reason?, usage}` | prompt = заполненный aiFilterPrompt (ядро); «фильтр не применялся» обрабатывает ядро до вызова |
|
||||
| `Classify` | `ClassifyRequest{system_prompt, user_context, provider_config}` | `ClassifyReply{ok, json?, usage}` | system_prompt = aiPrompt+cardPrompt, user_context = «Доски + примеры + Сообщение» (собирает ядро); json — сырой ответ модели строкой; строгий маппинг в карточку — ядро |
|
||||
| `GenerateKeywords` | `GenerateKeywordsRequest{description, provider_config}` | `GenerateKeywordsReply{keywords[], usage}` | фикс. промпт (routes L36–47); очистку `_clean_keywords` и мягкие ошибки делает ядро (Ruling 11) |
|
||||
| `EvaluateFit` | `EvaluateFitRequest{text, description, keywords[], provider_config}` | `EvaluateFitReply{fit, reason?, usage}` | промпт discovery_eval L50–54; ядро зовёт при aiEnabled, сбой → эвристика |
|
||||
|
||||
`provider_config` — конфиг активного провайдера на запрос (Ruling 5 «в теле
|
||||
каждого запроса»): ядро собирает эффективный конфиг (настройка `aiConfigs`
|
||||
тенанта хранит `{apiKey, baseUrl, model}` в camelCase, ключ шифруется AES-GCM;
|
||||
`apiStyle` — из каталога `AiProviders`) и передаёт в теле; сервис настроек не
|
||||
хранит. Форма — сообщение `ProviderConfig`:
|
||||
|
||||
| Поле | Обязательность | Описание |
|
||||
|---|---|---|
|
||||
| `provider_id` | да | Id провайдера (ключ `aiConfigs`/каталога: deepseek/openai/anthropic/ollama/lmstudio/custom…) |
|
||||
| `base_url` | да | Эффективный базовый URL API (`aiConfigs.baseUrl` или дефолт каталога) |
|
||||
| `api_key` | опц. | Ключ открытым текстом (расшифрован ядром); пуст у локальных провайдеров — заголовок не шлётся |
|
||||
| `model` | да | Активная модель (`aiConfigs.model` или первая из каталога) |
|
||||
| `api_style` | опц. | Стиль API: пуст — OpenAI-совместимый (`{base}/chat/completions`, Bearer); `"anthropic"` — Messages API (`{base}/v1/messages`, x-api-key + anthropic-version) |
|
||||
|
||||
Сообщения:
|
||||
- `Usage{prompt, completion, total}` — оценка токенов в каждом reply (из usage
|
||||
API-ответа; при отсутствии ≈chars/4; ядро копит в KV `aiTokenUsage`).
|
||||
|
||||
Ошибки: провайдер не ответил корректно после ретраев → `UNAVAILABLE` с detail
|
||||
«ИИ (имя) не ответил корректно — повторите попытку через несколько секунд».
|
||||
`ClassifyReply.ok=false` — ответ без разбираемого JSON (не RPC-ошибка); ядро
|
||||
трактует как «не разобрано» и использует локальный путь.
|
||||
@@ -0,0 +1,172 @@
|
||||
// ai.proto — контракт между ядром Deal и ai-service (этап 6).
|
||||
//
|
||||
// ai-service — фасад LLM-провайдеров без БД (Ruling 5, дизайн-док §7.3):
|
||||
// ядро передаёт в теле каждого запроса готовые (заполненные) промпты и/или
|
||||
// текст + конфиг активного провайдера (ProviderConfig); сервис вызывает
|
||||
// провайдера (OpenAI-совместимые POST {base}/chat/completions, Anthropic
|
||||
// POST {base}/v1/messages; temperature 0.2, таймауты 90/60 с, retry 2 с
|
||||
// паузами 0.8/2 с) и возвращает ответ + оценку токенов. Конфиг провайдера
|
||||
// (id/base/model/apiKey/api_style) ядро кладёт в поле provider_config каждого
|
||||
// запроса — сервис настроек тенанта не знает и не хранит.
|
||||
//
|
||||
// Каждый RPC обязан нести в gRPC-metadata два заголовка (Ruling 1):
|
||||
// tenant-id — id тенанта (строка; учёт токенов в ядре по нему);
|
||||
// service-token — общий токен сервисов (env DEAL_SERVICE_TOKEN); неверный/
|
||||
// пустой → UNAUTHENTICATED.
|
||||
//
|
||||
// Ошибки домена — gRPC-статусы (Ruling 1/5):
|
||||
// INVALID_ARGUMENT — невалидный запрос (пустой текст/промпт и т.п.);
|
||||
// UNAVAILABLE — провайдер не ответил корректно после ретраев; detail =
|
||||
// «ИИ (имя) не ответил корректно — повторите попытку через
|
||||
// несколько секунд» (ядро падает в локальный разбор).
|
||||
//
|
||||
// Учёт токенов (Ruling 5): каждый reply несёт usage{prompt/completion/total}.
|
||||
// Берётся из usage API-ответа провайдера; при отсутствии оценивается по
|
||||
// символам (≈chars/4). Ядро копит значения в tenant-KV aiTokenUsage.
|
||||
//
|
||||
// Deadlines (клиент ядра): все RPC — 120 с (90 с провайдер + ретраи 0.8/2 с;
|
||||
// при недоступности ядро не ждёт повторно — Ruling 6 кэш/фолбэк).
|
||||
syntax = "proto3";
|
||||
|
||||
package deal.ai.v1;
|
||||
|
||||
option csharp_namespace = "Deal.Grpc.Ai";
|
||||
|
||||
service AiService {
|
||||
// ИИ-фильтр входящих сообщений (ai.py filter_incoming L188–198, Ruling 5):
|
||||
// ядро шлёт заполненный aiFilterPrompt (fill_prompt делает ядро) и текст;
|
||||
// решение {pass, reason}. Ветку «фильтр не применялся» (aiFilterEnabled/
|
||||
// недоступность) обрабатывает ядро до вызова — сервис всегда отвечает.
|
||||
rpc Filter(FilterRequest) returns (FilterReply);
|
||||
|
||||
// Полный разбор лида (ai.py classify L218–258, Ruling 5): ядро собирает
|
||||
// system_prompt = заполненные aiPrompt + cardPrompt и user-контекст
|
||||
// «Доски + примеры разметки + Сообщение»; сервис возвращает извлечённый
|
||||
// ответ модели как json-строку (типовую схему задаёт промпт). Строгий
|
||||
// маппинг json → AiParsedCardDto делает ядро (1:1 normalize_stack/
|
||||
// clean_budget/build_contacts).
|
||||
rpc Classify(ClassifyRequest) returns (ClassifyReply);
|
||||
|
||||
// Генерация ключевых слов для discovery-задачи по описанию (фикс. промпт
|
||||
// discovery_routes L36–47 + описание; Ruling 5): ответ {keywords}. Очистку
|
||||
// (_clean_keywords: ≤30, ≤60 симв., дедуп) и мягкие ошибки делает ядро.
|
||||
rpc GenerateKeywords(GenerateKeywordsRequest) returns (GenerateKeywordsReply);
|
||||
|
||||
// Оценка соответствия сообщения задаче поиска (промпт discovery_eval
|
||||
// L50–54; Ruling 5/10): текст + описание + ключи задачи → {fit, reason}.
|
||||
// Ядро зовёт только при aiEnabled; сбой/не-JSON — фолбэк на эвристику.
|
||||
rpc EvaluateFit(EvaluateFitRequest) returns (EvaluateFitReply);
|
||||
}
|
||||
|
||||
// --- Запросы/ответы AiService ---
|
||||
|
||||
// Конфиг активного LLM-провайдера на запрос (Ruling 5: ядро расшифровывает
|
||||
// aiConfigs и передаёт в теле каждого запроса; сервис не хранит настроек).
|
||||
// Форма 1:1 с эффективным конфигом core: настройка aiConfigs тенанта хранит
|
||||
// {apiKey, baseUrl, model} (camelCase; apiKey шифруется AES-GCM этапа 2),
|
||||
// api_style — из каталога AiProviders (Settings); HTTP-клиенту провайдера
|
||||
// нужны baseUrl+model+apiKey для запроса и api_style для выбора схемы вызова
|
||||
// (OpenAI-совместимые chat/completions vs Anthropic Messages API).
|
||||
message ProviderConfig {
|
||||
// Id провайдера (ключ aiConfigs / каталога AiProviders: deepseek, openai,
|
||||
// anthropic, ollama, lmstudio, custom…).
|
||||
string provider_id = 1;
|
||||
// Эффективный базовый URL API (aiConfigs.baseUrl или дефолт каталога).
|
||||
string base_url = 2;
|
||||
// API-ключ открытым текстом (расшифрован ядром); пуст для локальных
|
||||
// провайдеров (ollama/lmstudio) — заголовок авторизации не шлётся.
|
||||
optional string api_key = 3;
|
||||
// Активная модель (aiConfigs.model или первая из каталога провайдера).
|
||||
string model = 4;
|
||||
// Стиль API: пуст — OpenAI-совместимый (POST {base}/chat/completions,
|
||||
// Bearer); "anthropic" — Messages API (POST {base}/v1/messages,
|
||||
// x-api-key + anthropic-version).
|
||||
optional string api_style = 5;
|
||||
}
|
||||
|
||||
message FilterRequest {
|
||||
// Заполненный промпт фильтра (настройка aiFilterPrompt с подстановкой
|
||||
// {domain}/{keywords} — делает ядро; Ruling 5).
|
||||
string prompt = 1;
|
||||
// Текст сообщения (ядро обрезает до 4000, как ai.py L193).
|
||||
string text = 2;
|
||||
// Конфиг активного провайдера (Ruling 5; все запросы ai-сервиса).
|
||||
ProviderConfig provider_config = 3;
|
||||
}
|
||||
|
||||
message FilterReply {
|
||||
// True — сообщение проходит фильтр (не спам/реклама/служебное).
|
||||
bool pass = 1;
|
||||
// Причина отказа при pass=false (текст ветки filter_ai; пуст при пропуске).
|
||||
optional string reason = 2;
|
||||
// Оценка токенов вызова (Ruling 5).
|
||||
Usage usage = 3;
|
||||
}
|
||||
|
||||
message ClassifyRequest {
|
||||
// system_prompt = заполненные aiPrompt + cardPrompt (структура карточки,
|
||||
// «О заявке»; собирает ядро — Ruling 5).
|
||||
string system_prompt = 1;
|
||||
// user-контекст: «Доски + примеры разметки + Новое сообщение» (собирает
|
||||
// ядро, 1:1 classify L243–251).
|
||||
string user_context = 2;
|
||||
// Конфиг активного провайдера (Ruling 5; все запросы ai-сервиса).
|
||||
ProviderConfig provider_config = 3;
|
||||
}
|
||||
|
||||
message ClassifyReply {
|
||||
// True — модель вернула разбираемый JSON (ok=false — ответ без JSON после
|
||||
// ретраев; ядро трактует как «не разобрано» и падает в локальный путь).
|
||||
bool ok = 1;
|
||||
// Сырой JSON-ответ модели (строкой; маппинг в карточку — в ядре).
|
||||
optional string json = 2;
|
||||
// Оценка токенов вызова (Ruling 5).
|
||||
Usage usage = 3;
|
||||
}
|
||||
|
||||
message GenerateKeywordsRequest {
|
||||
// Описание ниши/задачи (ядро обрезает до 4000, discovery_routes L29).
|
||||
string description = 1;
|
||||
// Конфиг активного провайдера (Ruling 5; все запросы ai-сервиса).
|
||||
ProviderConfig provider_config = 2;
|
||||
}
|
||||
|
||||
message GenerateKeywordsReply {
|
||||
// Сгенерированные ключи (пустой список — модель не выделила ключи;
|
||||
// чистку/дедуп и мягкую ошибку для UI делает ядро — Ruling 11).
|
||||
repeated string keywords = 1;
|
||||
// Оценка токенов вызова (Ruling 5).
|
||||
Usage usage = 2;
|
||||
}
|
||||
|
||||
message EvaluateFitRequest {
|
||||
// Текст сообщения для оценки (выборка кандидата; ядро ограничивает 4000).
|
||||
string text = 1;
|
||||
// Описание задачи поиска (discovery_eval L51).
|
||||
string description = 2;
|
||||
// Ключи задачи (discovery_eval L52; подставляются в промпт сервисом).
|
||||
repeated string keywords = 3;
|
||||
// Конфиг активного провайдера (Ruling 5; все запросы ai-сервиса).
|
||||
ProviderConfig provider_config = 4;
|
||||
}
|
||||
|
||||
message EvaluateFitReply {
|
||||
// True — сообщение относится к сфере/задаче (JSON {"fit": 0|1}).
|
||||
bool fit = 1;
|
||||
// Краткая причина решения модели (пуст, если модель её не дала).
|
||||
optional string reason = 2;
|
||||
// Оценка токенов вызова (Ruling 5).
|
||||
Usage usage = 3;
|
||||
}
|
||||
|
||||
// Оценка токенов вызова провайдера (Ruling 5: usage{prompt/completion/total};
|
||||
// из usage API-ответа, при отсутствии — по символам ≈chars/4).
|
||||
message Usage {
|
||||
// Токены запроса (system + user).
|
||||
uint32 prompt = 1;
|
||||
// Токены ответа модели.
|
||||
uint32 completion = 2;
|
||||
// Суммарно (prompt + completion; может отличаться от суммы при подсчёте
|
||||
// провайдером — берём как есть).
|
||||
uint32 total = 3;
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// ml.proto — контракт между ядром Deal и ml-service (этап 6).
|
||||
//
|
||||
// Инкрементальная наивно-байесовская модель по терминам, 1:1 с python
|
||||
// mlservice/model.py (predict L184–293, status L325–345, reset L348–354,
|
||||
// learn_batch L147–173) и DTO ядра Deal.Contracts.Integrations.Models
|
||||
// (MlPredictResultDto/MlServiceStatusDto/MlEvalDto/MlResetResultDto).
|
||||
// Модель per-tenant: пул в ml-service, файл SQLite data/ml/<tenantId>.sqlite
|
||||
// (Ruling 4). Обучение ядро шлёт батчами из очереди ml_outbox
|
||||
// (MlOutboxFlushScheduler, Ruling 6).
|
||||
//
|
||||
// Каждый RPC обязан нести в gRPC-metadata два заголовка (Ruling 1):
|
||||
// tenant-id — id тенанта (строка; модель тенанта — в пуле сервиса);
|
||||
// service-token — общий токен сервисов (env DEAL_SERVICE_TOKEN); неверный/
|
||||
// пустой → UNAUTHENTICATED.
|
||||
//
|
||||
// Ошибки домена — gRPC-статусы с detail = текст причины 1:1 (Ruling 1):
|
||||
// INVALID_ARGUMENT — невалидный запрос (пустой text и т.п.);
|
||||
// UNAVAILABLE — хранилище модели недоступно (ядро отвечает «не уверен»,
|
||||
// Ruling 6 — кэш reachable 15 с).
|
||||
//
|
||||
// Семантика неготовой модели: Predict НЕ ошибка — модель без опыта отвечает
|
||||
// фиксированным «не уверен»: take=false, label пуст, scores пуст, hits=0,
|
||||
// ready=false, margin пуст, terms пуст, type пуст (Ruling 5 этапа 2, 1:1).
|
||||
//
|
||||
// Deadlines (клиент ядра): Predict — 5 с; Status/Reset — 10 с; TrainBatch — 30 с
|
||||
// (батч ≤100 примеров, одна транзакция).
|
||||
syntax = "proto3";
|
||||
|
||||
package deal.ml.v1;
|
||||
|
||||
option csharp_namespace = "Deal.Grpc.Ml";
|
||||
|
||||
service MlService {
|
||||
// Предсказание по тексту сообщения (model.py predict L184–293).
|
||||
// take/label/scores/hits/margin/terms/type осмысленны только при take=true;
|
||||
// scores — до 5 лучших «класс → вес» (round 3), margin — адаптивный порог
|
||||
// (0.9/0.7/0.5/0.35 после 0/60/150/400 примеров), terms — узнанные термины
|
||||
// класса-победителя, type — решение о типе заявки (t:hire/t:order).
|
||||
rpc Predict(PredictRequest) returns (PredictReply);
|
||||
|
||||
// Статус модели тенанта (model.py status L325–345): ready/classes/learned/eval.
|
||||
// classes — «label → вес» (round 2, по убыванию); eval — самооценка по окну
|
||||
// последних подтверждённых решений (EVAL_WINDOW). Модель создаётся лениво
|
||||
// по первому обращению (Ruling 4) — отсутствие опыта это НЕ ошибка.
|
||||
rpc Status(StatusRequest) returns (StatusReply);
|
||||
|
||||
// Полный сброс модели тенанта (model.py reset L348–354): очистка классов,
|
||||
// терминов и журнала самооценки + пересоздание файла. Ok=true; мягкая
|
||||
// ошибка — Ok=false + error (ядро чистит свою ml_outbox только при успехе).
|
||||
rpc Reset(ResetRequest) returns (ResetReply);
|
||||
|
||||
// Пакетное обучение (model.py learn_batch L147–173): одна транзакция +
|
||||
// пакетные вставки терминов; самооценка по действиям пользователя (delta=1,
|
||||
// не t:*) до применения. Ответ — число применённых примеров.
|
||||
rpc TrainBatch(TrainBatchRequest) returns (TrainBatchReply);
|
||||
}
|
||||
|
||||
message PredictRequest {
|
||||
// Текст сообщения (ядро передаёт уже обрезанный/нормализованный, как
|
||||
// ml_routes.py L86–90; пустой/пробельный — не ошибка: ответ «не уверен»).
|
||||
string text = 1;
|
||||
}
|
||||
|
||||
message PredictReply {
|
||||
// True — модель уверена (take) и решение можно использовать без ИИ.
|
||||
bool take = 1;
|
||||
// Класс решения: id колонки канбана (b_…) или "spam"; пуст, если не уверена.
|
||||
optional string label = 2;
|
||||
// Веса классов: «label → вес» (до 5 лучших; пуст у неготовой модели).
|
||||
map<string, double> scores = 3;
|
||||
// Сколько терминов класса-победителя модель узнала в тексте.
|
||||
int32 hits = 4;
|
||||
// Модель обучена (набрала MIN_TOTAL/MIN_WINNER/MIN_WINNER_SPAM) и может
|
||||
// принимать решения.
|
||||
bool ready = 5;
|
||||
// Порог уверенности решения (адаптивный margin, 2 знака); пуст — нет решения.
|
||||
optional double margin = 6;
|
||||
// Узнанные термины класса-победителя (подсказка структуры карточки, ≤8).
|
||||
repeated string terms = 7;
|
||||
// Решение о типе заявки (hire/order); пуст — модель тип не определила.
|
||||
TypeDecision type = 8;
|
||||
}
|
||||
|
||||
// Решение ML о типе заявки (predict L233–238; MlTypeDecisionDto).
|
||||
message TypeDecision {
|
||||
// True — модель уверена в типе.
|
||||
bool take = 1;
|
||||
// Тип: "hire" | "order".
|
||||
string label = 2;
|
||||
// Внутренний класс ML: "t:hire" | "t:order" (не показывается UI).
|
||||
string value = 3;
|
||||
// Запас уверенности (margin, 2 знака).
|
||||
double margin = 4;
|
||||
}
|
||||
|
||||
message StatusRequest {}
|
||||
|
||||
message StatusReply {
|
||||
// Модель готова принимать решения.
|
||||
bool ready = 1;
|
||||
// Классы модели: «label → вес» (round 2; пуст, пока нет обучения).
|
||||
map<string, double> classes = 2;
|
||||
// Всего примеров, на которых модель обучалась (сумма по классам).
|
||||
int32 learned = 3;
|
||||
// Самооценка модели по последним подтверждённым решениям.
|
||||
ModelEval eval = 4;
|
||||
}
|
||||
|
||||
// Окно самооценки модели (model.py status L329–339; MlEvalDto).
|
||||
message ModelEval {
|
||||
// Решений в окне самооценки (последние EVAL_WINDOW).
|
||||
int32 count = 1;
|
||||
// Из них совпавших с действием пользователя.
|
||||
int32 correct = 2;
|
||||
// Доля верных (correct/count, 0..1; 0 при пустом окне).
|
||||
double accuracy = 3;
|
||||
}
|
||||
|
||||
message ResetRequest {}
|
||||
|
||||
message ResetReply {
|
||||
// True — модель сброшена (и ядро очищает свою очередь обучения).
|
||||
bool ok = 1;
|
||||
// Текст ошибки при сбое сброса (пуст при успехе) — мягкая ошибка.
|
||||
optional string error = 2;
|
||||
}
|
||||
|
||||
message TrainBatchRequest {
|
||||
// Примеры обучения (1 транзакция на батч; ядро шлёт ≤100 за цикл, Ruling 6).
|
||||
repeated TrainExample items = 1;
|
||||
}
|
||||
|
||||
// Один обучающий пример (строка ml_outbox ядра: text/label/delta).
|
||||
message TrainExample {
|
||||
// Текст примера (source_msg карточки или title).
|
||||
string text = 1;
|
||||
// Метка: id доски (b_…), "spam" либо тип "t:hire"/"t:order".
|
||||
string label = 2;
|
||||
// Вес сигнала: 1.0 — действие пользователя; −1.0 — снять метку;
|
||||
// 0.4/0.6 — сигналы ИИ/правил (этапы 4/6).
|
||||
double delta = 3;
|
||||
}
|
||||
|
||||
message TrainBatchReply {
|
||||
// Число применённых примеров (= len(items) при успехе).
|
||||
int32 learned = 1;
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
// telegram.proto — контракт между ядром Deal и telegram-service (этап 6).
|
||||
//
|
||||
// Два сервиса в одном файле (дизайн-док §6.2, план Task 1, Ruling 1/7):
|
||||
// * TelegramService — команды ядра к telegram-service (порт-гейт ITelegramGateway):
|
||||
// подключение/отключение аккаунта, каталог диалогов, мониторинг, backfill,
|
||||
// превью, discovery-операции (поиск/инфо/чтение/вступление/выход);
|
||||
// * IngressService — исходящий поток telegram-service → ядро: сырые сообщения
|
||||
// (PushMessage), синхронизация каталога (SyncDialogs), статус аккаунта
|
||||
// (ReportStatus). Сервер ингресса живёт в Deal.Api (:5082, Ruling 7).
|
||||
//
|
||||
// Семантика методов 1:1 с python-прототипом backend/app/services/telegram.py
|
||||
// (имена L134–873) и api-map §3.3/§4.8/§4.9; хранение диалогов/статуса — только
|
||||
// в ядре (модуль Deal.Modules.Telegram, Ruling 7), сервис БД тенантов не знает.
|
||||
//
|
||||
// Каждый RPC обязан нести в gRPC-metadata два заголовка (Ruling 1):
|
||||
// tenant-id — id тенанта (строка; единственный источник принадлежности,
|
||||
// полю в теле не доверяем);
|
||||
// service-token — общий токен сервисов (env DEAL_SERVICE_TOKEN); неверный/
|
||||
// пустой → UNAUTHENTICATED.
|
||||
//
|
||||
// Ошибки домена — gRPC-статусы с detail = текст причины 1:1 с прототипом:
|
||||
// INVALID_ARGUMENT — неверный ввод/неверный код/неверный пароль и т.п.;
|
||||
// NOT_FOUND — диалог/сущность не найдены (нет сессии тенанта и т.п.);
|
||||
// FAILED_PRECONDITION— операция невозможна в текущей фазе (нет сессии и т.п.);
|
||||
// RESOURCE_EXHAUSTED — FloodWait Telegram (detail начинается с префикса "flood");
|
||||
// UNAVAILABLE — недоступность Telegram/сети (безопасный повтор).
|
||||
//
|
||||
// Значения строк (канон контракта, .NET-код обеих сторон — новый):
|
||||
// * phase: idle|phone|code|password|qr|ready (как status() прототипа L85);
|
||||
// * kind: channel (канал) | group (группа/супергруппа) | forum (форум) |
|
||||
// chat (личный чат/бот). 1:1 с _kind_of (L461–466): broadcast →
|
||||
// channel, megagroup/gigagroup/group → group, остальное → chat.
|
||||
// Forum выставляется отдельным флагом is_forum (GetInfo); в
|
||||
// каталоге (RefreshDialogs) форум приходит как group.
|
||||
//
|
||||
// Deadlines (клиент ядра; уточняются адаптерами T2+):
|
||||
// * быстрые команды статуса/мониторинга — 10 с;
|
||||
// * сетевые операции Telegram (QR/код/поиск/инфо/чтение/вступление) — 60 с;
|
||||
// * Backfill/RefreshDialogs (паузы анти-бана 1.5–3 с/сообщение) — 120 с;
|
||||
// * IngressService (локальная сеть core) — 10 с (сбой догоняет sweep).
|
||||
syntax = "proto3";
|
||||
|
||||
package deal.telegram.v1;
|
||||
|
||||
option csharp_namespace = "Deal.Grpc.Telegram";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TelegramService — команды ядра → telegram-service (клиентская сторона в core)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
service TelegramService {
|
||||
// Текущий статус аккаунта/фазы входа тенанта (status() прототипа L103–119).
|
||||
// live-поля для GET /api/tg/status (Ruling 8); monitored/keysSet ядро считает
|
||||
// само. Нет сессии — FAILED_PRECONDITION «Telegram не подключён».
|
||||
rpc GetStatus(GetStatusRequest) returns (GetStatusReply);
|
||||
|
||||
// Вход по номеру телефона: запросить код (start_phone L134–147).
|
||||
// api_id/api_hash — глобальные ключи приложения Telegram, задаёт оператор (ТЗ §4.1/§8.1),
|
||||
// передаёт ядро в теле (Ruling 3); нет ключей — ядро отвечает 400 «Ключи Telegram
|
||||
// не заданы оператором» до вызова. Ответ: новая фаза ("code").
|
||||
rpc StartPhone(StartPhoneRequest) returns (StartPhoneReply);
|
||||
|
||||
// Начать QR-вход (qr_start L286–300). Ответ: фаза + qrUrl (t.me/qr/...);
|
||||
// если аккаунт уже авторизован — фаза "ready", qrUrl пуст.
|
||||
rpc StartQr(StartQrRequest) returns (StartQrReply);
|
||||
|
||||
// Отправить SMS-код (submit_code L149–166). Ошибки: «Неверный код»,
|
||||
// «Код истёк — запросите новый» → INVALID_ARGUMENT. Нужен 2FA — фаза
|
||||
// "password", иначе "ready". Фазы вне "code" → FAILED_PRECONDITION.
|
||||
rpc SendCode(SendCodeRequest) returns (SendCodeReply);
|
||||
|
||||
// Облачный пароль 2FA (submit_password L168–176). Ошибка «Неверный облачный
|
||||
// пароль» → INVALID_ARGUMENT. Ответ: фаза ("ready").
|
||||
rpc SendPassword(SendPasswordRequest) returns (SendPasswordReply);
|
||||
|
||||
// Отключить аккаунт, удалить сессию тенанта (disconnect L189–207).
|
||||
rpc Logout(LogoutRequest) returns (LogoutReply);
|
||||
|
||||
// Синхронизировать каталог диалогов из Telegram (refresh_dialogs L505–519):
|
||||
// актуальный список sources диалогов аккаунта (entries). Удаление/обновление
|
||||
// каталога и авто-мониторинг новых делает ядро (SyncFromTelegram, Ruling 7).
|
||||
rpc RefreshDialogs(RefreshDialogsRequest) returns (RefreshDialogsReply);
|
||||
|
||||
// Включить/выключить мониторинг диалога (set_monitor L536–546): обновляет
|
||||
// зеркало monitored в сервисе. Первый backfill (force=false) запускает ядро
|
||||
// отдельным RPC Backfill. Ответ: ok/enabled.
|
||||
rpc SetMonitor(SetMonitorRequest) returns (SetMonitorReply);
|
||||
|
||||
// Мониторинг всех диалогов сразу (set_monitor_all L548–567). Ответ:
|
||||
// ok/count/enabled (count — сколько диалогов в каталоге тенанта).
|
||||
rpc SetMonitorAll(SetMonitorAllRequest) returns (SetMonitorAllReply);
|
||||
|
||||
// Перечитать последние ~10 сообщений диалога и отправить их в ядро потоком
|
||||
// PushMessage (backfill_dialog L349–390; паузы анти-бана 1.5–3 с/сообщение,
|
||||
// mark-as-read). force=true — «Перечитать» по кнопке даже для разобранных.
|
||||
// Ответ: сколько сообщений отправлено (processed).
|
||||
rpc Backfill(BackfillRequest) returns (BackfillReply);
|
||||
|
||||
// Последние сообщения диалога для превью (dialog_messages L583–620):
|
||||
// свежие из Telegram; признак lead и фолбэк на БД добавляет ядро
|
||||
// (у сервиса нет БД тенанта). limit 1..50 (api-map /dialogs/preview).
|
||||
rpc ReadRecent(ReadRecentRequest) returns (ReadRecentReply);
|
||||
|
||||
// Глобальный поиск каналов/групп по ключу (discovery_search L624–664).
|
||||
// Пауза анти-бана после поиска — внутри сервиса. Личные чаты/боты ядро
|
||||
// отсеивает само (Ruling 10). Результат — entries канала/группы.
|
||||
rpc Search(SearchRequest) returns (SearchReply);
|
||||
|
||||
// Инфо об источнике для оценки (discovery_info L666–716): имя/username/kind/
|
||||
// hue + participants и is_forum (полный чат). Сбои определения не роняют
|
||||
// RPC: participants пуст, остальные поля — из entity/каталога.
|
||||
rpc GetInfo(GetInfoRequest) returns (GetInfoReply);
|
||||
|
||||
// Выборка последних сообщений источника для оценки кандидата
|
||||
// (discovery_read L718–760): форумы читаются по активным темам. История
|
||||
// недоступна (приватный/закрытый источник) — ok=false, error="no_history",
|
||||
// это НЕ ошибка RPC. limit ≥ 1; topic_id/topic_title заполнены для форумов.
|
||||
rpc ReadForEval(ReadForEvalRequest) returns (ReadForEvalReply);
|
||||
|
||||
// Вступить в канал/группу по @username (discovery_join L818–839; ручной
|
||||
// join вне квот — паузу перед авто-join делает воркер ядра, Ruling 10).
|
||||
// FloodWait → RESOURCE_EXHAUSTED (detail с префиксом "flood").
|
||||
rpc Join(JoinRequest) returns (JoinReply);
|
||||
|
||||
// Выйти из канала/группы (discovery_leave L841–848). NOT_FOUND — нет
|
||||
// диалога/членства.
|
||||
rpc Leave(LeaveRequest) returns (LeaveReply);
|
||||
}
|
||||
|
||||
// --- Запросы/ответы TelegramService ---
|
||||
|
||||
message GetStatusRequest {}
|
||||
|
||||
// Статус аккаунта/фазы входа (shape прототипа status() L110–118; monitored и
|
||||
// keysSet ядро добавляет само из своей БД/настроек — Ruling 8).
|
||||
message GetStatusReply {
|
||||
// Фаза входа: idle|phone|code|password|qr|ready.
|
||||
string phase = 1;
|
||||
// Клиент Telegram подключён и авторизован.
|
||||
bool connected = 2;
|
||||
// Жив ли realtime-listener (поток новых сообщений → PushMessage).
|
||||
bool listener = 3;
|
||||
// Аккаунт "@username" (для справки; источник истины — KV tgAccount по
|
||||
// ReportStatus, ядро использует KV — Ruling 8).
|
||||
string account = 4;
|
||||
// Текст последней ошибки (null, если ошибки нет).
|
||||
optional string error = 5;
|
||||
// URL QR-входа (заполнен только при phase == "qr").
|
||||
optional string qr_url = 6;
|
||||
}
|
||||
|
||||
// Подключение по телефону: ключи API передаёт ядро (Ruling 3).
|
||||
message StartPhoneRequest {
|
||||
// Номер телефона в международном формате (как ввёл пользователь).
|
||||
string phone = 1;
|
||||
// api_id приложения Telegram (глобальные ключи, задаёт оператор; 5..9 цифр).
|
||||
int32 api_id = 2;
|
||||
// api_hash приложения Telegram (глобальные ключи, задаёт оператор).
|
||||
string api_hash = 3;
|
||||
}
|
||||
|
||||
message StartPhoneReply {
|
||||
// Фаза после запроса кода ("code"); при ошибке — RPC-статус.
|
||||
string phase = 1;
|
||||
}
|
||||
|
||||
message StartQrRequest {
|
||||
// api_id/api_hash приложения Telegram (см. StartPhoneRequest).
|
||||
int32 api_id = 1;
|
||||
string api_hash = 2;
|
||||
}
|
||||
|
||||
message StartQrReply {
|
||||
// Фаза после запуска: "qr" (ждём сканирования) либо "ready" (уже вошли).
|
||||
string phase = 1;
|
||||
// URL вида https://t.me/qr/... для отрисовки QR; пуст при phase != "qr".
|
||||
string qr_url = 2;
|
||||
}
|
||||
|
||||
message SendCodeRequest {
|
||||
// Код из SMS/Telegram-сообщения.
|
||||
string code = 1;
|
||||
}
|
||||
|
||||
message SendCodeReply {
|
||||
// Фаза после проверки кода: "password" (нужен 2FA) или "ready".
|
||||
string phase = 1;
|
||||
}
|
||||
|
||||
message SendPasswordRequest {
|
||||
// Облачный пароль 2FA.
|
||||
string password = 1;
|
||||
}
|
||||
|
||||
message SendPasswordReply {
|
||||
// Фаза после входа ("ready").
|
||||
string phase = 1;
|
||||
}
|
||||
|
||||
message LogoutRequest {}
|
||||
|
||||
message LogoutReply {
|
||||
// True — аккаунт отключён, сессия тенанта удалена.
|
||||
bool ok = 1;
|
||||
}
|
||||
|
||||
message RefreshDialogsRequest {}
|
||||
|
||||
message RefreshDialogsReply {
|
||||
// Актуальный каталог диалогов аккаунта (id/name/username/kind/hue).
|
||||
// Ядро применяет его через SyncFromTelegram (Ruling 7).
|
||||
repeated DialogEntry entries = 1;
|
||||
}
|
||||
|
||||
// Один диалог/канал каталога или результат поиска (shape refresh L516 и
|
||||
// discovery_search L653–660: tuple id/name/handle/kind/hue; handle == username).
|
||||
message DialogEntry {
|
||||
// Подписанный id диалога: каналы "-100…", группы "-…", личные "+…".
|
||||
string id = 1;
|
||||
// Отображаемое имя (title/first_name) или id, если имени нет.
|
||||
string name = 2;
|
||||
// Username (handle) источника; пуст, если нет публичного username.
|
||||
string username = 3;
|
||||
// Тип: channel|group|forum|chat (канон контракта, см. шапку файла).
|
||||
string kind = 4;
|
||||
// Цвет источника из палитры DIALOG_HUES (hex, "#rrggbb") — считает сервис.
|
||||
string hue = 5;
|
||||
}
|
||||
|
||||
message SetMonitorRequest {
|
||||
// Id диалога каталога.
|
||||
string dialog_id = 1;
|
||||
// True — мониторить (сообщения → PushMessage в ядро), false — выключить.
|
||||
bool enabled = 2;
|
||||
}
|
||||
|
||||
message SetMonitorReply {
|
||||
bool ok = 1;
|
||||
// Зеркальное значение enabled (для ответов эндпоинтов {ok, enabled}).
|
||||
bool enabled = 2;
|
||||
}
|
||||
|
||||
message SetMonitorAllRequest {
|
||||
// True — мониторить все диалоги каталога, false — снять мониторинг со всех.
|
||||
bool enabled = 1;
|
||||
}
|
||||
|
||||
message SetMonitorAllReply {
|
||||
bool ok = 1;
|
||||
// Сколько диалогов в каталоге тенанта (api-map /monitor-all → count).
|
||||
int32 count = 2;
|
||||
bool enabled = 3;
|
||||
}
|
||||
|
||||
message BackfillRequest {
|
||||
// Id диалога для перечитывания.
|
||||
string dialog_id = 1;
|
||||
// True — перечитать, даже если диалог уже разобран (кнопка «Перечитать»).
|
||||
bool force = 2;
|
||||
}
|
||||
|
||||
message BackfillReply {
|
||||
// Сколько сообщений отправлено в ядро потоком PushMessage.
|
||||
int32 processed = 1;
|
||||
}
|
||||
|
||||
message ReadRecentRequest {
|
||||
// Id диалога.
|
||||
string dialog_id = 1;
|
||||
// Сколько последних сообщений (1..50; api-map /dialogs/preview limit 1..50).
|
||||
int32 limit = 2;
|
||||
}
|
||||
|
||||
message ReadRecentReply {
|
||||
// Последние сообщения (от новых к старым). lead/фолбэк на БД — в ядре.
|
||||
repeated PreviewMessage messages = 1;
|
||||
}
|
||||
|
||||
// Сообщение превью диалога (api-map §4.8 L351: {id, text, time, lead}).
|
||||
message PreviewMessage {
|
||||
// Id сообщения в Telegram (int); фолбэк-сообщения из БД ядра — строки
|
||||
// "m_<dialog>_<msg>", поэтому значение передаётся строкой.
|
||||
string id = 1;
|
||||
// Текст сообщения.
|
||||
string text = 2;
|
||||
// Время сообщения, epoch-ms.
|
||||
int64 time = 3;
|
||||
}
|
||||
|
||||
message SearchRequest {
|
||||
// Поисковый запрос (ключ задачи discovery).
|
||||
string query = 1;
|
||||
// Верхняя граница результатов (прототип: default 30).
|
||||
int32 limit = 2;
|
||||
}
|
||||
|
||||
message SearchReply {
|
||||
// Найденные источники (каналы/группы; личные чаты/ботов отсеивает ядро).
|
||||
repeated DialogEntry results = 1;
|
||||
}
|
||||
|
||||
message GetInfoRequest {
|
||||
// Id источника (подписанный; из каталога или результата поиска).
|
||||
string dialog_id = 1;
|
||||
}
|
||||
|
||||
// Инфо об источнике для оценки кандидата discovery (discovery_info L674–682).
|
||||
message ChannelInfo {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
string username = 3;
|
||||
// Тип: channel|group|forum|chat.
|
||||
string kind = 4;
|
||||
string hue = 5;
|
||||
// Число участников (full_chat); пусто — определить не удалось.
|
||||
optional int32 participants = 6;
|
||||
// True — мегагруппа-форум (темы); ядро трактует kind как "forum" (Ruling 10).
|
||||
bool is_forum = 7;
|
||||
}
|
||||
|
||||
message GetInfoReply {
|
||||
ChannelInfo info = 1;
|
||||
}
|
||||
|
||||
message ReadForEvalRequest {
|
||||
// Id источника.
|
||||
string dialog_id = 1;
|
||||
// Размер выборки (прототип discovery_read: limit сообщений/тем).
|
||||
int32 limit = 2;
|
||||
}
|
||||
|
||||
message ReadForEvalReply {
|
||||
// True — выборка получена; false — история недоступна без членства.
|
||||
bool ok = 1;
|
||||
// Код причины при ok=false: "no_history" (остальные поля пусты).
|
||||
optional string error = 2;
|
||||
// Сообщения выборки (форумы — по активным темам, topic_id/topic_title
|
||||
// заполнены; для обычных источников — null).
|
||||
repeated EvalMessage messages = 3;
|
||||
}
|
||||
|
||||
// Сообщение выборки discovery_read (_discovery_message_item L803–816).
|
||||
message EvalMessage {
|
||||
// Id сообщения в Telegram.
|
||||
int64 id = 1;
|
||||
// Текст сообщения (непустой; пустые тексты отбрасывает сервис).
|
||||
string text = 2;
|
||||
// Время сообщения, epoch-ms.
|
||||
int64 date_ms = 3;
|
||||
// Id темы форума (для обычных источников пусто).
|
||||
optional int64 topic_id = 4;
|
||||
// Название темы форума (для обычных источников пусто).
|
||||
optional string topic_title = 5;
|
||||
}
|
||||
|
||||
message JoinRequest {
|
||||
// @username источника (без "@"; пусто → INVALID_ARGUMENT).
|
||||
string username = 1;
|
||||
}
|
||||
|
||||
message JoinReply {
|
||||
bool ok = 1;
|
||||
}
|
||||
|
||||
message LeaveRequest {
|
||||
// Id диалога для выхода.
|
||||
string dialog_id = 1;
|
||||
}
|
||||
|
||||
message LeaveReply {
|
||||
bool ok = 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IngressService — исходящий поток telegram-service → ядро
|
||||
// (gRPC-сервер в Deal.Api :5082; Ruling 7; интерцептор service-token;
|
||||
// tenantId из metadata → собственный scope с ITenantContext.SetTenant)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
service IngressService {
|
||||
// Новое/догоняющее сообщение мониторящегося диалога → очередь пайплайна
|
||||
// ядра (PipelineIngestService.EnqueueAsync, контракт demo-ingest; + превью в
|
||||
// TgMessages). Дубль dialog+msgId уже в очереди — не растёт (duplicate=true).
|
||||
rpc PushMessage(PushMessageRequest) returns (PushMessageReply);
|
||||
|
||||
// Синхронизация каталога диалогов: ядро применяет entries (SyncFromTelegram:
|
||||
// авто-мониторинг по autoMonitorNew, обновление, удаление отсутствующих) и
|
||||
// отвечает актуальным списком monitored id — сервис держит зеркало
|
||||
// мониторинга в памяти (Ruling 7), по нему фильтрует события realtime.
|
||||
rpc SyncDialogs(SyncDialogsRequest) returns (SyncDialogsReply);
|
||||
|
||||
// Периодический/событийный статус аккаунта: ядро пишет KV tgStatus/tgAccount
|
||||
// и публикует SSE system_status + тосты на переходах фаз (Ruling 7).
|
||||
rpc ReportStatus(ReportStatusRequest) returns (ReportStatusReply);
|
||||
}
|
||||
|
||||
// Сообщение из потока в ядро. Поля 1:1 с QueuedMessage/PipelineIngestRequest
|
||||
// (Ruling 7, demo-ingest L7–59): dialog_id + канальные поля плоские; msg_id —
|
||||
// дубль-гвард; msg_at — время исходного сообщения, без него ядро подставит now.
|
||||
message PushMessageRequest {
|
||||
// Id диалога-источника (подписанный; пуст — приём no-op).
|
||||
string dialog_id = 1;
|
||||
// Имя канала/диалога (title/first_name или id).
|
||||
string channel_name = 2;
|
||||
// Username канала/диалога (пуст, если нет).
|
||||
string channel_handle = 3;
|
||||
// Цвет канала из палитры DIALOG_HUES (hex; считает сервис — Ruling 7).
|
||||
string channel_hue = 4;
|
||||
// Id исходного сообщения в Telegram (дубль-гвард dialog+msgId).
|
||||
optional int64 msg_id = 5;
|
||||
// Текст сообщения (сервис шлёт как есть; приём обрежет до 6000).
|
||||
string text = 6;
|
||||
// Время исходного сообщения, epoch-ms; пусто — ядро подставит now.
|
||||
optional int64 msg_at = 7;
|
||||
}
|
||||
|
||||
message PushMessageReply {
|
||||
// True — сообщение принято (no-op с пустым текстом/диалогом — accepted=false).
|
||||
bool accepted = 1;
|
||||
// True — дубль dialog_id+msg_id уже в очереди (очередь не выросла).
|
||||
bool duplicate = 2;
|
||||
}
|
||||
|
||||
message SyncDialogsRequest {
|
||||
// Актуальный каталог диалогов (собирает сервис, как refresh_dialogs).
|
||||
repeated DialogEntry entries = 1;
|
||||
}
|
||||
|
||||
message SyncDialogsReply {
|
||||
// Id диалогов с включённым мониторингом (зеркало сервиса после синка).
|
||||
repeated string monitored_ids = 1;
|
||||
}
|
||||
|
||||
// Статус аккаунта для ядра (shape прототипа _publish_status L315–316/status()).
|
||||
message ReportStatusRequest {
|
||||
// Фаза: idle|phone|code|password|qr|ready.
|
||||
string phase = 1;
|
||||
// Клиент подключён и авторизован.
|
||||
bool connected = 2;
|
||||
// Realtime-listener жив.
|
||||
bool listener = 3;
|
||||
// Аккаунт "@username" (пуст после выхода) → KV tgAccount.
|
||||
string account = 4;
|
||||
// Текст ошибки (пуст, если нет) → KV tgStatus.error.
|
||||
optional string error = 5;
|
||||
// URL QR-входа при phase == "qr".
|
||||
optional string qr_url = 6;
|
||||
}
|
||||
|
||||
message ReportStatusReply {
|
||||
// True — статус принят и сохранён ядром.
|
||||
bool ok = 1;
|
||||
}
|
||||
Reference in New Issue
Block a user