直接给出针对 webui/index.html 和 src/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.tsgrep -n "EventSource\|text/event-stream\|chat/stream\|stream" src/web.ts
修改模式(在创建 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是注释,客户端EventSource的onmessage不会触发,但浏览器层面保持连接活跃)。
event: shutdown 在服务关闭前process.on('SIGTERM', () => {
// 遍历所有活跃的 SSE 客户端 res,发送关闭事件
// 具体实现需要维护一个客户端列表,这里先留作后续优化
process.exit(0);
});
(此次可暂不实现,仅添加心跳更紧迫)
webui/index.htmlEventSource 实例化位置grep -n "new EventSource\|EventSource(" webui/index.html
/**
* 创建带自动重连和心跳监听的 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();
}
};
}
EventSource 调用找到原有代码段:
// 原:const eventSource = new EventSource(...);
// 改为:
const sseConnection = createRobustEventSource(url, {
onMessage: (event) => {
// 原来 eventSource.onmessage 的逻辑搬到这里
const data = JSON.parse(event.data);
// ... 处理流式数据
},
onError: (err) => {
// 可记录错误
},
onConnect: () => {
// 可清除"重连中"提示
}
});
// 当需要断开时(如切换会话、登出)调用 sseConnection.close()
<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 控制显示。
当前若 token 在 URL 查询参数中,保持即可。注意:生产环境务必使用 HTTPS,避免 token 暴露。
cd /home/administrator/CmdCode-V0.5
bun run src/cli.ts
# 或单独启动 web 服务: bun run src/web.ts(如支持)cp webui/index.html.bak.$(date +%Y%m%d) webui/index.html
cp src/web.ts.bak.$(date +%Y%m%d) src/web.ts
预期收益:短时网络闪断、后端重启、反向代理超时都将被前端无感重连覆盖,用户体验大幅提升,且改动量极小。
请按指令顺序执行,有任何报错或适配性问题随时反馈。