👤 用户
需要
🤖 DeepSeek 助手

🔧 SSE 连接加固方案

直接给出针对 webui/index.htmlsrc/web.ts 的精确修改指令,完成后前端具备自动重连、心跳保活与状态提示,后端定时发送心跳。


一、准备工作(备份)

cd /home/administrator/CmdCode-V0.5
cp webui/index.html webui/index.html.bak.$(date +%Y%m%d)
cp src/web.ts src/web.ts.bak.$(date +%Y%m%d)

二、后端修改:src/web.ts

1. 找到 SSE 端点

grep -n "EventSource\|text/event-stream\|chat/stream\|stream" src/web.ts

2. 在 SSE 端点的响应头设置后,立即启动心跳定时器

修改模式(在创建 SSE 流的函数内部):

// 原代码大致为:
res.writeHead(200, {
  'Content-Type': 'text/event-stream',
  'Cache-Control': 'no-cache',
  'Connection': 'keep-alive',
  'Access-Control-Allow-Origin': '*',  // 根据实际情况调整
});

// ... 之后可能有一个 sendEvent 函数

// 👇 在现有 SSE 逻辑的起始处添加心跳定时器
const heartbeatInterval = setInterval(() => {
  // 发送 SSE 注释,对前端可见但无数据开销,可维持连接
  res.write(':ping\n\n');
}, 30000); // 30秒一次,可根据需求调整

// 👇 当聊天流结束或连接关闭时,清除定时器
req.on('close', () => {
  clearInterval(heartbeatInterval);
  // 可能还有清理会话等逻辑
});
注意:心跳必须是有效的 SSE 格式(:ping\n\n 是注释,客户端 EventSourceonmessage 不会触发,但浏览器层面保持连接活跃)。

3. 可选:发送 event: shutdown 在服务关闭前

process.on('SIGTERM', () => {
  // 遍历所有活跃的 SSE 客户端 res,发送关闭事件
  // 具体实现需要维护一个客户端列表,这里先留作后续优化
  process.exit(0);
});

(此次可暂不实现,仅添加心跳更紧迫)


三、前端修改:webui/index.html

1. 找到 EventSource 实例化位置

grep -n "new EventSource\|EventSource(" webui/index.html

2. 封装带重连的 SSE 连接管理器

/**
 * 创建带自动重连和心跳监听的 SSE 连接
 * @param {string} url - SSE 端点 URL
 * @param {object} options - { onMessage, onError, onConnect, onClose }
 * @returns {object} - { close: Function }
 */
function createRobustEventSource(url, options) {
  const { onMessage, onError, onConnect, onClose } = options;
  let es = null;
  let reconnectTimer = null;
  let reconnectDelay = 1000; // 初始 1s
  const maxReconnectDelay = 30000; // 最大 30s
  let intentionalClose = false;

  // 更新 UI 连接状态指示器(如果存在)
  function setStatus(status) {
    const el = document.getElementById('connection-status');
    if (el) el.textContent = status;
  }

  function connect() {
    if (intentionalClose) return;

    setStatus('connecting');
    es = new EventSource(url);

    es.onopen = () => {
      setStatus('connected');
      reconnectDelay = 1000; // 重置为初始值
      if (onConnect) onConnect();
    };

    es.onmessage = (event) => {
      // 忽略心跳的空数据(心跳是注释,不会触发onmessage,但预防性检查)
      if (!event.data || event.data.trim() === '') return;
      onMessage(event);
    };

    // 可选:监听自定义事件(如 heartbeat)
    es.addEventListener('heartbeat', () => {
      // 可记录最后心跳时间,若超时主动重连
    });

    es.onerror = (error) => {
      setStatus('disconnected');
      es.close();
      // 如果不是主动关闭,则尝试重连
      if (!intentionalClose) {
        console.log(`SSE 连接断开,${Math.round(reconnectDelay/1000)}s 后重连...`);
        clearTimeout(reconnectTimer);
        reconnectTimer = setTimeout(() => {
          connect();
          reconnectDelay = Math.min(reconnectDelay * 2, maxReconnectDelay); // 指数退避
        }, reconnectDelay);
      }
      if (onError) onError(error);
    };
  }

  // 启动连接
  connect();

  // 返回关闭方法,允许外部主动断开(且不重连)
  return {
    close: () => {
      intentionalClose = true;
      clearTimeout(reconnectTimer);
      if (es) es.close();
      setStatus('closed');
      if (onClose) onClose();
    }
  };
}

3. 替换原有的 EventSource 调用

找到原有代码段:

// 原:const eventSource = new EventSource(...);
// 改为:
const sseConnection = createRobustEventSource(url, {
  onMessage: (event) => {
    // 原来 eventSource.onmessage 的逻辑搬到这里
    const data = JSON.parse(event.data);
    // ... 处理流式数据
  },
  onError: (err) => {
    // 可记录错误
  },
  onConnect: () => {
    // 可清除"重连中"提示
  }
});

// 当需要断开时(如切换会话、登出)调用 sseConnection.close()

4. 添加简单的连接状态提示(可选)

<div id="connection-status" style="position:fixed; top:0; left:50%; transform:translateX(-50%); background:#333; color:#fff; padding:2px 12px; border-radius:0 0 6px 6px; font-size:12px; opacity:0.8;">
  连接中...
</div>

并将其初始样式设为隐藏,由 JS 控制显示。

5. 保持 token 传递方式不变

当前若 token 在 URL 查询参数中,保持即可。注意:生产环境务必使用 HTTPS,避免 token 暴露。


四、验证修改

  1. 重启后端:
    cd /home/administrator/CmdCode-V0.5
    bun run src/cli.ts
    # 或单独启动 web 服务: bun run src/web.ts(如支持)
  2. 打开前端页面,开始一次聊天,观察:
    • 控制台无大量重连报错
    • 连接状态正常显示"已连接"
    • 手动停止后端服务,前端应显示"断开连接"并开始重连
    • 重启后端,前端应自动恢复连接并继续接收消息
  3. 检查后端日志:每 30 秒应有心跳写入(可能被忽略,不影响功能)

五、回滚方案

cp webui/index.html.bak.$(date +%Y%m%d) webui/index.html
cp src/web.ts.bak.$(date +%Y%m%d) src/web.ts

预期收益:短时网络闪断、后端重启、反向代理超时都将被前端无感重连覆盖,用户体验大幅提升,且改动量极小。

请按指令顺序执行,有任何报错或适配性问题随时反馈。