You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 02:24:53