feat(ctrt): 完成 CTRT-01~03 响应契约统一 - 三端拦截器+portal_token清理+调用点适配

This commit is contained in:
Simon
2026-07-07 22:56:49 +08:00
parent d56a9a6079
commit 6f0fbbb066
48 changed files with 6183 additions and 613 deletions
+117 -26
View File
@@ -17,6 +17,7 @@ import {
getCurrentConversation,
sendMessage,
pollMessages,
getMessages,
shake,
getApprovalLinks,
getSoftwareDownloads,
@@ -211,7 +212,8 @@ export const useConversationStore = defineStore('conversation', () => {
/** 是否有活跃会话(会话未结单) */
const hasActiveConversation = computed(() => {
if (!currentConversation.value) return false
return currentConversation.value.status !== 'closed'
// resolved: 已结单(后端状态)
return currentConversation.value.status !== 'resolved'
})
/** 当前用户是否为被邀请的参与者(非原始员工) */
@@ -613,6 +615,56 @@ export const useConversationStore = defineStore('conversation', () => {
}
}
/**
* 获取消息列表(历史消息)
* 获取当前会话的完整消息历史,用于首次加载或翻页
* @param params 查询参数(limit 和 before
*/
async function fetchMessages(params?: { limit?: number; before?: string }): Promise<void> {
// 未登录或无活跃会话时不获取
if (!isLoggedIn.value || !hasActiveConversation.value) return
try {
const data = await getMessages(params)
if (data.items && data.items.length > 0) {
// 消息去重
const uniqueMessages = data.items.filter(msg => {
if (processedMessageIds.value.has(msg.message_id)) {
return false
}
trackProcessedMessageId(msg.message_id)
return true
})
if (uniqueMessages.length > 0) {
// 如果没有 before 参数(全量加载),直接替换消息列表
// 如果有 before 参数(翻页),追加到列表末尾
if (!params?.before) {
messages.value = uniqueMessages
} else {
messages.value.push(...uniqueMessages)
}
// 更新最后消息 ID
const lastMsg = uniqueMessages[uniqueMessages.length - 1]
if (lastMsg) {
lastMessageId.value = lastMsg.message_id
}
console.log('[Store] 获取到历史消息:', uniqueMessages.length, '条')
// 保存到缓存
const convId = currentConversation.value?.conversation_id
if (convId) {
saveMessagesToCache(convId, messages.value)
}
}
}
} catch (error) {
console.error('[Store] 获取历史消息失败:', error)
}
}
/**
* 启动消息轮询
* 使用 setInterval 每 3 秒轮询一次新消息
@@ -644,10 +696,11 @@ export const useConversationStore = defineStore('conversation', () => {
* 招手/敲桌子 — 呼叫 IT 坐席
* 调用后端招手接口,返回趣味话术
* 将话术以系统消息形式插入对话列表
* @returns 分配结果: assigned(已分配坐席) / queued(排队中) / assign_failed(分配失败)
*/
async function shakeAgent(): Promise<void> {
async function shakeAgent(): Promise<string> {
// 防止重复点击
if (shaking.value) return
if (shaking.value) return 'pending'
shaking.value = true
try {
@@ -670,12 +723,28 @@ export const useConversationStore = defineStore('conversation', () => {
}
messages.value.push(systemMsg)
// 如果已分配坐席,更新话术内容
if (data.assign_result === 'assigned' && data.assigned_agent_id) {
systemMsg.content = `${data.funny_phrase}\n\n🎉 坐席已为您服务,请稍候...`
// 更新会话状态
if (currentConversation.value) {
currentConversation.value.status = 'serving'
}
} else if (data.assign_result === 'queued') {
// 进入排队
systemMsg.content = `${data.funny_phrase}\n\n⏳ 当前无空闲坐席,您已进入排队,请耐心等待...`
}
// 如果招手后坐席已接入(status === 'serving'),刷新会话信息
if (data.conversation?.status === 'serving') {
await fetchCurrentConversation()
}
// 返回分配结果
return data.assign_result || 'queued'
} catch (error) {
console.error('[Store] 招手失败:', error)
return 'error'
} finally {
shaking.value = false
}
@@ -774,11 +843,11 @@ export const useConversationStore = defineStore('conversation', () => {
// (后端 /h5/conversations/current 理论上应返回刚加入的会话)
}
// 清空消息列表,重新加载
// 清空消息列表,重新加载历史消息
messages.value = []
lastMessageId.value = ''
// 立即拉取一次消息,避免等3秒轮询
await pollNewMessages()
// 获取完整的历史消息
await fetchMessages()
console.log('[Store] 已切换到邀请会话:', conversationId)
} catch (error) {
console.error('[Store] 切换会话失败:', error)
@@ -896,8 +965,9 @@ export const useConversationStore = defineStore('conversation', () => {
* 初始化应用
* 1. 获取用户信息
* 2. 获取当前会话
* 3. 加载审批链接和软件下载
* 4. 启动消息轮询
* 3. 获取消息历史(新增 fetchMessages
* 4. 加载审批链接和软件下载
* 5. 启动消息轮询
*/
async function initialize(): Promise<void> {
if (initialized.value) return
@@ -905,7 +975,7 @@ export const useConversationStore = defineStore('conversation', () => {
try {
console.log('[Store] 开始初始化应用...')
// ===== 步骤1:并行加载用户信息和会话(不等待消息) =====
// ===== 步骤1:并行加载用户信息和会话 =====
await Promise.all([
fetchUserInfo(),
fetchCurrentConversation(),
@@ -918,32 +988,52 @@ export const useConversationStore = defineStore('conversation', () => {
if (cached && cached.length > 0) {
console.log(`[Store] 加载缓存消息 ${cached.length}`)
messages.value = cached
// 从缓存更新最后消息ID
const lastCached = cached[cached.length - 1]
if (lastCached) {
lastMessageId.value = lastCached.message_id
}
}
}
// ===== 步骤3后台加载后端消息(不阻塞UI =====
// 使用 Promise.resolve().then() 让它在下一次事件循环执行,不阻塞主线程
// ===== 步骤3获取历史消息(新增,使用 getMessages API =====
// 后台加载,不阻塞 UI
Promise.resolve().then(async () => {
try {
const freshMessages = await pollMessages()
if (freshMessages.length > 0) {
// 合并缓存和最新消息
if (convId) {
messages.value = mergeMessages(messages.value, freshMessages)
const data = await getMessages({ limit: 50 })
if (data.items && data.items.length > 0) {
// 消息去重
const uniqueMessages = data.items.filter(msg => {
if (processedMessageIds.value.has(msg.message_id)) {
return false
}
trackProcessedMessageId(msg.message_id)
return true
})
if (uniqueMessages.length > 0) {
// 合并缓存和历史消息
if (convId && messages.value.length > 0) {
messages.value = mergeMessages(messages.value, uniqueMessages)
} else {
messages.value = uniqueMessages
}
// 更新最后消息ID
const lastMsg = uniqueMessages[uniqueMessages.length - 1]
if (lastMsg) {
lastMessageId.value = lastMsg.message_id
}
// 保存到缓存
saveMessagesToCache(convId, messages.value)
} else {
messages.value = freshMessages
if (convId) {
saveMessagesToCache(convId, messages.value)
}
console.log(`[Store] 历史消息已加载,共 ${messages.value.length}`)
}
// 更新最后消息ID
const lastMsg = freshMessages[freshMessages.length - 1]
if (lastMsg) {
lastMessageId.value = lastMsg.message_id
}
console.log(`[Store] 后端消息已合并,共 ${messages.value.length}`)
}
} catch (e) {
console.warn('[Store] 加载后端消息失败:', e)
console.warn('[Store] 加载历史消息失败:', e)
}
})
@@ -1016,6 +1106,7 @@ export const useConversationStore = defineStore('conversation', () => {
handleOAuthCallback,
fetchUserInfo,
fetchCurrentConversation,
fetchMessages,
sendNewMessage,
pollNewMessages,
startPolling,