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

Flutter应用连接OpenAI时WebSocket连接关闭,求解决方案

问题根源分析

你的代码存在几个核心问题导致WebSocket连接被关闭:

  1. 错误的WebSocket端点:wss://chat.openai.com/api/chat是ChatGPT网页端的内部私有接口,不对外开放,且需要复杂的会话认证,直接连接会被服务器拒绝并关闭连接。
  2. 缺少必要的认证信息:OpenAI的所有API都需要身份验证(API密钥或会话令牌),你的代码未携带任何认证信息,服务器会直接断开连接。
  3. 消息格式不符合要求:即使连接成功,你发送的纯文本消息不符合OpenAI API的格式规范,服务器无法处理并会关闭连接。
  4. 状态管理与错误处理不完善:errorMessage未初始化会导致运行时错误;isConnected在连接发起后立即标记为已连接,未等待实际连接成功;没有连接断开后的重连机制。

修复方案

1. 替换为OpenAI官方流式HTTP接口(推荐)

OpenAI官方提供的Chat Completions流式HTTP接口更稳定,无需WebSocket。首先添加依赖:

dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0

2. 修改核心代码

import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My Chat App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const MyHomePage(title: 'My Chat App'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({required this.title, super.key});

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final TextEditingController messageController = TextEditingController();
  List<ChatMessage> messages = [];
  bool isLoading = false;
  String? errorMessage;
  // 替换为你的OpenAI API密钥
  final String openAiApiKey = 'YOUR_OPENAI_API_KEY';

  Future<void> sendMessage() async {
    if (messageController.text.isEmpty || isLoading) return;

    final userMessage = ChatMessage(sender: 'You', text: messageController.text);
    setState(() {
      messages.add(userMessage);
      isLoading = true;
      errorMessage = null;
      messageController.clear();
    });

    try {
      final url = Uri.parse('https://api.openai.com/v1/chat/completions');
      final request = http.Request('POST', url)
        ..headers.addAll({
          'Authorization': 'Bearer $openAiApiKey',
          'Content-Type': 'application/json',
        })
        ..body = jsonEncode({
          'model': 'gpt-3.5-turbo',
          'messages': messages.map((msg) => {
                'role': msg.sender == 'You' ? 'user' : 'assistant',
                'content': msg.text,
              }).toList(),
          'stream': true,
        });

      final response = await request.send();
      if (response.statusCode != HttpStatus.ok) {
        final errorBody = await response.stream.bytesToString();
        throw Exception('请求失败: ${response.statusCode}, $errorBody');
      }

      String assistantReply = '';
      ChatMessage? assistantMessage;

      await for (final chunk in response.stream.transform(utf8.decoder)) {
        for (final line in chunk.split('\n')) {
          if (line.startsWith('data: ')) {
            final data = line.substring(6);
            if (data == '[DONE]') break;
            try {
              final jsonData = jsonDecode(data);
              final content = jsonData['choices'][0]['delta']['content'] as String?;
              if (content != null) {
                assistantReply += content;
                setState(() {
                  assistantMessage = ChatMessage(sender: 'AI', text: assistantReply);
                  if (messages.last.sender == 'AI') {
                    messages.removeLast();
                  }
                  messages.add(assistantMessage!);
                });
              }
            } catch (e) {
              continue;
            }
          }
        }
      }
    } catch (e) {
      setState(() {
        errorMessage = '请求错误: $e';
      });
    } finally {
      setState(() {
        isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Expanded(
              child: ListView.builder(
                itemCount: messages.length,
                itemBuilder: (context, index) {
                  final message = messages[index];
                  return Padding(
                    padding: const EdgeInsets.symmetric(vertical: 8.0),
                    child: Column(
                      crossAxisAlignment: message.sender == 'You'
                          ? CrossAxisAlignment.end
                          : CrossAxisAlignment.start,
                      children: [
                        Text(
                          message.sender,
                          style: const TextStyle(fontWeight: FontWeight.bold),
                        ),
                        const SizedBox(height: 4),
                        Container(
                          padding: const EdgeInsets.symmetric(
                              horizontal: 12, vertical: 8),
                          decoration: BoxDecoration(
                            color: message.sender == 'You'
                                ? Colors.blue[100]
                                : Colors.grey[100],
                            borderRadius: BorderRadius.circular(12),
                          ),
                          child: Text(message.text),
                        ),
                      ],
                    ),
                  );
                },
              ),
            ),
            if (errorMessage != null)
              Padding(
                padding: const EdgeInsets.symmetric(vertical: 8.0),
                child: Text(errorMessage!, style: const TextStyle(color: Colors.red)),
              ),
            Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: messageController,
                    enabled: !isLoading,
                    decoration: const InputDecoration(
                      hintText: '输入你的问题...',
                      border: OutlineInputBorder(),
                    ),
                    onSubmitted: (_) => sendMessage(),
                  ),
                ),
                const SizedBox(width: 8),
                ElevatedButton(
                  onPressed: isLoading ? null : sendMessage,
                  child: isLoading
                      ? const SizedBox(
                          width: 20,
                          height: 20,
                          child: CircularProgressIndicator(strokeWidth: 2),
                        )
                      : const Text('发送'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class ChatMessage {
  final String sender;
  final String text;

  ChatMessage({required this.sender, required this.text});
}

3. 若坚持使用WebSocket(OpenAI Real-Time API)

需先申请OpenAI Real-Time API访问权限,示例连接代码:

import 'package:web_socket_channel/web_socket_channel.dart' as ws;

void connectToWebSocket() async {
  try {
    final uri = Uri.parse('wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01');
    final channel = ws.WebSocketChannel.connect(uri, headers: {
      'Authorization': 'Bearer $openAiApiKey',
      'OpenAI-Beta': 'realtime=v1',
    });

    channel.stream.listen((data) {
      final jsonData = jsonDecode(data);
      setState(() {
        messages.add(ChatMessage(sender: 'AI', text: jsonData['message']['content'][0]['text']));
      });
    }, onError: (error) {
      setState(() {
        errorMessage = 'WebSocket错误: $error';
      });
    }, onDone: () {
      setState(() {
        errorMessage = 'WebSocket连接关闭';
        // 添加重连逻辑
        // Future.delayed(const Duration(seconds: 5), connectToWebSocket);
      });
    });
  } catch (error) {
    setState(() {
      errorMessage = '连接失败: $error';
    });
  }
}

4. 其他优化点

  • 将errorMessage初始化为null,避免空指针错误。
  • 添加加载状态,防止重复发送请求。
  • 优化消息UI,区分用户与AI消息的展示位置。
  • WebSocket场景下可添加自动重连逻辑。

注意事项

  • 不要硬编码API密钥,建议使用环境变量或安全存储方式。
  • 确保你的OpenAI API密钥有足够的权限和额度。
  • 遵守OpenAI的API使用规范和速率限制。

内容的提问来源于stack exchange,提问作者Android_devNL

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 08:01:03