.NET WebSocket集成Copilot技能首次请求超时问题求助
问题描述
.NET代码实现的WebSocket功能在本地模拟器测试正常,但部署到Azure或通过ngrok作为Copilot技能运行时,首次触发消息虽能成功初始化WebSocket连接(实际消息收发正常),但会出现响应超时错误。代码逻辑全部放在DefaultActivityHandler的OnMessageActivity方法中。
核心原因
Bot Framework(包括Copilot技能)的消息处理是请求-响应模式,对单个请求的响应时长有严格限制(通常为15秒)。当前代码中StartWebSocketClientAsync方法里的while (client.State == WebSocketState.Open)循环会持续阻塞请求线程,直到WebSocket连接关闭,远超响应超时阈值,导致平台判定请求超时。
另外,ReceiveMessagesAsync中调用turnContext.SendActivityAsync(completeMessage)时未使用await,会导致异步操作未被正确等待,可能引发上下文生命周期相关的异常。
解决方案
1. 立即返回初始化响应,避免阻塞请求线程
重构StartWebSocketClientAsync,移除阻塞线程的while循环,在WebSocket连接成功后立即向用户返回初始化成功的消息,释放请求线程。
2. 后台管理WebSocket连接,与请求上下文解耦
使用单例或后台服务管理WebSocket连接,避免直接持有TurnContext(TurnContext是请求级别的上下文,请求结束后会被回收)。改用ConversationReference存储会话信息,后续通过BotAdapter发送消息。
3. 修正异步操作的await问题
确保所有异步方法调用都使用await,避免隐式的异步操作丢失。
修改后的代码示例
重构WebSocket管理逻辑
// 单例类管理WebSocket连接和会话引用 public class WebSocketConnectionManager { private readonly Dictionary<string, (ClientWebSocket Client, ConversationReference Reference)> _connections = new(); private readonly IBotFrameworkHttpAdapter _adapter; public WebSocketConnectionManager(IBotFrameworkHttpAdapter adapter) { _adapter = adapter; } public async Task AddConnectionAsync(string threadId, ClientWebSocket client, ConversationReference reference) { _connections[threadId] = (client, reference); // 启动后台接收消息任务 _ = ReceiveMessagesAsync(client, reference); } private async Task ReceiveMessagesAsync(ClientWebSocket client, ConversationReference reference) { var buffer = new byte[1048576]; var messageBuilder = new StringBuilder(); try { while (client.State == WebSocketState.Open) { WebSocketReceiveResult result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); if (result.MessageType == WebSocketMessageType.Close) { try { await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); } catch (WebSocketException wex) { Console.WriteLine($"WebSocket Close Error: {wex.Message}"); } break; } else { messageBuilder.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); if (result.EndOfMessage) { string completeMessage = messageBuilder.ToString(); if (!string.IsNullOrEmpty(completeMessage)) { // 使用ConversationReference发送消息 await _adapter.ContinueConversationAsync(reference, async (context, token) => { await context.SendActivityAsync(completeMessage); }, CancellationToken.None); } messageBuilder.Clear(); } } } } catch (Exception ex) { await _adapter.ContinueConversationAsync(reference, async (context, token) => { await context.SendActivityAsync($"WebSocket接收错误: {ex.Message}"); }, CancellationToken.None); } finally { // 移除连接 var threadId = _connections.FirstOrDefault(kv => kv.Value.Client == client).Key; if (!string.IsNullOrEmpty(threadId)) { _connections.Remove(threadId); } client.Dispose(); } } public (ClientWebSocket Client, ConversationReference Reference)? GetConnection(string threadId) { _connections.TryGetValue(threadId, out var connection); return connection; } }
修改OnMessageActivity中的处理逻辑
private readonly WebSocketConnectionManager _connectionManager; // 通过依赖注入获取WebSocketConnectionManager public YourBotHandler(WebSocketConnectionManager connectionManager) { _connectionManager = connectionManager; } protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken) { // 假设首次消息是初始化WebSocket的指令 if (turnContext.Activity.Text.Equals("init ws", StringComparison.OrdinalIgnoreCase)) { var client = new ClientWebSocket(); string uri = "wss://echo.websocket.org/"; await client.ConnectAsync(new Uri(uri), cancellationToken); // 保存会话引用 var conversationReference = turnContext.Activity.GetConversationReference(); // 添加到连接管理器 await _connectionManager.AddConnectionAsync(turnContext.Activity.Conversation.Id, client, conversationReference); // 立即返回初始化成功响应,释放请求线程 await turnContext.SendActivityAsync("WebSocket连接已初始化,可开始发送消息", cancellationToken: cancellationToken); return; } // 后续消息发送到WebSocket var connection = _connectionManager.GetConnection(turnContext.Activity.Conversation.Id); if (connection != null && connection.Value.Client.State == WebSocketState.Open) { byte[] messageBytes = Encoding.UTF8.GetBytes(turnContext.Activity.Text); await connection.Value.Client.SendAsync(new ArraySegment<byte>(messageBytes), WebSocketMessageType.Text, true, cancellationToken); await turnContext.SendActivityAsync("消息已发送到WebSocket服务器", cancellationToken: cancellationToken); } else { await turnContext.SendActivityAsync("WebSocket连接未初始化,请先发送'init ws'指令", cancellationToken: cancellationToken); } }
注册依赖服务
在Program.cs中添加单例注册:
builder.Services.AddSingleton<WebSocketConnectionManager>();
关键注意事项
- 请求响应模式限制:Bot Framework的每个消息请求必须在超时时间内返回响应,不能在请求线程中执行长时间阻塞操作。
- TurnContext生命周期:
TurnContext仅在当前请求周期内有效,不能在后台任务中持有,必须使用ConversationReference进行后续消息发送。 - 异步操作规范:所有异步方法调用必须使用
await,避免引发异步操作未完成导致的资源泄漏或异常。
内容的提问来源于stack exchange,提问作者Shanmukha Reddy

