Deal.Api/Http -> Services/Models/Extensions; Contracts/Integrations и SharedKernel/Tenants -> Abstractions/Models; extension-классы telegram/ml -> Extensions. namespace/using/FQN мигрированы, using дедуплицированы.
100 lines
5.5 KiB
C#
100 lines
5.5 KiB
C#
using Deal.Api.Events;
|
||
using Deal.Api.Extensions;
|
||
using Deal.Api.Models;
|
||
using Deal.Api.Services;
|
||
using Deal.Contracts.Integrations.Abstractions;
|
||
using Deal.Contracts.Integrations.Models;
|
||
using Deal.Api.Dtos;
|
||
|
||
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 (!context.HasUser())
|
||
{
|
||
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 (!context.HasUser())
|
||
{
|
||
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
|
||
}
|
||
|
||
IColumnSuggester suggester = context.RequestServices.GetRequiredService<IColumnSuggester>();
|
||
SuggestKeywordsResultDto result = await suggester.SuggestKeywordsAsync(ct);
|
||
return Results.Ok(result);
|
||
}
|
||
}
|