最近在博客中集成了 AI 聊天功能,包含两种交互方式:右下角的小窗(widget)和点击”新窗口打开”后的全屏页面。然而,在测试时发现一个令人头疼的问题:小窗里的聊天记录无法同步到大窗。
问题现象
- 用户在右下角小窗与 AI 对话,发送了几条消息
- 点击”新窗口打开”按钮,期望在大窗中继续对话
- 大窗打开后,只显示默认的欢迎消息,之前的对话记录全部丢失
这显然是一个用户体验的灾难——用户以为可以”无缝切换”到大窗继续聊天,结果却像是开启了一个全新的对话。

问题分析
初步排查
首先检查了两个页面的存储机制:
小窗(Widget):
localStorage.setItem('chat_session_id', sessionID);
|
大窗(Fullscreen):
sessionStorage.setItem('chat_session_id', sessionID);
|
发现问题:存储位置不一致!
深入分析
即使统一了存储位置,问题依然存在。进一步分析发现多个问题叠加:
1. 跨窗口通信缺失
小窗打开大窗时,没有传递 session_id:
chatNewWindow.addEventListener('click', function() { var chatWindow = window.open('/chat/', 'chatWindow', ...); });
|
直接打开 /chat/,没有任何参数,大窗无法知道应该使用哪个会话。
2. Session ID 被覆盖
WebSocket 连接成功后,服务器会返回一个 session_id,代码无条件使用这个值:
ws.onmessage = function(event) { var data = JSON.parse(event.data); if (data.type === 'connected') { sessionID = data.session_id; } };
|
问题在于:服务器可能会为”新连接”分配一个新的 session_id,这会导致原来的会话丢失。
3. 历史消息加载方式不同
小窗通过 HTTP API 主动拉取历史:
fetch('https://api.example.com/chat/history/' + sessionID) .then(res => res.json()) .then(data => renderMessages(data.messages));
|
大窗却期望 WebSocket 推送历史消息:
ws.onmessage = function(event) { if (data.messages) { renderMessages(data.messages); } };
|
实际上 WebSocket 的 welcome 消息并没有包含历史消息,导致大窗始终无法加载历史记录。
4. 字段命名不一致
服务器返回的消息中,用户标识字段有时是 type: 1,有时是 is_user: true,代码没有正确处理这种兼容。
解决方案
第一步:URL 传递 Session ID
小窗打开大窗时,通过 URL 参数传递 session_id:
chatNewWindow.addEventListener('click', function() { var sessionId = localStorage.getItem('chat_session_id') || sessionID; var chatUrl = '/chat/'; if (sessionId) { chatUrl += '?session_id=' + encodeURIComponent(sessionId); } var chatWindow = window.open(chatUrl, 'chatWindow', ...); });
|
第二步:大窗读取 URL 参数
大窗页面加载时,从 URL 读取并保存 session_id:
function getUrlParam(name) { var urlParams = new URLSearchParams(window.location.search); return urlParams.get(name); }
var urlSessionId = getUrlParam('session_id'); if (urlSessionId) { sessionStorage.setItem('chat_session_id', urlSessionId); localStorage.setItem('chat_session_id', urlSessionId); sessionID = urlSessionId; }
|
第三步:防止 Session ID 被覆盖
WebSocket 连接时,如果已经有 session_id,则不再使用服务器返回的新 ID:
ws.onmessage = function(event) { var data = JSON.parse(event.data); if (data.type === 'connected') { if (!sessionID) { sessionID = data.session_id; localStorage.setItem('chat_session_id', sessionID); } else { console.log('[Chat] Keeping existing session_id:', sessionID); } } };
|
第四步:统一历史消息加载方式
大窗也使用 HTTP API 加载历史消息,与小窗保持一致:
function loadChatHistory(sid) { fetch('https://api.example.com/chat/history/' + encodeURIComponent(sid)) .then(response => response.json()) .then(data => { var welcomeMsg = chatBody.querySelector('.chat-message.system'); chatBody.innerHTML = ''; if (welcomeMsg) { chatBody.appendChild(welcomeMsg); } if (data.messages && data.messages.length > 0) { data.messages.forEach(function(m) { var isUser = (m.type === 1); appendMessage(m.content, isUser, m.created_at, m.image_url); }); } }); }
ws.onmessage = function(event) { var data = JSON.parse(event.data); if (data.type === 'connected') { if (sessionID) { loadChatHistory(sessionID); } } };
|
第五步:修复字段兼容问题
处理服务器返回的不同字段格式:
var isUser = (msg.type === 1) || msg.is_user;
|
经验总结
1. 跨窗口状态共享
当需要在多个窗口/标签页间共享状态时,不能仅依赖客户端存储(localStorage/sessionStorage)。URL 参数是最可靠的跨窗口通信方式。
2. 服务端 Session 管理
如果服务端会为每个 WebSocket 连接分配新的 session_id,客户端必须明确告知服务端”我要加入已有会话”,而不是被动接受新会话。
3. 数据加载策略
WebSocket 适合实时消息推送,但历史数据加载更适合用 HTTP API:
- HTTP 支持缓存、分页、断点续传
- 避免 WebSocket 消息过大导致阻塞
- 便于调试(可以直接在浏览器地址栏访问 API)
4. 防御式编程
sessionID = data.session_id;
if (!sessionID) { sessionID = data.session_id; }
|
5. 字段兼容性
前后端协议升级时,保持向后兼容:
var isUser = (msg.type === 1) || msg.is_user || msg.from === 'user';
|
调试技巧
在整个排查过程中,以下调试方法非常有用:
- 控制台日志:在关键节点打印
session_id 和消息内容
- Network 面板:确认 HTTP API 返回的数据格式
- WebSocket 面板(Chrome DevTools):查看 WebSocket 消息往来
- Application 面板:检查 localStorage/sessionStorage 的实际存储内容
最终效果
修复后,用户可以在小窗开始对话,点击”新窗口打开”后无缝切换到大窗继续聊天,所有历史消息完整保留。关闭大窗后,小窗仍然保持连接,可以继续对话。
这种体验对于需要长时间多轮对话的场景(如代码调试、复杂咨询)非常有价值。
相关代码变更:
文章作者:阿文
版权声明:本博客所有文章除特别声明外,均采用
CC BY-NC-SA 4.0 许可协议。转载请注明来自
阿文的博客!
评论
0 条评论