SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/ Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue, контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер), Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог). Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0, тесты 1340/130/52/38/9 зелёные.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
<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="sources.proto" GrpcServices="Both" />
|
||||
<Protobuf Include="ml.proto" GrpcServices="Both" />
|
||||
<Protobuf Include="ai.proto" GrpcServices="Both" />
|
||||
<Protobuf Include="storage.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,138 @@
|
||||
//
|
||||
// ядро передаёт в теле каждого запроса готовые (заполненные) промпты и/или
|
||||
// текст + конфиг активного провайдера (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 каждого
|
||||
// запроса — сервис настроек тенанта не знает и не хранит.
|
||||
//
|
||||
// tenant-id — id тенанта (строка; учёт токенов в ядре по нему);
|
||||
// service-token — общий токен сервисов (env DEAL_SERVICE_TOKEN); неверный/
|
||||
// пустой → UNAUTHENTICATED.
|
||||
//
|
||||
// INVALID_ARGUMENT — невалидный запрос (пустой текст/промпт и т.п.);
|
||||
// UNAVAILABLE — провайдер не ответил корректно после ретраев; detail =
|
||||
// «ИИ (имя) не ответил корректно — повторите попытку через
|
||||
// несколько секунд» (ядро падает в локальный разбор).
|
||||
//
|
||||
// Берётся из usage API-ответа провайдера; при отсутствии оценивается по
|
||||
// символам (≈chars/4). Ядро копит значения в tenant-KV aiTokenUsage.
|
||||
//
|
||||
// Deadlines (клиент ядра): все RPC — 120 с (90 с провайдер + ретраи 0.8/2 с;
|
||||
syntax = "proto3";
|
||||
|
||||
package deal.ai.v1;
|
||||
|
||||
option csharp_namespace = "Deal.Grpc.Ai";
|
||||
|
||||
service AiService {
|
||||
// ядро шлёт заполненный aiFilterPrompt (fill_prompt делает ядро) и текст;
|
||||
// решение {pass, reason}. Ветку «фильтр не применялся» (aiFilterEnabled/
|
||||
// недоступность) обрабатывает ядро до вызова — сервис всегда отвечает.
|
||||
rpc Filter(FilterRequest) returns (FilterReply);
|
||||
|
||||
// «Доски + примеры разметки + Сообщение»; сервис возвращает извлечённый
|
||||
// ответ модели как json-строку (типовую схему задаёт промпт). Строгий
|
||||
// clean_budget/build_contacts).
|
||||
rpc Classify(ClassifyRequest) returns (ClassifyReply);
|
||||
|
||||
// Генерация ключевых слов для discovery-задачи по описанию (фикс. промпт
|
||||
rpc GenerateKeywords(GenerateKeywordsRequest) returns (GenerateKeywordsReply);
|
||||
|
||||
// Оценка соответствия сообщения задаче поиска (промпт discovery_eval
|
||||
// Ядро зовёт только при aiEnabled; сбой/не-JSON — фолбэк на эвристику.
|
||||
rpc EvaluateFit(EvaluateFitRequest) returns (EvaluateFitReply);
|
||||
}
|
||||
|
||||
// --- Запросы/ответы AiService ---
|
||||
|
||||
// aiConfigs и передаёт в теле каждого запроса; сервис не хранит настроек).
|
||||
// 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 с подстановкой
|
||||
string prompt = 1;
|
||||
string text = 2;
|
||||
ProviderConfig provider_config = 3;
|
||||
}
|
||||
|
||||
message FilterReply {
|
||||
// True — сообщение проходит фильтр (не спам/реклама/служебное).
|
||||
bool pass = 1;
|
||||
// Причина отказа при pass=false (текст ветки filter_ai; пуст при пропуске).
|
||||
optional string reason = 2;
|
||||
Usage usage = 3;
|
||||
}
|
||||
|
||||
message ClassifyRequest {
|
||||
string system_prompt = 1;
|
||||
// user-контекст: «Доски + примеры разметки + Новое сообщение» (собирает
|
||||
string user_context = 2;
|
||||
ProviderConfig provider_config = 3;
|
||||
}
|
||||
|
||||
message ClassifyReply {
|
||||
// True — модель вернула разбираемый JSON (ok=false — ответ без JSON после
|
||||
// ретраев; ядро трактует как «не разобрано» и падает в локальный путь).
|
||||
bool ok = 1;
|
||||
// Сырой JSON-ответ модели (строкой; маппинг в карточку — в ядре).
|
||||
optional string json = 2;
|
||||
Usage usage = 3;
|
||||
}
|
||||
|
||||
message GenerateKeywordsRequest {
|
||||
string description = 1;
|
||||
ProviderConfig provider_config = 2;
|
||||
}
|
||||
|
||||
message GenerateKeywordsReply {
|
||||
// Сгенерированные ключи (пустой список — модель не выделила ключи;
|
||||
repeated string keywords = 1;
|
||||
Usage usage = 2;
|
||||
}
|
||||
|
||||
message EvaluateFitRequest {
|
||||
// Текст сообщения для оценки (выборка кандидата; ядро ограничивает 4000).
|
||||
string text = 1;
|
||||
string description = 2;
|
||||
repeated string keywords = 3;
|
||||
ProviderConfig provider_config = 4;
|
||||
}
|
||||
|
||||
message EvaluateFitReply {
|
||||
// True — сообщение относится к сфере/задаче (JSON {"fit": 0|1}).
|
||||
bool fit = 1;
|
||||
// Краткая причина решения модели (пуст, если модель её не дала).
|
||||
optional string reason = 2;
|
||||
Usage usage = 3;
|
||||
}
|
||||
|
||||
// из usage API-ответа, при отсутствии — по символам ≈chars/4).
|
||||
message Usage {
|
||||
// Токены запроса (system + user).
|
||||
uint32 prompt = 1;
|
||||
// Токены ответа модели.
|
||||
uint32 completion = 2;
|
||||
// Суммарно (prompt + completion; может отличаться от суммы при подсчёте
|
||||
// провайдером — берём как есть).
|
||||
uint32 total = 3;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
//
|
||||
// (MlPredictResultDto/MlServiceStatusDto/MlEvalDto/MlResetResultDto).
|
||||
// Модель per-tenant: пул в ml-service, файл SQLite data/ml/<tenantId>.sqlite
|
||||
//
|
||||
// tenant-id — id тенанта (строка; модель тенанта — в пуле сервиса);
|
||||
// service-token — общий токен сервисов (env DEAL_SERVICE_TOKEN); неверный/
|
||||
// пустой → UNAUTHENTICATED.
|
||||
//
|
||||
// INVALID_ARGUMENT — невалидный запрос (пустой text и т.п.);
|
||||
// UNAVAILABLE — хранилище модели недоступно (ядро отвечает «не уверен»,
|
||||
//
|
||||
// Семантика неготовой модели: Predict НЕ ошибка — модель без опыта отвечает
|
||||
// фиксированным «не уверен»: take=false, label пуст, scores пуст, hits=0,
|
||||
//
|
||||
// Deadlines (клиент ядра): Predict — 5 с; Status/Reset — 10 с; TrainBatch — 30 с
|
||||
// (батч ≤100 примеров, одна транзакция).
|
||||
syntax = "proto3";
|
||||
|
||||
package deal.ml.v1;
|
||||
|
||||
option csharp_namespace = "Deal.Grpc.Ml";
|
||||
|
||||
service MlService {
|
||||
// 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);
|
||||
|
||||
// classes — «label → вес» (round 2, по убыванию); eval — самооценка по окну
|
||||
// последних подтверждённых решений (EVAL_WINDOW). Модель создаётся лениво
|
||||
rpc Status(StatusRequest) returns (StatusReply);
|
||||
|
||||
// терминов и журнала самооценки + пересоздание файла. Ok=true; мягкая
|
||||
// ошибка — Ok=false + error (ядро чистит свою ml_outbox только при успехе).
|
||||
rpc Reset(ResetRequest) returns (ResetReply);
|
||||
|
||||
// пакетные вставки терминов; самооценка по действиям пользователя (delta=1,
|
||||
// не t:*) до применения. Ответ — число применённых примеров.
|
||||
rpc TrainBatch(TrainBatchRequest) returns (TrainBatchReply);
|
||||
}
|
||||
|
||||
message PredictRequest {
|
||||
// Текст сообщения (ядро передаёт уже обрезанный/нормализованный, как
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
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 — снять метку;
|
||||
double delta = 3;
|
||||
}
|
||||
|
||||
message TrainBatchReply {
|
||||
// Число применённых примеров (= len(items) при успехе).
|
||||
int32 learned = 1;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// Контракт входящего потока источников: сервис-источник → ядро.
|
||||
//
|
||||
// Любой источник (telegram, whatsapp, avito, сайт, файл, excel) приводит свои
|
||||
// данные к единому контракту SourceItem и шлёт их одним вызовом PushSource.
|
||||
// Ядро не знает о природе источника: вид задаётся полем source.kind.
|
||||
//
|
||||
// tenant-id — id тенанта (metadata; единственный источник принадлежности);
|
||||
// service-token — общий токен сервисов (env DEAL_SERVICE_TOKEN); неверный/
|
||||
// пустой → UNAUTHENTICATED.
|
||||
//
|
||||
// Файлы вложений источник сам выгружает в сервис данных (storage.proto) и
|
||||
// передаёт здесь ссылкой в content.data.
|
||||
syntax = "proto3";
|
||||
|
||||
package deal.sources.v1;
|
||||
|
||||
option csharp_namespace = "Deal.Grpc.Sources";
|
||||
|
||||
service SourceIngressService {
|
||||
// Запись источника → очередь конвейера ядра. Дубль (вид+оригинал+внешний id)
|
||||
// уже в очереди — очередь не растёт (duplicate=true).
|
||||
rpc PushSource(PushSourceRequest) returns (PushSourceReply);
|
||||
}
|
||||
|
||||
message PushSourceRequest {
|
||||
SourceRefProto source = 1;
|
||||
SourceContentProto content = 2;
|
||||
// True — вернуть из отсева: правила/устарелость/ИИ-фильтр пропускаются.
|
||||
bool force = 3;
|
||||
}
|
||||
|
||||
message SourceRefProto {
|
||||
// Дискриминатор источника, задаёт владелец (telegram/file/local/...).
|
||||
string kind = 1;
|
||||
// Идентификатор записи в источнике (сообщение/строка/файл).
|
||||
optional string external_id = 2;
|
||||
// Подпись источника для интерфейса.
|
||||
optional string display_name = 3;
|
||||
// Ссылка на оригинал (url, deep-link, путь).
|
||||
optional string origin_ref = 4;
|
||||
optional string author = 5;
|
||||
// Время получения записи, epoch-ms.
|
||||
int64 received_at = 6;
|
||||
// Прочие метаданные источника (в т.ч. цвет интерфейса — hue).
|
||||
map<string, string> extra = 7;
|
||||
}
|
||||
|
||||
message SourceContentProto {
|
||||
optional string text = 1;
|
||||
optional string html = 2;
|
||||
optional string author = 3;
|
||||
optional string subject = 4;
|
||||
// Вложения: ссылки на объекты сервиса данных.
|
||||
repeated DataRefProto data = 5;
|
||||
// Ссылки, не являющиеся файлами.
|
||||
repeated string links = 6;
|
||||
repeated ContactRefProto contacts = 7;
|
||||
map<string, string> extra = 8;
|
||||
}
|
||||
|
||||
message DataRefProto {
|
||||
string id = 1;
|
||||
string ref = 2;
|
||||
optional string kind = 3;
|
||||
optional string mime_type = 4;
|
||||
optional string file_name = 5;
|
||||
optional int64 size = 6;
|
||||
optional int32 width = 7;
|
||||
optional int32 height = 8;
|
||||
optional double duration_sec = 9;
|
||||
optional string preview_ref = 10;
|
||||
optional string caption = 11;
|
||||
optional int32 order = 12;
|
||||
map<string, string> meta = 13;
|
||||
}
|
||||
|
||||
message ContactRefProto {
|
||||
optional string kind = 1;
|
||||
optional string name = 2;
|
||||
optional string phone = 3;
|
||||
optional string email = 4;
|
||||
optional string url = 5;
|
||||
}
|
||||
|
||||
message PushSourceReply {
|
||||
// True — запись принята (пустой текст и пустой контент — accepted=false).
|
||||
bool accepted = 1;
|
||||
// True — дубль уже в очереди (очередь не выросла).
|
||||
bool duplicate = 2;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// Контракт общего сервиса данных: загрузка/выгрузка/стат/удаление объектов вложений.
|
||||
//
|
||||
// tenant-id — id тенанта (metadata); объект хранится в пространстве тенанта;
|
||||
// service-token — общий токен сервисов (env DEAL_SERVICE_TOKEN); неверный/пустой →
|
||||
// UNAUTHENTICATED (интерцептор хоста).
|
||||
//
|
||||
// Тип объекта (image/video/audio/document/archive/other) определяет сервис —
|
||||
// контракт типы вложений не задаёт.
|
||||
syntax = "proto3";
|
||||
|
||||
package deal.storage.v1;
|
||||
|
||||
option csharp_namespace = "Deal.Grpc.Storage";
|
||||
|
||||
service StorageService {
|
||||
// Поток: первое сообщение несёт meta, далее — data.
|
||||
rpc Upload(stream UploadRequest) returns (UploadReply);
|
||||
|
||||
// Поток содержимого объекта частями.
|
||||
rpc Download(DownloadRequest) returns (stream DownloadChunk);
|
||||
|
||||
// Дескриптор объекта (без содержимого).
|
||||
rpc Stat(StatRequest) returns (StatReply);
|
||||
|
||||
// Удаление объекта (отсутствующий — успех).
|
||||
rpc Delete(DeleteRequest) returns (DeleteReply);
|
||||
}
|
||||
|
||||
message UploadRequest {
|
||||
UploadMeta meta = 1;
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
message UploadMeta {
|
||||
string tenant_id = 1;
|
||||
string file_name = 2;
|
||||
string content_type = 3;
|
||||
}
|
||||
|
||||
message UploadReply {
|
||||
// Идентификатор объекта — ключ в хранилище.
|
||||
string id = 1;
|
||||
// Ссылка на скачивание/отображение.
|
||||
string ref = 2;
|
||||
// Тип, определённый сервисом (image/video/audio/document/archive/other).
|
||||
string kind = 3;
|
||||
string mime_type = 4;
|
||||
string file_name = 5;
|
||||
int64 size = 6;
|
||||
optional int32 width = 7;
|
||||
optional int32 height = 8;
|
||||
optional double duration_sec = 9;
|
||||
optional string preview_ref = 10;
|
||||
}
|
||||
|
||||
message DownloadRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message DownloadChunk {
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
message StatRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message StatReply {
|
||||
bool found = 1;
|
||||
UploadReply info = 2;
|
||||
}
|
||||
|
||||
message DeleteRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message DeleteReply {}
|
||||
@@ -0,0 +1,390 @@
|
||||
//
|
||||
// * TelegramService — команды ядра к telegram-service (порт-гейт ITelegramGateway):
|
||||
// подключение/отключение аккаунта, каталог диалогов, мониторинг, backfill,
|
||||
// превью, discovery-операции (поиск/инфо/чтение/вступление/выход);
|
||||
// * IngressService — исходящий поток telegram-service → ядро: синхронизация
|
||||
// каталога (SyncDialogs), статус аккаунта (ReportStatus).
|
||||
// * Сообщения мониторящихся диалогов приходят в ядро generic-контрактом
|
||||
// sources.proto (PushSource) — ядро не привязано к природе источника.
|
||||
//
|
||||
//
|
||||
// tenant-id — id тенанта (строка; единственный источник принадлежности,
|
||||
// полю в теле не доверяем);
|
||||
// service-token — общий токен сервисов (env DEAL_SERVICE_TOKEN); неверный/
|
||||
// пустой → UNAUTHENTICATED.
|
||||
//
|
||||
// INVALID_ARGUMENT — неверный ввод/неверный код/неверный пароль и т.п.;
|
||||
// NOT_FOUND — диалог/сущность не найдены (нет сессии тенанта и т.п.);
|
||||
// FAILED_PRECONDITION— операция невозможна в текущей фазе (нет сессии и т.п.);
|
||||
// RESOURCE_EXHAUSTED — FloodWait Telegram (detail начинается с префикса "flood");
|
||||
// UNAVAILABLE — недоступность Telegram/сети (безопасный повтор).
|
||||
//
|
||||
// Значения строк (канон контракта, .NET-код обеих сторон — новый):
|
||||
// * kind: channel (канал) | group (группа/супергруппа) | forum (форум) |
|
||||
// channel, megagroup/gigagroup/group → group, остальное → chat.
|
||||
// Forum выставляется отдельным флагом is_forum (GetInfo); в
|
||||
// каталоге (RefreshDialogs) форум приходит как group.
|
||||
//
|
||||
// * быстрые команды статуса/мониторинга — 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 {
|
||||
// само. Нет сессии — FAILED_PRECONDITION «Telegram не подключён».
|
||||
rpc GetStatus(GetStatusRequest) returns (GetStatusReply);
|
||||
|
||||
// не заданы оператором» до вызова. Ответ: новая фаза ("code").
|
||||
rpc StartPhone(StartPhoneRequest) returns (StartPhoneReply);
|
||||
|
||||
// если аккаунт уже авторизован — фаза "ready", qrUrl пуст.
|
||||
rpc StartQr(StartQrRequest) returns (StartQrReply);
|
||||
|
||||
// «Код истёк — запросите новый» → INVALID_ARGUMENT. Нужен 2FA — фаза
|
||||
// "password", иначе "ready". Фазы вне "code" → FAILED_PRECONDITION.
|
||||
rpc SendCode(SendCodeRequest) returns (SendCodeReply);
|
||||
|
||||
// пароль» → INVALID_ARGUMENT. Ответ: фаза ("ready").
|
||||
rpc SendPassword(SendPasswordRequest) returns (SendPasswordReply);
|
||||
|
||||
rpc Logout(LogoutRequest) returns (LogoutReply);
|
||||
|
||||
// актуальный список sources диалогов аккаунта (entries). Удаление/обновление
|
||||
rpc RefreshDialogs(RefreshDialogsRequest) returns (RefreshDialogsReply);
|
||||
|
||||
// зеркало monitored в сервисе. Первый backfill (force=false) запускает ядро
|
||||
// отдельным RPC Backfill. Ответ: ok/enabled.
|
||||
rpc SetMonitor(SetMonitorRequest) returns (SetMonitorReply);
|
||||
|
||||
// ok/count/enabled (count — сколько диалогов в каталоге тенанта).
|
||||
rpc SetMonitorAll(SetMonitorAllRequest) returns (SetMonitorAllReply);
|
||||
|
||||
// Перечитать последние ~10 сообщений диалога и отправить их в ядро потоком
|
||||
// mark-as-read). force=true — «Перечитать» по кнопке даже для разобранных.
|
||||
// Ответ: сколько сообщений отправлено (processed).
|
||||
rpc Backfill(BackfillRequest) returns (BackfillReply);
|
||||
|
||||
// свежие из Telegram; признак lead и фолбэк на БД добавляет ядро
|
||||
rpc ReadRecent(ReadRecentRequest) returns (ReadRecentReply);
|
||||
|
||||
// Исходное сообщение источника по id (remote-просмотр исходника карточки).
|
||||
// found=false — сообщение не найдено/удалено (не ошибка RPC).
|
||||
rpc ReadSource(ReadSourceRequest) returns (ReadSourceReply);
|
||||
|
||||
// Пауза анти-бана после поиска — внутри сервиса. Личные чаты/боты ядро
|
||||
rpc Search(SearchRequest) returns (SearchReply);
|
||||
|
||||
// hue + participants и is_forum (полный чат). Сбои определения не роняют
|
||||
// RPC: participants пуст, остальные поля — из entity/каталога.
|
||||
rpc GetInfo(GetInfoRequest) returns (GetInfoReply);
|
||||
|
||||
// Выборка последних сообщений источника для оценки кандидата
|
||||
// недоступна (приватный/закрытый источник) — ok=false, error="no_history",
|
||||
// это НЕ ошибка RPC. limit ≥ 1; topic_id/topic_title заполнены для форумов.
|
||||
rpc ReadForEval(ReadForEvalRequest) returns (ReadForEvalReply);
|
||||
|
||||
// FloodWait → RESOURCE_EXHAUSTED (detail с префиксом "flood").
|
||||
rpc Join(JoinRequest) returns (JoinReply);
|
||||
|
||||
// диалога/членства.
|
||||
rpc Leave(LeaveRequest) returns (LeaveReply);
|
||||
}
|
||||
|
||||
// --- Запросы/ответы TelegramService ---
|
||||
|
||||
message GetStatusRequest {}
|
||||
|
||||
message GetStatusReply {
|
||||
// Фаза входа: idle|phone|code|password|qr|ready.
|
||||
string phase = 1;
|
||||
// Клиент Telegram подключён и авторизован.
|
||||
bool connected = 2;
|
||||
// Жив ли realtime-listener (поток новых сообщений → PushMessage).
|
||||
bool listener = 3;
|
||||
// Аккаунт "@username" (для справки; источник истины — KV tgAccount по
|
||||
string account = 4;
|
||||
// Текст последней ошибки (null, если ошибки нет).
|
||||
optional string error = 5;
|
||||
// URL QR-входа (заполнен только при phase == "qr").
|
||||
optional string qr_url = 6;
|
||||
}
|
||||
|
||||
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).
|
||||
repeated DialogEntry entries = 1;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
int32 limit = 2;
|
||||
}
|
||||
|
||||
message ReadRecentReply {
|
||||
// Последние сообщения (от новых к старым). lead/фолбэк на БД — в ядре.
|
||||
repeated PreviewMessage messages = 1;
|
||||
}
|
||||
|
||||
message ReadSourceRequest {
|
||||
// Id диалога-источника.
|
||||
string dialog_id = 1;
|
||||
// Id исходного сообщения в Telegram.
|
||||
int64 msg_id = 2;
|
||||
}
|
||||
|
||||
message ReadSourceReply {
|
||||
// True — сообщение найдено и передано; false — нет (поля пусты).
|
||||
bool found = 1;
|
||||
// Текст исходного сообщения.
|
||||
optional string text = 2;
|
||||
// Время сообщения, epoch-ms.
|
||||
optional int64 time = 3;
|
||||
}
|
||||
|
||||
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;
|
||||
int32 limit = 2;
|
||||
}
|
||||
|
||||
message SearchReply {
|
||||
// Найденные источники (каналы/группы; личные чаты/ботов отсеивает ядро).
|
||||
repeated DialogEntry results = 1;
|
||||
}
|
||||
|
||||
message GetInfoRequest {
|
||||
// Id источника (подписанный; из каталога или результата поиска).
|
||||
string dialog_id = 1;
|
||||
}
|
||||
|
||||
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;
|
||||
bool is_forum = 7;
|
||||
}
|
||||
|
||||
message GetInfoReply {
|
||||
ChannelInfo info = 1;
|
||||
}
|
||||
|
||||
message ReadForEvalRequest {
|
||||
// Id источника.
|
||||
string dialog_id = 1;
|
||||
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;
|
||||
}
|
||||
|
||||
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 → ядро
|
||||
// tenantId из metadata → собственный scope с ITenantContext.SetTenant)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
service IngressService {
|
||||
// Синхронизация каталога диалогов: ядро применяет entries (SyncFromTelegram:
|
||||
// авто-мониторинг по autoMonitorNew, обновление, удаление отсутствующих) и
|
||||
// отвечает актуальным списком monitored id — сервис держит зеркало
|
||||
rpc SyncDialogs(SyncDialogsRequest) returns (SyncDialogsReply);
|
||||
|
||||
// Периодический/событийный статус аккаунта: ядро пишет KV tgStatus/tgAccount
|
||||
rpc ReportStatus(ReportStatusRequest) returns (ReportStatusReply);
|
||||
}
|
||||
|
||||
message SyncDialogsRequest {
|
||||
// Актуальный каталог диалогов (собирает сервис, как refresh_dialogs).
|
||||
repeated DialogEntry entries = 1;
|
||||
}
|
||||
|
||||
message SyncDialogsReply {
|
||||
// Id диалогов с включённым мониторингом (зеркало сервиса после синка).
|
||||
repeated string monitored_ids = 1;
|
||||
}
|
||||
|
||||
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