深夜提醒

现在是深夜,建议您注意休息,不要熬夜哦~

🏮 🏮 🏮

新年快乐

祝君万事如意心想事成!

2024 桐庐半程马拉松
00:00:00
时间
0.00
距离(公里)
--:--
配速
--
步频
--
心率 (bpm)
--
配速
步频
|
share-image
ESC

AI 聊天会话共享问题排查与解决

最近在博客中集成了 AI 聊天功能,包含两种交互方式:右下角的小窗(widget)和点击”新窗口打开”后的全屏页面。然而,在测试时发现一个令人头疼的问题:小窗里的聊天记录无法同步到大窗

问题现象

  1. 用户在右下角小窗与 AI 对话,发送了几条消息
  2. 点击”新窗口打开”按钮,期望在大窗中继续对话
  3. 大窗打开后,只显示默认的欢迎消息,之前的对话记录全部丢失

这显然是一个用户体验的灾难——用户以为可以”无缝切换”到大窗继续聊天,结果却像是开启了一个全新的对话。

AI 聊天会话共享问题排查与解决

问题分析

初步排查

首先检查了两个页面的存储机制:

小窗(Widget)

// 使用 localStorage 存储 session_id
localStorage.setItem('chat_session_id', sessionID);

大窗(Fullscreen)

// 使用 sessionStorage 存储 session_id
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') {
// 只有当前没有 sessionID 时才使用服务器返回的
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);
});
}
});
}

// WebSocket 连接成功后调用
ws.onmessage = function(event) {
var data = JSON.parse(event.data);
if (data.type === 'connected') {
// ...
if (sessionID) {
loadChatHistory(sessionID);
}
}
};

第五步:修复字段兼容问题

处理服务器返回的不同字段格式:

// 兼容两种格式:type === 1 或 is_user
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';

调试技巧

在整个排查过程中,以下调试方法非常有用:

  1. 控制台日志:在关键节点打印 session_id 和消息内容
  2. Network 面板:确认 HTTP API 返回的数据格式
  3. WebSocket 面板(Chrome DevTools):查看 WebSocket 消息往来
  4. Application 面板:检查 localStorage/sessionStorage 的实际存储内容

最终效果

修复后,用户可以在小窗开始对话,点击”新窗口打开”后无缝切换到大窗继续聊天,所有历史消息完整保留。关闭大窗后,小窗仍然保持连接,可以继续对话。

这种体验对于需要长时间多轮对话的场景(如代码调试、复杂咨询)非常有价值。


相关代码变更

文章作者:阿文
文章链接: https://www.awen.me/post/fe5a12ea.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿文的博客

评论

0 条评论
😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘 🛑📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭 ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️ 🈯💹❇️ ✳️🌐 💠Ⓜ️🌀 💤🏧🚾 🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️
您的评论由 AI 智能审核,一般1分钟内会展示,若不展示请确认你的评论是否符合社区和法律规范
加载中...

选择联系方式

留言反馈

😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘 🛑📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭 ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️ 🈯💹❇️ ✳️🌐 💠Ⓜ️🌀 💤🏧🚾 🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️