fix(ai-chat): 修复历史记录加载、WebSocket长连接、服务端消息格式兼容

- 修复 WebSocket message_end 未收到问题:兼容 data.event === 'done' + data.data.conversation_id 嵌套格式
- 修复子组件 onShow/onUnload 不触发:移到页面 index.vue 的页面生命周期中
- 修复 loadHistoryIfNeeded 被重复 methods 块覆盖:合并到同一 methods 块
- 修复历史消息映射字段:msg.query → msg.content,添加 type:'text'
- 去掉所有有帮助/没帮助按钮
- CONVERSATION_KEY 改为统一导出常量
- WebSocket 长连接重构 + #ifdef 环境隔离
This commit is contained in:
2026-05-14 17:50:48 +08:00
parent fed3962965
commit bd778c0283
3 changed files with 66 additions and 52 deletions

View File

@@ -3,7 +3,7 @@ import http from './http.js'
import store from '@/store/index.js' import store from '@/store/index.js'
let currentConversationId = null; let currentConversationId = null;
const CONVERSATION_KEY = 'ai_conversation_id'; export const CONVERSATION_KEY = 'ai_conversation_id';
/** /**
* 获取域名配置参数(避免重复代码) * 获取域名配置参数(避免重复代码)
@@ -111,16 +111,24 @@ function ensureWebSocketConnected() {
if (data.event === 'message') { if (data.event === 'message') {
var text = data.answer || data.content || data.text || ''; var text = data.answer || data.content || data.text || '';
// 兼容嵌套 data.data.content 格式
if (!text && data.data) {
text = data.data.content || data.data.answer || '';
}
wsMessageContent += text; wsMessageContent += text;
if (wsMessageOnChunk) wsMessageOnChunk(text); if (wsMessageOnChunk) wsMessageOnChunk(text);
} else if (data.event === 'message_end') { } else if (data.event === 'message_end' || data.event === 'done') {
var conversationId = data.conversation_id || ''; // 兼容两种结束信号:message_end(流结束)或 done(服务端完成)
// 兼容两种数据层级:conversation_id 可能在顶层或 data.data 中
var conversationId = data.conversation_id || (data.data && data.data.conversation_id) || '';
console.log('[AI WS] 流结束信号, event:', data.event, 'type:', data.type, 'conversationId:', conversationId, '当前内存:', currentConversationId);
// 保存会话ID(独立函数,不使用 this) // 保存会话ID(独立函数,不使用 this)
if (conversationId) { if (conversationId) {
if (conversationId !== currentConversationId) { if (conversationId !== currentConversationId) {
currentConversationId = conversationId; currentConversationId = conversationId;
try { try {
uni.setStorageSync(CONVERSATION_KEY, conversationId); uni.setStorageSync(CONVERSATION_KEY, conversationId);
console.log('[AI WS] 会话ID已持久化到 storage:', conversationId);
} catch (e) { } catch (e) {
console.error('保存会话ID失败:', e); console.error('保存会话ID失败:', e);
} }
@@ -133,6 +141,9 @@ function ensureWebSocketConnected() {
wsMessageContent = ''; wsMessageContent = '';
if (wsMessageOnComplete) wsMessageOnComplete({ content: content, conversation_id: convId }); if (wsMessageOnComplete) wsMessageOnComplete({ content: content, conversation_id: convId });
if (wsMessageResolve) wsMessageResolve({ content: content, conversation_id: convId }); if (wsMessageResolve) wsMessageResolve({ content: content, conversation_id: convId });
} else {
// 兜底日志:打印所有未识别的消息类型
console.log('[AI WS] 未识别的消息:', JSON.stringify(data).substring(0, 200));
} }
} catch (e) { } catch (e) {
console.error('WebSocket 消息解析失败:', e, '原始数据:', res.data); console.error('WebSocket 消息解析失败:', e, '原始数据:', res.data);
@@ -507,6 +518,7 @@ export default {
if (!params.conversation_id) { if (!params.conversation_id) {
throw new Error('会话ID(conversation_id)是必填参数'); throw new Error('会话ID(conversation_id)是必填参数');
} }
console.log('[AI History] 请求历史, conversation_id:', params.conversation_id, 'limit:', params.limit);
const requestData = { const requestData = {
uniacid: params.uniacid || this.getUniacid(), uniacid: params.uniacid || this.getUniacid(),
conversation_id: params.conversation_id, conversation_id: params.conversation_id,

View File

@@ -426,7 +426,7 @@
</view> </view>
</template> </template>
<script> <script>
import aiService from '@/common/js/ai-service.js' import aiService, { CONVERSATION_KEY } from '@/common/js/ai-service.js'
import IMAGE_CONFIG from '@/common/js/image-config.js' import IMAGE_CONFIG from '@/common/js/image-config.js'
export default { export default {
@@ -545,31 +545,6 @@ export default {
return hasContent || hasEmoji return hasContent || hasEmoji
} }
}, },
onShow() {
// 如果不是 AI智能客服,立即退出并跳转
if (customerServiceType !== 'ai') {
uni.showToast({ title: '当前客服类型不支持此页面', icon: 'none' });
setTimeout(() => {
uni.navigateBack({ delta: 1 });
}, 1000);
return;
}
// 只在初次渲染或会话ID发生变化时才加载历史
const localConvId = this.getConversationIdFromLocal();
const shouldReload = this.currentConversationId !== localConvId;
if (localConvId && shouldReload) {
// 切换到已有的历史会话 → 重新加载历史
this.currentConversationId = localConvId;
aiService.setConversationId(localConvId);
this.loadChatHistoryIfExist();
// 注意: historyOffset 和 hasMoreHistory 在 loadChatHistoryIfExist() 内部设置
} else if (!localConvId && !this.currentConversationId) {
// 全新会话,初始化偏移量
this.historyOffset = this.messages.length;
}
// 会话未变化且已有消息 → 跳过加载,保留当前消息
},
// 新增:组件销毁时清除所有定时器 // 新增:组件销毁时清除所有定时器
beforeDestroy() { beforeDestroy() {
if (this.scrollTimer) clearTimeout(this.scrollTimer); if (this.scrollTimer) clearTimeout(this.scrollTimer);
@@ -589,6 +564,7 @@ export default {
// 初始化用户昵称(缓存优先) // 初始化用户昵称(缓存优先)
this.initUserNicknameData() this.initUserNicknameData()
this.currentConversationId = aiService.getConversationId(); this.currentConversationId = aiService.getConversationId();
console.log('[AI Chat] created, currentConversationId:', this.currentConversationId, 'messages.length:', this.messages.length);
}, },
mounted() { mounted() {
// 初始加载时滚底(仅一次) // 初始加载时滚底(仅一次)
@@ -596,10 +572,6 @@ export default {
this.scrollToBottom() this.scrollToBottom()
}) })
}, },
onUnload() {
// 页面销毁时断开 WebSocket 长连接
aiService.disconnectWebSocket();
},
watch: { watch: {
messages: { messages: {
handler(newVal, oldVal) { handler(newVal, oldVal) {
@@ -614,6 +586,31 @@ export default {
} }
}, },
methods: { methods: {
/**
* 供父页面调用的加载历史方法
* 在 page onShow 中通过 ref 调用
*/
loadHistoryIfNeeded() {
const localConvId = this.getConversationIdFromLocal();
const shouldReload = this.currentConversationId !== localConvId;
console.log('[AI Chat] loadHistoryIfNeeded | localConvId:', localConvId, '| currentConvId:', this.currentConversationId, '| shouldReload:', shouldReload, '| messages.length:', this.messages.length, '| initialMessages.length:', this.initialMessages.length);
if (localConvId && shouldReload) {
console.log('[AI Chat] → 分支1: ID变化,加载历史');
this.currentConversationId = localConvId;
aiService.setConversationId(localConvId);
this.loadChatHistoryIfExist();
} else if (localConvId && !shouldReload && this.messages.length <= this.initialMessages.length) {
console.log('[AI Chat] → 分支2: ID不变但只有初始消息,从服务端恢复');
this.currentConversationId = localConvId;
this.loadChatHistoryIfExist();
} else if (!localConvId && !this.currentConversationId) {
console.log('[AI Chat] → 分支3: 全新会话');
this.historyOffset = this.messages.length;
} else {
console.log('[AI Chat] → 分支4: 跳过加载,保留当前消息, messages.length:', this.messages.length);
}
},
// 生成消息的唯一 key(兼容非 H5 平台) // 生成消息的唯一 key(兼容非 H5 平台)
getMessageKey(message, index) { getMessageKey(message, index) {
const id = message.id || message.timestamp || index; const id = message.id || message.timestamp || index;
@@ -633,14 +630,14 @@ export default {
// 【新增:会话 ID 本地缓存方法】 // 【新增:会话 ID 本地缓存方法】
saveConversationIdToLocal(convId) { saveConversationIdToLocal(convId) {
if (convId) { if (convId) {
uni.setStorageSync('dify_conversation_id', convId); uni.setStorageSync(CONVERSATION_KEY, convId);
} }
}, },
getConversationIdFromLocal() { getConversationIdFromLocal() {
return uni.getStorageSync('dify_conversation_id') || null; return uni.getStorageSync(CONVERSATION_KEY) || null;
}, },
clearConversationIdFromLocal() { clearConversationIdFromLocal() {
uni.removeStorageSync('dify_conversation_id'); uni.removeStorageSync(CONVERSATION_KEY);
}, },
// 初始化音频上下文 // 初始化音频上下文
initAudioContext() { initAudioContext() {
@@ -684,44 +681,47 @@ export default {
// 新增:加载历史记录 // 新增:加载历史记录
async loadChatHistoryIfExist() { async loadChatHistoryIfExist() {
const convId = aiService.getConversationId(); const convId = aiService.getConversationId();
console.log('[AI Chat] loadChatHistoryIfExist 被调用, convId:', convId);
if (convId) { if (convId) {
try { try {
// 调用正确的方法,并传入会话ID // 调用正确的方法,并传入会话ID
console.log('[AI Chat] 请求历史 API...');
const response = await aiService.getConversationHistory({ const response = await aiService.getConversationHistory({
conversation_id: convId conversation_id: convId
}); });
console.log('[AI Chat] 历史 API 返回:', JSON.stringify(response).substring(0, 200));
if (!response.success || !Array.isArray(response.messages)) { if (!response.success || !Array.isArray(response.messages)) {
console.warn('历史记录为空或格式无效'); console.warn('[AI Chat] 历史记录为空或格式无效, success:', response.success, 'messages:', Array.isArray(response.messages));
return; return;
} }
console.log('[AI Chat] 原始消息数:', response.messages.length, 'total:', response.total);
// 需要根据实际返回的 response.data.messages 来映射 // 需要根据实际返回的 response.data.messages 来映射
this.messages = response.messages.map(msg => { this.messages = response.messages.map(msg => {
let content = ''; let content = '';
if (msg.role === 'user') { if (msg.role === 'user') {
content = msg.query || (typeof msg.inputs === 'string' ? msg.inputs : JSON.stringify(msg.inputs || '')) || ''; content = msg.query || msg.content || (typeof msg.inputs === 'string' ? msg.inputs : JSON.stringify(msg.inputs || '')) || '';
} else { } else {
content = msg.answer || ''; content = msg.answer || msg.content || '';
} }
// 只使用后端返回的真实时间,不使用假时间 // 只使用后端返回的真实时间,不使用假时间
const realTimestamp = msg.created_at || msg.create_time || msg.time; const realTimestamp = msg.created_at || msg.create_time || msg.time;
return { return {
id: msg.message_id || (Date.now() + Math.random() * 10000), id: msg.message_id || (Date.now() + Math.random() * 10000),
role: msg.role === 'user' ? 'user' : 'ai', role: msg.role === 'user' ? 'user' : 'ai',
type: 'text',
content: content, content: content,
timestamp: realTimestamp, timestamp: realTimestamp,
actions: msg.role !== 'user' ? [ actions: []
{ id: 1, text: '有帮助', type: 'like' },
{ id: 2, text: '没帮助', type: 'dislike' }
] : []
}; };
}).filter(msg => msg.content.trim() !== ''); // 过滤掉内容为空的消息 }).filter(msg => msg.content.trim() !== ''); // 过滤掉内容为空的消息
console.log('[AI Chat] 过滤后消息数:', this.messages.length, '内容摘要:', this.messages.map(m => ({role: m.role, content: m.content.substring(0, 30)})));
// 根据服务端 total 判断是否还有更多历史可加载 // 根据服务端 total 判断是否还有更多历史可加载
const msgCount = this.messages.length; const msgCount = this.messages.length;
this.hasMoreHistory = response.total > msgCount; this.hasMoreHistory = response.total > msgCount;
this.historyOffset = msgCount; this.historyOffset = msgCount;
} catch (error) { } catch (error) {
console.error('加载历史记录失败:', error); console.error('[AI Chat] 加载历史记录失败:', error.message, error.stack);
// 只需调用服务层的清除方法,它会处理本地存储 // 只需调用服务层的清除方法,它会处理本地存储
aiService.clearConversationId(); aiService.clearConversationId();
this.currentConversationId = null; this.currentConversationId = null;
@@ -1003,10 +1003,7 @@ async loadChatHistoryIfExist() {
timestamp: Date.now(), timestamp: Date.now(),
conversationId: response.conversationId, conversationId: response.conversationId,
messageId: response.messageId, messageId: response.messageId,
actions: [ actions: []
{ id: 1, text: '有帮助', type: 'like' },
{ id: 2, text: '没帮助', type: 'dislike' }
]
} }
this.shouldScrollToBottom = true this.shouldScrollToBottom = true
this.messages.push(aiMessage) this.messages.push(aiMessage)
@@ -1144,10 +1141,7 @@ _finishStream(msg, result) {
} }
} }
msg.actions = [ msg.actions = []
{ id: 1, text: '有帮助', type: 'like' },
{ id: 2, text: '没帮助', type: 'dislike' }
]
this.$nextTick(() => this.scrollToBottom()) this.$nextTick(() => this.scrollToBottom())
this.$emit('ai-response', msg) this.$emit('ai-response', msg)

View File

@@ -76,6 +76,7 @@ import navigationHelper from '@/common/js/navigation';
import { EventSafety } from '@/common/js/event-safety'; import { EventSafety } from '@/common/js/event-safety';
import aiChatMessage from './ai-chat-message.vue'; import aiChatMessage from './ai-chat-message.vue';
import IMAGE_CONFIG from '@/common/js/image-config.js'; import IMAGE_CONFIG from '@/common/js/image-config.js';
import aiService from '@/common/js/ai-service.js';
export default { export default {
components: { components: {
@@ -146,6 +147,7 @@ export default {
}, },
onShow() { onShow() {
console.log('[AI Chat Page] onShow 触发');
// 监听键盘高度变化 // 监听键盘高度变化
uni.onKeyboardHeightChange((res) => { uni.onKeyboardHeightChange((res) => {
this.keyboardHeight = res.height; this.keyboardHeight = res.height;
@@ -153,6 +155,10 @@ export default {
this.$refs.chat.updateKeyboardHeight(res.height); this.$refs.chat.updateKeyboardHeight(res.height);
} }
}); });
// 每次页面显示时尝试加载历史记录
if (this.$refs.chat && this.$refs.chat.loadHistoryIfNeeded) {
this.$refs.chat.loadHistoryIfNeeded();
}
}, },
onHide() { onHide() {
@@ -165,7 +171,9 @@ export default {
}, },
onUnload() { onUnload() {
console.log('[AI Chat Page] onUnload 触发');
this.cleanup(); this.cleanup();
aiService.disconnectWebSocket();
}, },
methods: { methods: {