507 lines
18 KiB
C#
507 lines
18 KiB
C#
|
using System;
|
|||
|
using System.Collections.Generic;
|
|||
|
using System.Net.WebSockets;
|
|||
|
using System.Runtime.InteropServices;
|
|||
|
using System.Threading.Tasks;
|
|||
|
using XiaoZhiSharp.Protocols;
|
|||
|
using XiaoZhiSharp.Services;
|
|||
|
using XiaoZhiSharp.Utils;
|
|||
|
using System.Threading;
|
|||
|
using XiaoZhiSharp.Models;
|
|||
|
|
|||
|
namespace XiaoZhiSharp
|
|||
|
{
|
|||
|
public class XiaoZhiAgent : IDisposable
|
|||
|
{
|
|||
|
private string _otaUrl { get; set; }
|
|||
|
private string _wsUrl { get; set; }
|
|||
|
private string _token { get; set; } = string.Empty;
|
|||
|
private string accessToken { get; set; } = "";
|
|||
|
private string refreshToken { get; set; } = "";
|
|||
|
private string _deviceId { get; set; } = SystemInfo.GetMacAddress();
|
|||
|
private string _clientId { get; set; } = SystemInfo.GenerateClientId();
|
|||
|
private string _userAgent { get; set; } = SystemInfo.GetUserAgent();
|
|||
|
private string _currentVersion { get; set; } = SystemInfo.GetApplicationVersion();
|
|||
|
private Services.Chat.ChatService? _chatService = null;
|
|||
|
private Services.IAudioService? _audioService = null;
|
|||
|
private Services.AudioOpusService _audioOpusService = new Services.AudioOpusService();
|
|||
|
private Services.OtaService? _otaService = null;
|
|||
|
private OtaResponse? _latestOtaResponse = null;
|
|||
|
|
|||
|
// 添加一个用于跟踪当前任务的变量
|
|||
|
private Task? _monitoringTask = null;
|
|||
|
private bool _disposed = false;
|
|||
|
private CancellationTokenSource? _recordingCts = null;
|
|||
|
|
|||
|
#region 属性
|
|||
|
public string WsUrl
|
|||
|
{
|
|||
|
get { return _wsUrl; }
|
|||
|
set { _wsUrl = value; }
|
|||
|
}
|
|||
|
public string OtaUrl
|
|||
|
{
|
|||
|
get { return _otaUrl; }
|
|||
|
set { _otaUrl = value; }
|
|||
|
}
|
|||
|
public Services.IAudioService? AudioService
|
|||
|
{
|
|||
|
get { return _audioService; }
|
|||
|
set { _audioService = value; }
|
|||
|
}
|
|||
|
public string DeviceId
|
|||
|
{
|
|||
|
get { return _deviceId; }
|
|||
|
set { _deviceId = value; }
|
|||
|
}
|
|||
|
public string ClientId
|
|||
|
{
|
|||
|
get { return _clientId; }
|
|||
|
set { _clientId = value; }
|
|||
|
}
|
|||
|
public string UserAgent
|
|||
|
{
|
|||
|
get { return _userAgent; }
|
|||
|
set { _userAgent = value; }
|
|||
|
}
|
|||
|
public string CurrentVersion
|
|||
|
{
|
|||
|
get { return _currentVersion; }
|
|||
|
set { _currentVersion = value; }
|
|||
|
}
|
|||
|
public string Token
|
|||
|
{
|
|||
|
get { return _token; }
|
|||
|
set { _token = value; }
|
|||
|
}
|
|||
|
public OtaResponse? LatestOtaResponse
|
|||
|
{
|
|||
|
get { return _latestOtaResponse; }
|
|||
|
}
|
|||
|
public bool IsPlaying
|
|||
|
{
|
|||
|
get { return _audioService != null && _audioService.IsPlaying; }
|
|||
|
}
|
|||
|
public bool IsRecording
|
|||
|
{
|
|||
|
get { return _audioService != null && _audioService.IsRecording; }
|
|||
|
}
|
|||
|
public WebSocketState ConnectState
|
|||
|
{
|
|||
|
get { return _chatService != null ? _chatService.ConnectState : 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;
|
|||
|
|
|||
|
public delegate Task AudioPcmEventHandler(byte[] pcm);
|
|||
|
public event AudioPcmEventHandler? OnAudioPcmEvent = null;
|
|||
|
|
|||
|
public delegate Task OtaEventHandler(OtaResponse? otaResponse);
|
|||
|
public event OtaEventHandler? OnOtaEvent = null;
|
|||
|
#endregion
|
|||
|
|
|||
|
#region 构造函数
|
|||
|
public XiaoZhiAgent() { }
|
|||
|
#endregion
|
|||
|
|
|||
|
public async Task<TResult<object>?> Login(LoginModel loginMdl)
|
|||
|
{
|
|||
|
_otaService ??= new Services.OtaService(_userAgent, _deviceId, _clientId);
|
|||
|
var loginRequest = loginMdl ?? new LoginModel()
|
|||
|
{
|
|||
|
TenantName = "ZXGY",
|
|||
|
Username = "admin",
|
|||
|
Password = "admin123",
|
|||
|
RememberMe = "true"
|
|||
|
};
|
|||
|
var loginResponse = await _otaService.LoginAsync(loginRequest.Url, loginRequest);
|
|||
|
|
|||
|
var data = loginResponse?.Data;
|
|||
|
if (data != null)
|
|||
|
{
|
|||
|
var jsonData = data as Newtonsoft.Json.Linq.JObject;
|
|||
|
accessToken = jsonData?["accessToken"]?.ToString() ?? "";
|
|||
|
refreshToken = jsonData?["refreshToken"]?.ToString() ?? "";
|
|||
|
}
|
|||
|
|
|||
|
return loginResponse;
|
|||
|
}
|
|||
|
|
|||
|
public async Task Start()
|
|||
|
{
|
|||
|
// 1. 首先进行OTA检查
|
|||
|
await CheckOtaUpdate();
|
|||
|
|
|||
|
// 2. 根据OTA响应决定WebSocket连接参数
|
|||
|
string wsUrl = _wsUrl;
|
|||
|
if (!string.IsNullOrWhiteSpace(_latestOtaResponse?.WebSocket?.Url))
|
|||
|
{
|
|||
|
var tempWsUrl = _latestOtaResponse.WebSocket.Url;
|
|||
|
var lastIdxOfSlash = tempWsUrl.LastIndexOf('/');
|
|||
|
var remain = tempWsUrl.Substring(0, lastIdxOfSlash);
|
|||
|
var postfifx = tempWsUrl.Substring(lastIdxOfSlash);
|
|||
|
lastIdxOfSlash = remain.LastIndexOf('/');
|
|||
|
var prefix = remain.Substring(0, lastIdxOfSlash + 1);
|
|||
|
wsUrl = $"{prefix}{_deviceId}{postfifx}";
|
|||
|
}
|
|||
|
wsUrl = _latestOtaResponse?.WebSocket?.Url ?? _wsUrl;
|
|||
|
|
|||
|
string token = accessToken;// _token;
|
|||
|
if (!string.IsNullOrWhiteSpace(_latestOtaResponse?.WebSocket?.Token))
|
|||
|
{
|
|||
|
token = _latestOtaResponse.WebSocket.Token;
|
|||
|
}
|
|||
|
//token = _latestOtaResponse?.WebSocket?.Token ?? _token;
|
|||
|
|
|||
|
LogConsole.InfoLine($"使用WebSocket URL: {wsUrl}");
|
|||
|
LogConsole.InfoLine($"使用Token: {token}");
|
|||
|
|
|||
|
// 3. 启动WebSocket连接
|
|||
|
_chatService = new Services.Chat.ChatService(wsUrl, token, _deviceId);
|
|||
|
_chatService.OnMessageEvent += ChatService_OnMessageEvent;
|
|||
|
if (Global.IsAudio)
|
|||
|
_chatService.OnAudioEvent += ChatService_OnAudioEvent;
|
|||
|
_chatService.Start();
|
|||
|
|
|||
|
// 4. 初始化音频服务
|
|||
|
if (Global.IsAudio)
|
|||
|
{
|
|||
|
if (_audioService == null)
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|||
|
{
|
|||
|
LogConsole.InfoLine("初始化音频服务-->当前操作系统是 Windows");
|
|||
|
_audioService = new AudioWaveService();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
/* PortAudio是跨平台的音频I/O库, 在 Linux 上有较好的兼容性。
|
|||
|
* 在使用前请确保系统(Linux)已安装 PortAudio 库,
|
|||
|
* 否则在使用PortAudioSharp.Stream时会报错:
|
|||
|
Unhandled exception. PortAudioSharp.PortAudioException: Error opening PortAudio Stream
|
|||
|
at PortAudioSharp.Stream..ctor(Nullable`1 inParams, Nullable`1 outParams, Double sampleRate,
|
|||
|
UInt32 framesPerBuffer, StreamFlags streamFlags, Callback callback, Object userData)
|
|||
|
*/
|
|||
|
_audioService = new AudioPortService();
|
|||
|
}
|
|||
|
}
|
|||
|
catch (Exception ex)
|
|||
|
{
|
|||
|
LogConsole.Error($"初始化音频服务失败: {ex.Message}");
|
|||
|
}
|
|||
|
}
|
|||
|
if (_audioService != null)
|
|||
|
_audioService.OnPcmAudioEvent += AudioService_OnPcmAudioEvent;
|
|||
|
}
|
|||
|
}
|
|||
|
public async Task Restart()
|
|||
|
{
|
|||
|
// 保存当前的音频服务引用,以便重新设置事件处理
|
|||
|
var currentAudioService = _audioService;
|
|||
|
|
|||
|
// 如果存在音频服务,先移除事件处理器
|
|||
|
if (currentAudioService != null)
|
|||
|
{
|
|||
|
// 移除事件处理器,避免重复订阅
|
|||
|
currentAudioService.OnPcmAudioEvent -= AudioService_OnPcmAudioEvent;
|
|||
|
}
|
|||
|
|
|||
|
// 释放现有资源
|
|||
|
_chatService?.Dispose();
|
|||
|
_otaService?.Dispose();
|
|||
|
|
|||
|
// 重置录音取消令牌
|
|||
|
_recordingCts?.Cancel();
|
|||
|
_recordingCts?.Dispose();
|
|||
|
_recordingCts = null;
|
|||
|
|
|||
|
// 重新启动服务
|
|||
|
await Start();
|
|||
|
|
|||
|
// 如果音频服务实例没有变化,则确保录音状态正确
|
|||
|
if (currentAudioService != null && _audioService == currentAudioService)
|
|||
|
{
|
|||
|
// 确保录音状态正确
|
|||
|
if (_audioService.IsRecording)
|
|||
|
{
|
|||
|
_audioService.StopRecording();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
LogConsole.InfoLine("重启完成");
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 执行OTA检查
|
|||
|
/// </summary>
|
|||
|
/// <returns></returns>
|
|||
|
public async Task<OtaResponse?> CheckOtaUpdate()
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
LogConsole.InfoLine("开始OTA检查...");
|
|||
|
|
|||
|
// 初始化OTA服务
|
|||
|
_otaService ??= new Services.OtaService(_userAgent, _deviceId, _clientId);
|
|||
|
|
|||
|
// 创建OTA请求
|
|||
|
var otaRequest = _otaService.CreateDefaultOtaRequest(_currentVersion, "", "xiaozhi-sharp", "xiaozhi-sharp-client");
|
|||
|
|
|||
|
// 发送OTA请求
|
|||
|
_latestOtaResponse = await _otaService.CheckOtaAsync(_otaUrl, accessToken, otaRequest);
|
|||
|
|
|||
|
// 触发OTA事件
|
|||
|
if (OnOtaEvent != null)
|
|||
|
await OnOtaEvent(_latestOtaResponse);
|
|||
|
|
|||
|
if (_latestOtaResponse != null)
|
|||
|
{
|
|||
|
LogConsole.InfoLine("OTA检查完成,获取到服务器配置信息");
|
|||
|
|
|||
|
// 显示激活信息
|
|||
|
if (_latestOtaResponse.Activation != null)
|
|||
|
{
|
|||
|
LogConsole.InfoLine($"激活码: {_latestOtaResponse.Activation.Code}");
|
|||
|
LogConsole.InfoLine($"激活消息: {_latestOtaResponse.Activation.Message}");
|
|||
|
}
|
|||
|
|
|||
|
// 显示固件信息
|
|||
|
if (_latestOtaResponse.Firmware != null)
|
|||
|
{
|
|||
|
LogConsole.InfoLine($"固件版本: {_latestOtaResponse.Firmware.Version}");
|
|||
|
if (!string.IsNullOrEmpty(_latestOtaResponse.Firmware.Url))
|
|||
|
{
|
|||
|
LogConsole.InfoLine($"固件下载地址: {_latestOtaResponse.Firmware.Url}");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 显示服务器时间信息
|
|||
|
if (_latestOtaResponse.ServerTime != null)
|
|||
|
{
|
|||
|
LogConsole.InfoLine($"服务器时间: {DateTimeOffset.FromUnixTimeMilliseconds(_latestOtaResponse.ServerTime.Timestamp)}");
|
|||
|
LogConsole.InfoLine($"时区: {_latestOtaResponse.ServerTime.Timezone}");
|
|||
|
}
|
|||
|
|
|||
|
// 显示MQTT配置信息
|
|||
|
if (_latestOtaResponse.Mqtt != null)
|
|||
|
{
|
|||
|
LogConsole.InfoLine($"MQTT服务器: {_latestOtaResponse.Mqtt.Endpoint}");
|
|||
|
LogConsole.InfoLine($"MQTT客户端ID: {_latestOtaResponse.Mqtt.ClientId}");
|
|||
|
}
|
|||
|
|
|||
|
// 显示WebSocket配置信息
|
|||
|
if (_latestOtaResponse.WebSocket != null)
|
|||
|
{
|
|||
|
LogConsole.InfoLine($"WebSocket服务器: {_latestOtaResponse.WebSocket.Url}");
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
LogConsole.InfoLine("OTA检查完成,使用默认配置");
|
|||
|
}
|
|||
|
|
|||
|
return _latestOtaResponse;
|
|||
|
}
|
|||
|
catch (Exception ex)
|
|||
|
{
|
|||
|
LogConsole.ErrorLine($"OTA检查异常: {ex.Message}");
|
|||
|
return null;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 创建包含WiFi信息的OTA请求
|
|||
|
/// </summary>
|
|||
|
/// <param name="ssid">WiFi网络名称</param>
|
|||
|
/// <param name="rssi">WiFi信号强度</param>
|
|||
|
/// <param name="channel">WiFi频道</param>
|
|||
|
/// <param name="ip">设备IP地址</param>
|
|||
|
/// <returns></returns>
|
|||
|
public async Task<OtaResponse?> CheckOtaUpdateWithWifi(string ssid, int rssi = -50, int channel = 1, string ip = "")
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
LogConsole.InfoLine($"开始OTA检查(WiFi: {ssid})...");
|
|||
|
|
|||
|
// 初始化OTA服务
|
|||
|
_otaService ??= new Services.OtaService(_userAgent, _deviceId, _clientId);
|
|||
|
|
|||
|
// 创建包含WiFi信息的OTA请求
|
|||
|
var otaRequest = _otaService.CreateWifiOtaRequest(_currentVersion, "",
|
|||
|
"xiaozhi-sharp-wifi", "xiaozhi-sharp-wifi-client", ssid, rssi, channel, ip);
|
|||
|
|
|||
|
// 发送OTA请求
|
|||
|
_latestOtaResponse = await _otaService.CheckOtaAsync(_otaUrl, accessToken, otaRequest);
|
|||
|
|
|||
|
// 触发OTA事件
|
|||
|
if (OnOtaEvent != null)
|
|||
|
await OnOtaEvent(_latestOtaResponse);
|
|||
|
|
|||
|
return _latestOtaResponse;
|
|||
|
}
|
|||
|
catch (Exception ex)
|
|||
|
{
|
|||
|
LogConsole.ErrorLine($"OTA检查异常: {ex.Message}");
|
|||
|
return null;
|
|||
|
}
|
|||
|
}
|
|||
|
private async Task AudioService_OnPcmAudioEvent(byte[] pcm)
|
|||
|
{
|
|||
|
byte[] opus = _audioOpusService.Encode(pcm);
|
|||
|
await _chatService.SendAudio(opus);
|
|||
|
}
|
|||
|
private async Task ChatService_OnAudioEvent(byte[] opus)
|
|||
|
{
|
|||
|
if (_audioService != null)
|
|||
|
{
|
|||
|
byte[] pcmData = _audioOpusService.Decode(opus);
|
|||
|
_audioService.AddOutSamples(pcmData);
|
|||
|
|
|||
|
if (OnAudioPcmEvent != null)
|
|||
|
await OnAudioPcmEvent(pcmData);
|
|||
|
}
|
|||
|
|
|||
|
if (OnAudioEvent != null)
|
|||
|
await OnAudioEvent(opus);
|
|||
|
}
|
|||
|
private async Task ChatService_OnMessageEvent(string type, string message)
|
|||
|
{
|
|||
|
//if (type == "answer_stop") {
|
|||
|
// await StopRecording();
|
|||
|
//}
|
|||
|
if (OnMessageEvent != null)
|
|||
|
await OnMessageEvent(type, message);
|
|||
|
}
|
|||
|
public async Task ChatMessage(string message)
|
|||
|
{
|
|||
|
if (_chatService != null)
|
|||
|
await _chatService.ChatMessage(message);
|
|||
|
}
|
|||
|
public async Task ChatAbort()
|
|||
|
{
|
|||
|
if (_chatService != null)
|
|||
|
await _chatService.ChatAbort();
|
|||
|
}
|
|||
|
public async Task McpMessage(string message)
|
|||
|
{
|
|||
|
if (_chatService != null)
|
|||
|
await _chatService.McpMessage(message);
|
|||
|
}
|
|||
|
/// <summary>
|
|||
|
/// 开始录音
|
|||
|
/// </summary>
|
|||
|
/// <param name="type">auto\manual</param>
|
|||
|
/// <returns></returns>
|
|||
|
public async Task StartRecording(string type = "manual")
|
|||
|
{
|
|||
|
if (_audioService != null)
|
|||
|
{
|
|||
|
if (type == "auto")
|
|||
|
{
|
|||
|
await _chatService.StartRecordingAuto();
|
|||
|
|
|||
|
// 创建新的监听任务
|
|||
|
_recordingCts?.Cancel(); // 如果已有任务,先取消
|
|||
|
_recordingCts = new CancellationTokenSource();
|
|||
|
var token = _recordingCts.Token;
|
|||
|
|
|||
|
_ = Task.Run(async () =>
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
while (!token.IsCancellationRequested)
|
|||
|
{
|
|||
|
if (_audioService.VadCounter >= Global.VadThreshold)
|
|||
|
{
|
|||
|
LogConsole.InfoLine($"VAD检测到静音,自动结束录音 (计数: {_audioService.VadCounter})");
|
|||
|
_audioService.StopRecording();
|
|||
|
await _chatService.StopRecording();
|
|||
|
break;
|
|||
|
}
|
|||
|
await Task.Delay(100, token); // 每0.1秒检查一次
|
|||
|
}
|
|||
|
}
|
|||
|
catch (TaskCanceledException)
|
|||
|
{
|
|||
|
// 任务被取消,正常退出
|
|||
|
}
|
|||
|
catch (Exception ex)
|
|||
|
{
|
|||
|
LogConsole.ErrorLine($"VAD监听任务异常: {ex.Message}");
|
|||
|
}
|
|||
|
}, token);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
await _chatService.StartRecording();
|
|||
|
}
|
|||
|
_audioService.StartRecording();
|
|||
|
}
|
|||
|
}
|
|||
|
public async Task StopRecording()
|
|||
|
{
|
|||
|
if (_audioService != null)
|
|||
|
{
|
|||
|
// 取消VAD监听任务
|
|||
|
_recordingCts?.Cancel();
|
|||
|
_recordingCts?.Dispose();
|
|||
|
_recordingCts = null;
|
|||
|
|
|||
|
_audioService.StopRecording();
|
|||
|
await _chatService.StopRecording();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 释放资源,在应用程序关闭时调用
|
|||
|
/// </summary>
|
|||
|
public void Dispose()
|
|||
|
{
|
|||
|
if (!_disposed)
|
|||
|
{
|
|||
|
_disposed = true;
|
|||
|
|
|||
|
// 停止VAD监听任务
|
|||
|
_recordingCts?.Cancel();
|
|||
|
_recordingCts?.Dispose();
|
|||
|
_recordingCts = null;
|
|||
|
|
|||
|
// 停止录音
|
|||
|
if (_audioService != null && _audioService.IsRecording)
|
|||
|
{
|
|||
|
_audioService.StopRecording();
|
|||
|
}
|
|||
|
|
|||
|
// 释放音频服务
|
|||
|
if (_audioService is IDisposable disposableAudioService)
|
|||
|
{
|
|||
|
disposableAudioService.Dispose();
|
|||
|
}
|
|||
|
|
|||
|
// 释放WebSocket连接
|
|||
|
_chatService?.Dispose();
|
|||
|
|
|||
|
// 释放OTA服务
|
|||
|
_otaService?.Dispose();
|
|||
|
|
|||
|
// 释放音频编码服务
|
|||
|
if (_audioOpusService is IDisposable disposableOpusService)
|
|||
|
{
|
|||
|
disposableOpusService.Dispose();
|
|||
|
}
|
|||
|
|
|||
|
// GC回收
|
|||
|
GC.SuppressFinalize(this);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|