2025-10-11 18:25:59 +08:00

258 lines
10 KiB
C#

using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using XiaoZhiSharp.Kernels;
using XiaoZhiSharp.Protocols;
using XiaoZhiSharp.Utils;
namespace XiaoZhiSharp.Services.Chat
{
public class ChatService : IDisposable
{
private static AsyncSemaphore s_asyncSemaphore = new AsyncSemaphore();
private string TAG = "小智";
private string _wsUrl { get; set; } = "wss://api.tenclass.net/xiaozhi/v1/";
private string? _token { get; set; } = "test-token";
private string? _deviceId { get; set; }
private string? _sessionId = "";
// 首次连接
private bool _isFirst = true;
private ClientWebSocket? _webSocket = null;
private bool _disposed = false;
private System.Timers.Timer _onAudioTimeout;
#region
public WebSocketState ConnectState { get { return _webSocket?.State ?? WebSocketState.None; } }
#endregion
#region
public delegate Task MessageEventHandler(string type, string message);
public event MessageEventHandler? OnMessageEvent = null;
public delegate Task AudioEventHandler(byte[] opus);
public event AudioEventHandler? OnAudioEvent = null;
#endregion
#region
public ChatService(string wsUrl, string token, string deviceId)
{
_wsUrl = wsUrl;
_token = token;
_deviceId = deviceId;
}
#endregion
public void Start()
{
Uri uri = new Uri(_wsUrl);
_webSocket = new ClientWebSocket();
_webSocket.Options.SetRequestHeader("Authorization", "Bearer " + _token);
_webSocket.Options.SetRequestHeader("Protocol-Version", "1");
_webSocket.Options.SetRequestHeader("Device-Id", _deviceId);
_webSocket.Options.SetRequestHeader("Client-Id", Guid.NewGuid().ToString());
_webSocket.ConnectAsync(uri, CancellationToken.None);
LogConsole.InfoLine($"{TAG} 连接中...");
Task.Run(async () =>
{
await ReceiveMessagesAsync();
});
// 语音合成播报超时
_onAudioTimeout = new System.Timers.Timer(500);
_onAudioTimeout.Elapsed += async (sender, e) => await OnAudioTimeout();
_onAudioTimeout.AutoReset = false;
}
/// <summary>
/// 语音播报完成
/// </summary>
private async Task OnAudioTimeout()
{
if (OnMessageEvent != null)
await OnMessageEvent("audio_stop", "");
}
private async Task ReceiveMessagesAsync()
{
if (_webSocket == null)
return;
var buffer = new byte[1024 * 15];
while (true)
{
using (await s_asyncSemaphore.WaitAsync())
{
if (_webSocket != null && _webSocket.State == WebSocketState.Open)
{
try
{
// 首次
if (_isFirst)
{
_isFirst = false;
LogConsole.InfoLine($"{TAG} 连接成功");
await SendMessageAsync(XiaoZhi_Protocol.Hello(Global.IsMcp));
}
var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
byte[] messageBytes = new byte[result.Count];
Array.Copy(buffer, messageBytes, result.Count);
if (result.MessageType == WebSocketMessageType.Text)
{
var message = Encoding.UTF8.GetString(messageBytes);
LogConsole.ReceiveLine($"{TAG} {message}");
if (!string.IsNullOrEmpty(message))
{
dynamic? msg = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(message);
if (msg == null)
{
LogConsole.ErrorLine($"{TAG} 接收到的消息格式错误: {message}");
continue;
}
_sessionId = msg.session_id;
if (msg.type == "mcp")
{
if (OnMessageEvent != null)
await OnMessageEvent("mcp", Newtonsoft.Json.JsonConvert.SerializeObject(msg.payload));
}
// 问
if (msg.type == "stt")
{
if (OnMessageEvent != null)
await OnMessageEvent("question", System.Convert.ToString(msg.text));
if (OnMessageEvent != null)
await OnMessageEvent("audio_start", "");
}
// 答
if (msg.type == "tts")
{
if (msg.state == "sentence_start")
{
if (OnMessageEvent != null)
await OnMessageEvent("answer", System.Convert.ToString(msg.text));
}
if (msg.state == "stop")
{
if (OnMessageEvent != null)
await OnMessageEvent("answer_stop", "");
}
}
// 情感
if (msg.type == "llm")
{
if (OnMessageEvent != null)
await OnMessageEvent("emotion", System.Convert.ToString(msg.emotion));
if (OnMessageEvent != null)
await OnMessageEvent("emotion_text", System.Convert.ToString(msg.text));
}
}
}
if (result.MessageType == WebSocketMessageType.Binary)
{
_onAudioTimeout.Stop();
// 触发事件
if (OnAudioEvent != null)
await OnAudioEvent(messageBytes);
_onAudioTimeout.Start();
}
}
catch (Exception ex)
{
LogConsole.ErrorLine($"{TAG} {ex.Message}");
}
}
}
Thread.Sleep(1); // 避免过于频繁的循环
}
}
private async Task SendMessageAsync(string message)
{
if (_webSocket == null)
return;
if (_webSocket.State == WebSocketState.Open)
{
var buffer = Encoding.UTF8.GetBytes(message);
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
LogConsole.SendLine($"{TAG} {message}");
}
}
private async Task SendAudioAsync(byte[] opus)
{
if (_webSocket == null)
return;
if (_webSocket.State == WebSocketState.Open)
{
await _webSocket.SendAsync(new ArraySegment<byte>(opus), WebSocketMessageType.Binary, true, CancellationToken.None);
}
}
public async Task SendAudio(byte[] audio)
{
await SendAudioAsync(audio);
}
public async Task ChatAbort()
{
await SendMessageAsync(XiaoZhi_Protocol.Abort());
}
public async Task ChatMessage(string message)
{
//await ChatAbort();
await SendMessageAsync(XiaoZhi_Protocol.Listen_Detect(message));
}
public async Task McpMessage(string message)
{
await SendMessageAsync(XiaoZhi_Protocol.Mcp(message, _sessionId));
}
public async Task StartRecording()
{
//await ChatAbort();
await SendMessageAsync(XiaoZhi_Protocol.Listen_Start("", "manual"));
}
public async Task StartRecordingAuto()
{
//await ChatAbort();
await SendMessageAsync(XiaoZhi_Protocol.Listen_Start("", "auto"));
}
public async Task StopRecording()
{
await SendMessageAsync(XiaoZhi_Protocol.Listen_Stop(_sessionId));
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
// 关闭WebSocket连接
try
{
if (_webSocket != null && (_webSocket.State == WebSocketState.Open || _webSocket.State == WebSocketState.Connecting))
{
_webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client closing", CancellationToken.None).Wait();
}
}
catch (Exception ex)
{
LogConsole.ErrorLine($"{TAG} 关闭WebSocket连接时出错: {ex.Message}");
}
_webSocket?.Dispose();
_webSocket = null;
_onAudioTimeout?.Dispose();
}
}
}
}