-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
825 lines (710 loc) · 29.8 KB
/
Program.cs
File metadata and controls
825 lines (710 loc) · 29.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
using Microsoft.CognitiveServices.Speech;
using Microsoft.CognitiveServices.Speech.Audio;
using Microsoft.Extensions.Configuration;
using NAudio.Wave;
using Serilog;
using System.Collections.Concurrent;
using VoiceAssistantConsoleApp.Services;
using VoiceAssistantConsoleApp.Utils;
namespace VoiceAssistantConsoleApp
{
public class Program
{
// ============== 配置区域 ==============
private static string KeywordModelPath = "keyword_xiaoyi.table";
private static string DifySpeechToTextEndpoint = string.Empty;
private static string DifyChatEndpoint = string.Empty;
private static string DifyToken = string.Empty;
#region 录音参数
/// <summary>
/// 采样率(Dify 推荐 16kHz)
/// </summary>
private static int SampleRate = 16000;
/// <summary>
/// 位深度 16bit
/// </summary>
private static int BitsPerSample = 16;
/// <summary>
/// 单声道
/// </summary>
private static int Channels = 1;
/// <summary>
/// 最大录音时长
/// </summary>
private static int MaxRecordingSeconds = 30;
/// <summary>
/// 静音检测阈值(毫秒)[安静环境:1000, 嘈杂环境:2000]
/// </summary>
private static int SilenceThresholdMs = 1500; // 静音持续时间(可调整 1000-2000)
/// <summary>
/// // 静音振幅阈值 [安静环境:0.01f, 嘈杂环境:0.05f]
/// </summary>
private static float SilenceAmplitude = 0.02f; // 音量阈值(环境噪音大可调高到 0.05)
/// <summary>
/// MP3 编码参数
/// </summary>
private static int Mp3BitRate = 64;
/// <summary>
/// 预录音缓冲(保存唤醒词前后的音频),保留唤醒前 500ms 的音频
/// </summary>
private static int PreRecordBufferMs = 500;
private static int BufferMilliseconds = 100;
#endregion
private static string WebSocketServerUrl = string.Empty;
private static int WakeWordTimeoutMinutes = 5;
// =====================================
private static WebSocketManager? _webSocketManager;
private static DifyService? _difyService;
private static AudioService? _audioService;
private static CancellationTokenSource? _cancellationTokenSource;
private static bool _isShuttingDown = false;
private static volatile bool isAwake = false;
private static volatile bool isRecording = false;
private static readonly List<Task> backgroundTasks = new List<Task>();
private static readonly object taskLock = new object();
public static async Task Main(string[] args)
{
// 初始化日志
InitializeLogging();
Console.WriteLine("=== Dify 语音助手启动(本地唤醒 + 立即录音)===");
// 在最开始就初始化
_cancellationTokenSource = new CancellationTokenSource();
// 初始化配置
InitializeConfiguration();
Console.WriteLine($"唤醒词模型: {KeywordModelPath}");
Console.WriteLine($"录音格式: MP3 ({Mp3BitRate} kbps)");
Console.WriteLine($"预录音缓冲: {PreRecordBufferMs}ms");
Console.WriteLine("等待唤醒...\n");
if (!File.Exists(KeywordModelPath))
{
Console.WriteLine($"错误: 找不到唤醒词模型文件 '{KeywordModelPath}'");
Console.WriteLine($"当前目录: {Directory.GetCurrentDirectory()}");
return;
}
// 注册 Ctrl+C 和程序退出事件
Console.CancelKeyPress += OnCancelKeyPress;
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
// 初始化服务
_difyService = new DifyService(DifyToken, DifySpeechToTextEndpoint, DifyChatEndpoint);
_audioService = new AudioService(SampleRate, BitsPerSample, Channels, Mp3BitRate);
_webSocketManager = new WebSocketManager(WebSocketServerUrl);
// 尝试连接(失败不影响启动)
try
{
await _webSocketManager.ConnectAsync();
}
catch (Exception ex)
{
Console.WriteLine($"WebSocket 初始连接失败: {ex.Message}");
Console.WriteLine("程序将继续运行,消息发送时会自动重连");
}
try
{
// 使用 CancellationToken 控制循环
while (!_cancellationTokenSource.Token.IsCancellationRequested)
{
try
{
await WaitForWakeWordAndRecordAsync(_cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
// 正常退出
Console.WriteLine("\n⏹️ 收到退出信号");
break;
}
catch (Exception ex)
{
Console.WriteLine($"错误: {ex.Message}");
Console.WriteLine($"堆栈: {ex.StackTrace}");
ResetState();
// 检查是否需要退出
if (_cancellationTokenSource.Token.IsCancellationRequested)
{
break;
}
// 短暂延迟后重试
try
{
await Task.Delay(1000, _cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
break;
}
}
}
}
catch (OperationCanceledException)
{
// 正常退出
Console.WriteLine("\n⏹️ 程序正在退出...");
}
finally
{
await CleanupResourcesAsync();
}
}
/// <summary>
/// 初始化日志
/// </summary>
private static void InitializeLogging()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.File(
"logs/voice-assistant-.txt",
rollingInterval: RollingInterval.Day,
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
}
/// <summary>
/// 初始化配置文件
/// </summary>
private static void InitializeConfiguration()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
var configuration = builder.Build();
// 录音配置
SampleRate = int.Parse(configuration["Audio:SampleRate"] ?? "16000");
BitsPerSample = int.Parse(configuration["Audio:BitsPerSample"] ?? "16");
Channels = int.Parse(configuration["Audio:Channels"] ?? "1");
MaxRecordingSeconds = int.Parse(configuration["Audio:MaxRecordingSeconds"] ?? "30");
SilenceThresholdMs = int.Parse(configuration["Audio:SilenceThresholdMs"] ?? "1500");
SilenceAmplitude = float.Parse(configuration["Audio:SilenceAmplitude"] ?? "0.02");
Mp3BitRate = int.Parse(configuration["Audio:Mp3BitRate"] ?? "64");
PreRecordBufferMs = int.Parse(configuration["Audio:PreRecordBufferMs"] ?? "500");
BufferMilliseconds = int.Parse(configuration["Audio:BufferMilliseconds"] ?? "100");
// Dify 配置
DifyToken = configuration["Dify:ApiKey"] ?? throw new InvalidOperationException("Dify ApiKey 未配置");
DifySpeechToTextEndpoint = configuration["Dify:SpeechToTextEndpoint"] ?? throw new InvalidOperationException("Dify SpeechToTextEndpoint 未配置");
DifyChatEndpoint = configuration["Dify:ChatEndpoint"] ?? throw new InvalidOperationException("Dify ChatEndpoint 未配置");
// WebSocket 配置
WebSocketServerUrl = configuration["WebSocket:ServerUrl"] ?? "ws://localhost:8080";
// 唤醒词配置
KeywordModelPath = configuration["KeywordModel:Path"] ?? "keyword_xiaoyi.table";
WakeWordTimeoutMinutes = int.Parse(configuration["KeywordModel:WakeWordTimeoutMinutes"] ?? "5");
Log.Information("配置加载完成");
Log.Information("📍 Dify Endpoint: {Endpoint}", DifySpeechToTextEndpoint);
Log.Information("📍 WebSocket Server: {ServerUrl}", WebSocketServerUrl);
Log.Information("📍 Keyword Model: {ModelPath}", KeywordModelPath);
}
/// <summary>
/// Ctrl+C 事件处理
/// </summary>
private static void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e)
{
Console.WriteLine("\n\n检测到 Ctrl+C,正在优雅退出...");
// 添加空检查
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
{
_cancellationTokenSource.Cancel();
}
// 取消默认行为(立即终止)
e.Cancel = true;
}
/// <summary>
/// 进程退出事件处理
/// </summary>
private static void OnProcessExit(object sender, EventArgs e)
{
Console.WriteLine("\n程序正在退出...");
// 添加空检查
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
{
_cancellationTokenSource.Cancel();
}
_isShuttingDown = true;
// 给一点时间让资源清理
Thread.Sleep(100);
}
/// <summary>
/// 等待唤醒词并立即开始录音(一体化方法)
/// </summary>
private static async Task WaitForWakeWordAndRecordAsync(CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
Log.Information("[状态] 等待唤醒词...");
string? wavFilePath = null;
string? mp3FilePath = null;
WaveInEvent? waveIn = null;
WaveFileWriter? writer = null;
using var localCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
try
{
// 1. 初始化音频配置和唤醒词识别器
var audioConfig = AudioConfig.FromDefaultMicrophoneInput();
var keywordRecognizer = new KeywordRecognizer(audioConfig);
// 2. 初始化录音设备
waveIn = new WaveInEvent
{
WaveFormat = new WaveFormat(SampleRate, BitsPerSample, Channels),
BufferMilliseconds = BufferMilliseconds
};
var keywordModel = KeywordRecognitionModel.FromFile(KeywordModelPath);
var preRecordBuffer = new CircularBuffer(
(int)(SampleRate * (BitsPerSample / 8) * Channels * (PreRecordBufferMs / 1000.0))
);
bool hasSound = false;
int silentBufferCount = 0;
int silentBufferThreshold = (int)(SilenceThresholdMs / (double)BufferMilliseconds);
bool isWakeWordDetected = false;
bool isRecordingStopped = false;
object lockObject = new object();
// 3. 数据处理事件
waveIn.DataAvailable += (sender, e) =>
{
if (localCts.Token.IsCancellationRequested)
return;
if (!isWakeWordDetected)
{
// 唤醒前:只保存到循环缓冲区
preRecordBuffer.Write(e.Buffer, 0, e.BytesRecorded);
}
else
{
// 唤醒后:写入文件并进行静音检测
try
{
writer?.Write(e.Buffer, 0, e.BytesRecorded);
// 计算音量
float rms = AudioService.CalculateRMS(e.Buffer, e.BytesRecorded);
if (rms > SilenceAmplitude)
{
hasSound = true;
silentBufferCount = 0;
Console.Write("█");
}
else
{
silentBufferCount++;
Console.Write("░");
}
if (hasSound && silentBufferCount >= silentBufferThreshold)
{
Console.WriteLine("\n检测到静音,停止录音");
waveIn.StopRecording();
}
}
catch (Exception ex)
{
Log.Error(ex, "数据处理错误");
}
}
};
waveIn.RecordingStopped += (sender, e) =>
{
lock (lockObject)
{
isRecordingStopped = true;
Monitor.Pulse(lockObject);
}
};
// 4. 开始持续录音
waveIn.StartRecording();
// 5. 等待唤醒词(带超时)
Task<KeywordRecognitionResult>? wakeTask = null;
KeywordRecognitionResult? result = null;
try
{
wakeTask = keywordRecognizer.RecognizeOnceAsync(keywordModel);
var timeoutTask = Task.Delay(TimeSpan.FromMinutes(WakeWordTimeoutMinutes), localCts.Token);
var completedTask = await Task.WhenAny(wakeTask, timeoutTask);
if (completedTask == timeoutTask)
{
if (localCts.Token.IsCancellationRequested)
{
Log.Information("唤醒检测已取消");
}
else
{
Log.Warning("唤醒检测超时");
}
// 超时时需要停止识别器
try
{
// 尝试停止识别(如果支持)
await keywordRecognizer.StopRecognitionAsync();
}
catch (Exception ex)
{
Log.Debug(ex, "停止识别器时出错(可忽略)");
}
// 等待识别任务完成(最多等待3秒)
try
{
using var waitCts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
result = await wakeTask.WaitAsync(waitCts.Token);
}
catch (OperationCanceledException)
{
Log.Warning("等待识别任务完成超时");
}
catch (Exception ex)
{
Log.Warning(ex, "等待识别任务完成时出错");
}
waveIn.StopRecording();
return;
}
result = await wakeTask;
}
catch (OperationCanceledException)
{
Log.Information("唤醒检测已取消");
// 取消时也要停止识别器
if (wakeTask != null && !wakeTask.IsCompleted)
{
try
{
await keywordRecognizer.StopRecognitionAsync();
using var waitCts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
await wakeTask.WaitAsync(waitCts.Token);
}
catch { }
}
waveIn.StopRecording();
throw;
}
// 6. 检查识别结果
if (result == null || result.Reason != ResultReason.RecognizedKeyword)
{
Log.Warning("未检测到唤醒词");
waveIn.StopRecording();
return;
}
if (localCts.Token.IsCancellationRequested)
{
Log.Information("检测到退出信号");
waveIn.StopRecording();
return;
}
// 7. 检测到唤醒词
Log.Information("已唤醒! 检测到关键词");
Console.WriteLine("正在录音...(静音自动停止)");
try
{
Console.Beep(800, 500);
}
catch
{
// 某些环境不支持 Beep
Console.WriteLine("当前环境不支持 Beep");
}
isWakeWordDetected = true;
isAwake = true;
// 8. 创建 WAV 文件并写入预录音缓冲区
wavFilePath = Path.Combine(Path.GetTempPath(), $"voice_{Guid.NewGuid()}.wav");
writer = new WaveFileWriter(wavFilePath, waveIn.WaveFormat);
var preRecordData = preRecordBuffer.ToArray();
if (preRecordData.Length > 0)
{
writer.Write(preRecordData, 0, preRecordData.Length);
Log.Information("已保存唤醒前 {PreRecordBufferMs}ms 音频", PreRecordBufferMs);
}
// 9. 等待录音完成(支持取消)
var startTime = DateTime.Now;
lock (lockObject)
{
while (!isRecordingStopped &&
(DateTime.Now - startTime).TotalSeconds < MaxRecordingSeconds)
{
Monitor.Wait(lockObject, 100);
if (localCts.Token.IsCancellationRequested)
{
Log.Information("录音被中断");
waveIn.StopRecording();
break;
}
}
}
if (!isRecordingStopped)
{
if (localCts.Token.IsCancellationRequested)
{
Log.Information("录音已取消");
}
else
{
Log.Warning("录音超时,自动停止");
}
waveIn.StopRecording();
lock (lockObject)
{
if (!isRecordingStopped)
{
Monitor.Wait(lockObject, 1000);
}
}
}
if (localCts.Token.IsCancellationRequested)
{
Log.Information("跳过后续处理");
return;
}
await Task.Delay(200, cancellationToken);
// 10. 释放资源
Log.Information("正在关闭录音设备...");
if (writer != null)
{
try
{
writer.Flush();
writer.Dispose();
writer = null;
Log.Information("WAV 文件已关闭");
}
catch (Exception ex)
{
Log.Warning(ex, "关闭 WAV 文件错误");
}
}
if (waveIn != null)
{
try
{
waveIn.Dispose();
waveIn = null;
Log.Information("录音设备已释放");
}
catch (Exception ex)
{
Log.Warning(ex, "释放录音设备错误");
}
}
await Task.Delay(200, cancellationToken);
// 11. 检查录音结果
if (!hasSound)
{
Log.Warning("未检测到有效语音");
return;
}
if (!File.Exists(wavFilePath))
{
Log.Error("WAV 文件不存在");
return;
}
var wavFileInfo = new FileInfo(wavFilePath);
if (wavFileInfo.Length < 1024)
{
Log.Warning("录音文件太小");
return;
}
Log.Information("WAV 录音完成: {Size} KB", wavFileInfo.Length / 1024.0);
if (localCts.Token.IsCancellationRequested)
{
Log.Information("跳过文件转换");
return;
}
// 12. 转换为 MP3
Log.Information("正在转换为 MP3...");
mp3FilePath = await _audioService!.ConvertWavToMp3Async(wavFilePath);
if (string.IsNullOrEmpty(mp3FilePath) || !File.Exists(mp3FilePath))
{
Log.Error("MP3 转换失败");
return;
}
var mp3FileInfo = new FileInfo(mp3FilePath);
Log.Information("MP3 转换完成: {Size} KB", mp3FileInfo.Length / 1024.0);
Log.Information("压缩率: {Rate}%", (1 - (double)mp3FileInfo.Length / wavFileInfo.Length) * 100);
// 13. 调用 Dify API
Log.Information("正在调用 Dify 语音转文字接口...");
string? recognizedText = await _difyService!.SpeechToTextAsync(mp3FilePath, cancellationToken);
// 14. 异步清理临时文件
var cleanupTask = Task.Run(() =>
{
AudioService.DeleteTempFile(wavFilePath, "WAV");
AudioService.DeleteTempFile(mp3FilePath, "MP3");
}, CancellationToken.None);
lock (taskLock)
{
backgroundTasks.Add(cleanupTask);
}
wavFilePath = null;
mp3FilePath = null;
// 15. 处理识别结果
if (!string.IsNullOrWhiteSpace(recognizedText))
{
Log.Information("识别结果: {Text}", recognizedText);
// 立即重置状态
ResetState();
Log.Information("状态已重置,可以接受下一次唤醒");
// 后台发送消息
var dialogTask = Task.Run(async () =>
{
try
{
if (_isShuttingDown || cancellationToken.IsCancellationRequested)
{
Log.Warning("程序正在退出,跳过消息发送");
return;
}
bool success = await _webSocketManager!.SendVoiceToTextAsync(recognizedText, cancellationToken);
if (success)
{
Log.Information($"消息已发送,内容如下: {recognizedText}");
}
else
{
Log.Error("语音转文字消息发送失败");
}
}
catch (OperationCanceledException)
{
Log.Warning("消息发送已取消");
}
catch (Exception ex)
{
Log.Error(ex, "消息发送错误");
}
}, cancellationToken);
lock (taskLock)
{
backgroundTasks.Add(dialogTask);
backgroundTasks.RemoveAll(t => t.IsCompleted);
Log.Debug("当前后台任务数: {Count}", backgroundTasks.Count);
}
return;
}
else
{
Log.Warning("未识别到有效文本");
}
}
catch (OperationCanceledException)
{
Log.Information("操作已取消");
throw;
}
catch (Exception ex)
{
Log.Error(ex, "处理错误");
}
finally
{
Log.Debug("正在清理方法内资源...");
try { writer?.Dispose(); } catch { }
try { waveIn?.Dispose(); } catch { }
if (!string.IsNullOrEmpty(wavFilePath) || !string.IsNullOrEmpty(mp3FilePath))
{
_ = Task.Run(() =>
{
AudioService.DeleteTempFile(wavFilePath, "WAV");
AudioService.DeleteTempFile(mp3FilePath, "MP3");
});
}
if (isAwake || isRecording)
{
ResetState();
}
Log.Debug("方法内资源清理完成");
}
}
/// <summary>
/// 重置状态
/// </summary>
private static void ResetState()
{
isAwake = false;
isRecording = false;
Console.WriteLine("\n\n等待下一次唤醒...\n");
}
/// <summary>
/// 清理所有资源
/// </summary>
private static async Task CleanupResourcesAsync()
{
_isShuttingDown = true;
Log.Information("正在清理资源...");
// 1. 断开 WebSocket
try
{
Log.Information("正在断开 WebSocket 连接...");
if (_webSocketManager != null)
{
await _webSocketManager.DisconnectAsync();
_webSocketManager.Dispose();
}
Log.Information("WebSocket 已断开");
}
catch (Exception ex)
{
Log.Warning(ex, "断开 WebSocket 错误");
}
// 2. 等待所有后台任务
try
{
await WaitForAllBackgroundTasksAsync();
}
catch (Exception ex)
{
Log.Warning(ex, "等待后台任务错误");
}
// 3. 释放服务
try
{
_difyService?.Dispose();
_audioService?.Dispose();
Log.Information("服务已释放");
}
catch (Exception ex)
{
Log.Warning(ex, "释放服务错误");
}
// 4. 释放 CancellationTokenSource
try
{
_cancellationTokenSource?.Dispose();
}
catch { }
Log.Information("资源清理完成");
Log.Information("程序已退出");
// 关闭日志
Log.CloseAndFlush();
}
/// <summary>
/// 等待所有后台任务完成
/// </summary>
private static async Task WaitForAllBackgroundTasksAsync()
{
Task[] tasks;
lock (taskLock)
{
tasks = backgroundTasks.Where(t => !t.IsCompleted).ToArray();
}
if (tasks.Length > 0)
{
Log.Information("等待 {Count} 个后台任务完成...", tasks.Length);
try
{
var timeoutTask = Task.Delay(5000);
var allTasksTask = Task.WhenAll(tasks);
var completedTask = await Task.WhenAny(allTasksTask, timeoutTask);
if (completedTask == timeoutTask)
{
Log.Warning("等待后台任务超时(5秒),强制继续");
}
else
{
Log.Information("所有后台任务已完成");
}
}
catch (Exception ex)
{
Log.Warning(ex, "等待后台任务错误");
}
}
else
{
Log.Information("没有待完成的后台任务");
}
}
}
}