feat: 长辈模式(大字大按钮 + 偏好按账号隔离)+ 语音输入(按住说话 + 实时转写 + 后端代理 DashScope ASR + 患者端专用)+ 健康仪表盘背景渐变

This commit is contained in:
MingNian
2026-07-21 10:53:16 +08:00
parent 0cb5b8e85a
commit 28f704c98e
25 changed files with 2214 additions and 131 deletions

View File

@@ -0,0 +1,69 @@
using System.Net.WebSockets;
using System.Text.Json;
using Health.Application.Speech;
namespace Health.WebApi.Endpoints;
public static class SpeechEndpoints
{
public static void MapSpeechEndpoints(this WebApplication app)
{
app.Map("/api/speech/realtime", async (
HttpContext http,
IRealtimeSpeechRecognitionProxy proxy,
ILoggerFactory loggerFactory) =>
{
if (!http.WebSockets.IsWebSocketRequest)
{
http.Response.StatusCode = StatusCodes.Status400BadRequest;
await http.Response.WriteAsJsonAsync(new
{
code = 40001,
data = (object?)null,
message = "该接口仅支持 WebSocket 连接",
});
return;
}
if (!proxy.IsConfigured)
{
http.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await http.Response.WriteAsJsonAsync(new
{
code = 50301,
data = (object?)null,
message = "实时语音识别服务尚未配置",
});
return;
}
using var socket = await http.WebSockets.AcceptWebSocketAsync();
try
{
await proxy.ProxyAsync(socket, http.RequestAborted);
}
catch (OperationCanceledException) when (http.RequestAborted.IsCancellationRequested)
{
// 客户端离开页面或断开网络。
}
catch (Exception ex)
{
loggerFactory.CreateLogger("SpeechEndpoints")
.LogError(ex, "Realtime speech proxy failed");
if (socket.State == WebSocketState.Open)
{
var payload = JsonSerializer.SerializeToUtf8Bytes(new
{
type = "error",
message = "语音服务暂时不可用,请稍后重试",
});
await socket.SendAsync(
payload,
WebSocketMessageType.Text,
true,
CancellationToken.None);
}
}
}).RequireAuthorization(policy => policy.RequireRole("User"));
}
}