Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
using Deal.Api.Events;
|
||||
using Deal.Api.Http;
|
||||
using Deal.Contracts.Integrations;
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
|
||||
namespace Deal.Api.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Эндпоинты ИИ-предложений: POST /api/ai/suggest-columns и POST /api/ai/suggest-keywords
|
||||
/// (план Task 14 L476–479; прототип dashboard_routes.py L395–409).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Контракт 1:1 с прототипом и api-map §3.2 L120–121: suggest-columns → <c>{ok:true, created:N}</c>
|
||||
/// либо <c>{ok:false, reason, cooldown?}</c>; suggest-keywords → <c>{ok:true, keywords:[…]}</c> либо
|
||||
/// <c>{ok:false, reason}</c>. Причины — мягкие ошибки (HTTP 200 с ok:false + reason), статусы 4xx/5xx
|
||||
/// не мапятся. Оба требуют сессию: 401 {detail} без куки (как остальные эндпоинты контейнеров); порт
|
||||
/// IColumnSuggester резолвится из RequestServices ПОСЛЕ проверки сессии (scoped на tenant-контекст
|
||||
/// запроса). При успехе suggest-columns публикуется SSE-toast «ИИ предложил колонок: N — откройте и
|
||||
/// решите» (sparkles, 1:1 с suggest.py L162); boards_changed НЕ шлём (Ruling 5: фронт перечитывает
|
||||
/// доски сам после ok). Публикации — из эндпоинта (Ruling 5): без подписчиков — no-op.
|
||||
/// </remarks>
|
||||
public static class AiSuggestEndpoints
|
||||
{
|
||||
// Префикс группы AI-эндпоинтов этапа (роутер dashboard, prefix="/api"; пути L395/L406).
|
||||
private const string AiGroupPrefix = "/api/ai";
|
||||
|
||||
// Путь предложения колонок (dashboard_routes.py L395).
|
||||
private const string SuggestColumnsPath = "/suggest-columns";
|
||||
|
||||
// Путь предложения ключевых слов (dashboard_routes.py L404).
|
||||
private const string SuggestKeywordsPath = "/suggest-keywords";
|
||||
|
||||
// OpenAPI-тег группы (в прототипе роутер dashboard — dashboard_routes.py).
|
||||
private const string OpenApiTag = "dashboard";
|
||||
|
||||
// SSE-тип события тоста (Ruling 5; api.js слушает 'toast').
|
||||
private const string ToastEventType = "toast";
|
||||
|
||||
// Текст тоста после успешных предложений колонок (suggest.py L162, 1:1).
|
||||
private const string SuggestToastTextFormat = "ИИ предложил колонок: {0} — откройте и решите";
|
||||
|
||||
// Иконка тоста предложений колонок (sparkles, 1:1 с прототипом).
|
||||
private const string SparklesIcon = "sparkles";
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует POST /api/ai/suggest-columns и POST /api/ai/suggest-keywords.
|
||||
/// </summary>
|
||||
/// <param name="app">Построитель маршрутов приложения.</param>
|
||||
/// <returns>Построитель маршрутов для цепочки вызовов.</returns>
|
||||
public static IEndpointRouteBuilder MapAiSuggestEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup(AiGroupPrefix).WithTags(OpenApiTag);
|
||||
group.MapPost(SuggestColumnsPath, SuggestColumnsAsync);
|
||||
group.MapPost(SuggestKeywordsPath, SuggestKeywordsAsync);
|
||||
return app;
|
||||
}
|
||||
|
||||
// POST /api/ai/suggest-columns: анализ «Неразобранного» и создание колонок-предложений.
|
||||
// Ответ — результат порта 1:1: {ok:true, created:N} — доски suggested=true созданы (эндпоинт шлёт
|
||||
// SSE-toast), {ok:false, reason} (+ cooldown) — мягкая причина (HTTP 200). Кулдаун/«мало карточек»/
|
||||
// «похожие колонки уже есть» — за адаптером LocalColumnSuggester (Ruling 3).
|
||||
private static async Task<IResult> SuggestColumnsAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
|
||||
IColumnSuggester suggester = context.RequestServices.GetRequiredService<IColumnSuggester>();
|
||||
SuggestColumnsResultDto result = await suggester.SuggestColumnsAsync(ct);
|
||||
if (result.Ok)
|
||||
{
|
||||
SseBroker broker = context.RequestServices.GetRequiredService<SseBroker>();
|
||||
broker.Publish(
|
||||
context.GetCurrentUser()!.TenantId,
|
||||
ToastEventType,
|
||||
new { text = string.Format(SuggestToastTextFormat, result.Created), icon = SparklesIcon });
|
||||
}
|
||||
|
||||
return Results.Ok(result);
|
||||
}
|
||||
|
||||
// POST /api/ai/suggest-keywords: слова-маркеры сферы по карточкам (настройки «Сфера и ключи»).
|
||||
// Ответ — результат порта 1:1: {ok:true, keywords:[…]} (≤60) либо {ok:false, reason} (HTTP 200).
|
||||
private static async Task<IResult> SuggestKeywordsAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
if (!HasUser(context))
|
||||
{
|
||||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||||
}
|
||||
|
||||
IColumnSuggester suggester = context.RequestServices.GetRequiredService<IColumnSuggester>();
|
||||
SuggestKeywordsResultDto result = await suggester.SuggestKeywordsAsync(ct);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
|
||||
// Разрешена ли сессия запроса (SessionMiddleware наполняет CurrentUser и tenant-контекст).
|
||||
// context: Контекст запроса.
|
||||
private static bool HasUser(HttpContext context) => context.GetCurrentUser() is not null;
|
||||
}
|
||||
Reference in New Issue
Block a user