feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
This commit is contained in:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
275
apps/api-server/Controllers/AdminController.cs
Normal file
275
apps/api-server/Controllers/AdminController.cs
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using D3ROVoice.Api.Data;
|
||||
using D3ROVoice.Api.Dtos;
|
||||
using D3ROVoice.Api.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace D3ROVoice.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AdminController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly ISttProxyService _sttService;
|
||||
private static readonly DateTime _serverStartTime = DateTime.UtcNow;
|
||||
|
||||
public AdminController(AppDbContext db, ISttProxyService sttService)
|
||||
{
|
||||
_db = db;
|
||||
_sttService = sttService;
|
||||
}
|
||||
|
||||
[HttpGet("stats")]
|
||||
public async Task<IActionResult> GetStats()
|
||||
{
|
||||
var totalUsers = await _db.Users.CountAsync();
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var activeUsersToday = await _db.Users.CountAsync(u => u.LastLoginAt >= today);
|
||||
var totalRequests = await _db.UsageLogs.CountAsync() + await _db.SttUsageLogs.CountAsync();
|
||||
var totalLlmCost = await _db.UsageLogs.SumAsync(u => (decimal?)u.CalculatedCost) ?? 0m;
|
||||
var totalSttCost = await _db.SttUsageLogs.SumAsync(u => (decimal?)u.CalculatedCost) ?? 0m;
|
||||
var totalCost = totalLlmCost + totalSttCost;
|
||||
var errorCount = await _db.ErrorLogs.CountAsync();
|
||||
|
||||
var recentErrors = await _db.ErrorLogs
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.Take(10)
|
||||
.Select(e => new ServerErrorLogDto(e.Id, e.ErrorType, e.Message, e.Endpoint, e.CreatedAt))
|
||||
.ToListAsync();
|
||||
|
||||
var uptime = (DateTime.UtcNow - _serverStartTime).TotalSeconds;
|
||||
|
||||
var stats = new ServerStatsDto(
|
||||
totalUsers,
|
||||
activeUsersToday,
|
||||
totalRequests,
|
||||
totalCost,
|
||||
uptime,
|
||||
errorCount,
|
||||
recentErrors
|
||||
);
|
||||
|
||||
return Ok(stats);
|
||||
}
|
||||
|
||||
[HttpGet("users")]
|
||||
public async Task<IActionResult> GetUsers()
|
||||
{
|
||||
var users = await _db.Users
|
||||
.OrderByDescending(u => u.CreatedAt)
|
||||
.Select(u => new UserInfoDto(u.Id, u.Email, u.Role, u.CreatedAt, u.LastLoginAt, u.IsActive))
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(users);
|
||||
}
|
||||
|
||||
// ── LLM Model Endpoints ───────────────────────────────────────────────
|
||||
|
||||
[HttpGet("endpoints")]
|
||||
public async Task<IActionResult> GetEndpoints()
|
||||
{
|
||||
var endpoints = await _db.ModelEndpoints
|
||||
.OrderBy(m => m.Id)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(endpoints);
|
||||
}
|
||||
|
||||
[HttpPost("endpoints")]
|
||||
public async Task<IActionResult> CreateEndpoint([FromBody] CreateModelEndpointDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.ModelId) || string.IsNullOrWhiteSpace(dto.ModelName))
|
||||
{
|
||||
return BadRequest(new { message = "ModelId와 ModelName은 필수 항목입니다." });
|
||||
}
|
||||
|
||||
var existing = await _db.ModelEndpoints.FirstOrDefaultAsync(m => m.ModelId == dto.ModelId);
|
||||
if (existing != null)
|
||||
{
|
||||
return Conflict(new { message = "이미 존재하는 ModelId입니다." });
|
||||
}
|
||||
|
||||
var endpoint = new ServiceModelEndpoint
|
||||
{
|
||||
ModelId = dto.ModelId.Trim(),
|
||||
ModelName = dto.ModelName.Trim(),
|
||||
Provider = dto.Provider.Trim(),
|
||||
EndpointUrl = dto.EndpointUrl.Trim(),
|
||||
ApiKey = dto.ApiKey ?? "",
|
||||
CostPer1kPromptTokens = dto.CostPer1kPromptTokens,
|
||||
CostPer1kCompletionTokens = dto.CostPer1kCompletionTokens,
|
||||
IsActive = true,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_db.ModelEndpoints.Add(endpoint);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(endpoint);
|
||||
}
|
||||
|
||||
[HttpPut("endpoints/{id}")]
|
||||
public async Task<IActionResult> UpdateEndpoint(int id, [FromBody] UpdateModelEndpointDto dto)
|
||||
{
|
||||
var endpoint = await _db.ModelEndpoints.FindAsync(id);
|
||||
if (endpoint == null) return NotFound();
|
||||
|
||||
endpoint.ModelName = dto.ModelName.Trim();
|
||||
endpoint.Provider = dto.Provider.Trim();
|
||||
endpoint.EndpointUrl = dto.EndpointUrl.Trim();
|
||||
endpoint.ApiKey = dto.ApiKey ?? "";
|
||||
endpoint.CostPer1kPromptTokens = dto.CostPer1kPromptTokens;
|
||||
endpoint.CostPer1kCompletionTokens = dto.CostPer1kCompletionTokens;
|
||||
endpoint.IsActive = dto.IsActive;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return Ok(endpoint);
|
||||
}
|
||||
|
||||
[HttpDelete("endpoints/{id}")]
|
||||
public async Task<IActionResult> DeleteEndpoint(int id)
|
||||
{
|
||||
var endpoint = await _db.ModelEndpoints.FindAsync(id);
|
||||
if (endpoint == null) return NotFound();
|
||||
|
||||
_db.ModelEndpoints.Remove(endpoint);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return Ok(new { message = "삭제되었습니다." });
|
||||
}
|
||||
|
||||
// ── STT / Transcription Provider Endpoints ────────────────────────────
|
||||
|
||||
[HttpGet("stt-endpoints")]
|
||||
public async Task<IActionResult> GetSttEndpoints()
|
||||
{
|
||||
var endpoints = await _sttService.GetAllEndpointsAsync();
|
||||
return Ok(endpoints);
|
||||
}
|
||||
|
||||
[HttpGet("stt-endpoints/{id}")]
|
||||
public async Task<IActionResult> GetSttEndpoint(int id)
|
||||
{
|
||||
var endpoints = await _sttService.GetAllEndpointsAsync();
|
||||
var endpoint = endpoints.FirstOrDefault(e => e.Id == id);
|
||||
if (endpoint == null) return NotFound(new { message = $"STT Endpoint {id} not found." });
|
||||
return Ok(endpoint);
|
||||
}
|
||||
|
||||
[HttpPost("stt-endpoints")]
|
||||
public async Task<IActionResult> CreateSttEndpoint([FromBody] CreateSttEndpointDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Name) || string.IsNullOrWhiteSpace(dto.EndpointUrl))
|
||||
{
|
||||
return BadRequest(new { message = "이름과 Endpoint URL은 필수입니다." });
|
||||
}
|
||||
|
||||
var endpoint = await _sttService.CreateEndpointAsync(dto);
|
||||
return Ok(endpoint);
|
||||
}
|
||||
|
||||
[HttpPut("stt-endpoints/{id}")]
|
||||
public async Task<IActionResult> UpdateSttEndpoint(int id, [FromBody] UpdateSttEndpointDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var endpoint = await _sttService.UpdateEndpointAsync(id, dto);
|
||||
return Ok(endpoint);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return NotFound(new { message = $"STT Endpoint {id} not found." });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("stt-endpoints/{id}")]
|
||||
public async Task<IActionResult> DeleteSttEndpoint(int id)
|
||||
{
|
||||
var deleted = await _sttService.DeleteEndpointAsync(id);
|
||||
if (!deleted) return NotFound();
|
||||
return Ok(new { message = "STT 엔드포인트가 성공적으로 삭제되었습니다." });
|
||||
}
|
||||
|
||||
[HttpPost("stt-endpoints/{id}/set-default")]
|
||||
public async Task<IActionResult> SetDefaultSttEndpoint(int id)
|
||||
{
|
||||
var success = await _sttService.SetDefaultEndpointAsync(id);
|
||||
if (!success) return NotFound();
|
||||
return Ok(new { id, isDefault = true, success = true, message = "기본 클라우드 전사 프로바이더로 설정되었습니다." });
|
||||
}
|
||||
|
||||
[HttpPost("stt-endpoints/{id}/test")]
|
||||
public async Task<IActionResult> TestSttEndpoint(int id)
|
||||
{
|
||||
var result = await _sttService.TestEndpointAsync(id);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost("stt-endpoints/test-direct")]
|
||||
public async Task<IActionResult> TestSttDirect([FromQuery] string endpointUrl, [FromQuery] string? apiKey)
|
||||
{
|
||||
var result = await _sttService.TestEndpointAsync(0, apiKey, endpointUrl);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("stt-usage")]
|
||||
public async Task<IActionResult> GetSttUsageReport()
|
||||
{
|
||||
var report = await _sttService.GetUsageReportAsync();
|
||||
return Ok(report);
|
||||
}
|
||||
|
||||
// ── LLM Usage Report ──────────────────────────────────────────────────
|
||||
|
||||
[HttpGet("usage")]
|
||||
public async Task<IActionResult> GetUsageReport()
|
||||
{
|
||||
var logs = await _db.UsageLogs.AsNoTracking().ToListAsync();
|
||||
|
||||
var totalRequests = logs.Count;
|
||||
var totalPromptTokens = logs.Sum(l => l.PromptTokens);
|
||||
var totalCompletionTokens = logs.Sum(l => l.CompletionTokens);
|
||||
var totalCost = logs.Sum(l => l.CalculatedCost);
|
||||
|
||||
var userSummaries = logs
|
||||
.GroupBy(l => new { l.UserId, l.UserEmail })
|
||||
.Select(g => new UserUsageSummaryDto(
|
||||
g.Key.UserId,
|
||||
g.Key.UserEmail,
|
||||
g.Count(),
|
||||
g.Sum(x => x.TotalTokens),
|
||||
g.Sum(x => x.CalculatedCost)
|
||||
))
|
||||
.OrderByDescending(u => u.TotalCost)
|
||||
.ToList();
|
||||
|
||||
var modelSummaries = logs
|
||||
.GroupBy(l => l.ModelId)
|
||||
.Select(g => new ModelUsageSummaryDto(
|
||||
g.Key,
|
||||
g.Key,
|
||||
g.Count(),
|
||||
g.Sum(x => x.TotalTokens),
|
||||
g.Sum(x => x.CalculatedCost)
|
||||
))
|
||||
.OrderByDescending(m => m.TotalCost)
|
||||
.ToList();
|
||||
|
||||
var report = new UsageReportDto(
|
||||
totalRequests,
|
||||
totalPromptTokens,
|
||||
totalCompletionTokens,
|
||||
totalCost,
|
||||
userSummaries,
|
||||
modelSummaries
|
||||
);
|
||||
|
||||
return Ok(report);
|
||||
}
|
||||
}
|
||||
72
apps/api-server/Controllers/AuthController.cs
Normal file
72
apps/api-server/Controllers/AuthController.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using D3ROVoice.Api.Dtos;
|
||||
using D3ROVoice.Api.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace D3ROVoice.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
|
||||
public AuthController(IAuthService authService)
|
||||
{
|
||||
_authService = authService;
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> Register([FromBody] RegisterDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Email) || string.IsNullOrWhiteSpace(dto.Password))
|
||||
{
|
||||
return BadRequest(new { message = "이메일과 비밀번호를 입력해주세요." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _authService.RegisterAsync(dto);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Conflict(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login([FromBody] LoginDto dto)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.Email) || string.IsNullOrWhiteSpace(dto.Password))
|
||||
{
|
||||
return BadRequest(new { message = "이메일과 비밀번호를 입력해주세요." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _authService.LoginAsync(dto);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
return Unauthorized(new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("me")]
|
||||
public async Task<IActionResult> GetMe()
|
||||
{
|
||||
var email = User.FindFirstValue(ClaimTypes.Email);
|
||||
if (string.IsNullOrEmpty(email)) return Unauthorized();
|
||||
|
||||
var user = await _authService.GetUserByEmailAsync(email);
|
||||
if (user == null) return NotFound();
|
||||
|
||||
return Ok(user);
|
||||
}
|
||||
}
|
||||
49
apps/api-server/Controllers/LlmController.cs
Normal file
49
apps/api-server/Controllers/LlmController.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using D3ROVoice.Api.Dtos;
|
||||
using D3ROVoice.Api.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace D3ROVoice.Api.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class LlmController : ControllerBase
|
||||
{
|
||||
private readonly ILlmProxyService _llmService;
|
||||
|
||||
public LlmController(ILlmProxyService llmService)
|
||||
{
|
||||
_llmService = llmService;
|
||||
}
|
||||
|
||||
[HttpPost("generate")]
|
||||
public async Task<IActionResult> Generate([FromBody] LlmGenerateRequest request)
|
||||
{
|
||||
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var userEmail = User.FindFirstValue(ClaimTypes.Email) ?? "unknown@user";
|
||||
int userId = int.TryParse(userIdStr, out var id) ? id : 0;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Prompt))
|
||||
{
|
||||
return BadRequest(new { message = "Prompt는 필수 항목입니다." });
|
||||
}
|
||||
|
||||
var result = await _llmService.GenerateAsync(userId, userEmail, request);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost("chat")]
|
||||
public async Task<IActionResult> Chat([FromBody] LlmChatRequest request)
|
||||
{
|
||||
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var userEmail = User.FindFirstValue(ClaimTypes.Email) ?? "unknown@user";
|
||||
int userId = int.TryParse(userIdStr, out var id) ? id : 0;
|
||||
|
||||
var result = await _llmService.ChatAsync(userId, userEmail, request);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
109
apps/api-server/Controllers/SttController.cs
Normal file
109
apps/api-server/Controllers/SttController.cs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using D3ROVoice.Api.Data;
|
||||
using D3ROVoice.Api.Dtos;
|
||||
using D3ROVoice.Api.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace D3ROVoice.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class SttController : ControllerBase
|
||||
{
|
||||
private readonly ISttProxyService _sttService;
|
||||
|
||||
public SttController(ISttProxyService sttService)
|
||||
{
|
||||
_sttService = sttService;
|
||||
}
|
||||
|
||||
[HttpPost("transcribe")]
|
||||
[Consumes("application/json", "multipart/form-data")]
|
||||
public async Task<IActionResult> Transcribe()
|
||||
{
|
||||
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var userEmail = User.FindFirstValue(ClaimTypes.Email) ?? "user@d3ro.voice";
|
||||
int userId = int.TryParse(userIdStr, out var id) ? id : 1;
|
||||
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
var file = form.Files.GetFile("file") ?? form.Files.GetFile("audio");
|
||||
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return BadRequest(new { message = "전송할 오디오 파일(file 또는 audio)이 필요합니다." });
|
||||
}
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
await file.CopyToAsync(memoryStream);
|
||||
var audioBytes = memoryStream.ToArray();
|
||||
|
||||
var language = form["language"].ToString();
|
||||
var prompt = form["prompt"].ToString();
|
||||
var model = form["model"].ToString();
|
||||
var provider = form["provider"].ToString();
|
||||
|
||||
var request = new SttTranscribeRequest(
|
||||
AudioBase64: null,
|
||||
Language: string.IsNullOrWhiteSpace(language) ? "ko" : language,
|
||||
InitialPrompt: string.IsNullOrWhiteSpace(prompt) ? null : prompt,
|
||||
ModelId: string.IsNullOrWhiteSpace(model) ? null : model,
|
||||
Provider: string.IsNullOrWhiteSpace(provider) ? null : provider
|
||||
);
|
||||
|
||||
var result = await _sttService.TranscribeAsync(
|
||||
userId,
|
||||
userEmail,
|
||||
request,
|
||||
audioBytes,
|
||||
file.ContentType ?? "audio/webm",
|
||||
file.FileName ?? "recording.webm"
|
||||
);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Read JSON body
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
var json = await reader.ReadToEndAsync();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return BadRequest(new { message = "요청 본문이 비어있습니다." });
|
||||
}
|
||||
|
||||
var request = System.Text.Json.JsonSerializer.Deserialize<SttTranscribeRequest>(
|
||||
json,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }
|
||||
);
|
||||
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.AudioBase64))
|
||||
{
|
||||
return BadRequest(new { message = "AudioBase64 데이터가 필요합니다." });
|
||||
}
|
||||
|
||||
var result = await _sttService.TranscribeAsync(userId, userEmail, request);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("providers")]
|
||||
public async Task<IActionResult> GetActiveProviders()
|
||||
{
|
||||
var endpoints = await _sttService.GetAllEndpointsAsync();
|
||||
return Ok(endpoints);
|
||||
}
|
||||
|
||||
[HttpPost("test")]
|
||||
public async Task<IActionResult> TestConnection([FromQuery] int endpointId = 0)
|
||||
{
|
||||
var result = await _sttService.TestEndpointAsync(endpointId);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue