chore: bump version to 2.10.7 and improve message sending reliability with explicit refresh and type safety

This commit is contained in:
jxxghp
2026-04-28 13:04:05 +08:00
parent b785769138
commit f761cdff00
3 changed files with 120 additions and 44 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "moviepilot", "name": "moviepilot",
"version": "2.10.5", "version": "2.10.7",
"private": true, "private": true,
"type": "module", "type": "module",
"bin": "dist/service.js", "bin": "dist/service.js",
+26 -15
View File
@@ -14,6 +14,12 @@ import { getQueryValue } from '@/@core/utils'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { clearAppBadge } from '@/utils/badge' import { clearAppBadge } from '@/utils/badge'
type MessageViewExpose = {
pauseSSE?: () => void
resumeSSE?: () => void
refreshLatestMessages?: () => Promise<void> | void
}
// 国际化 // 国际化
const { t } = useI18n() const { t } = useI18n()
@@ -63,7 +69,7 @@ const sendButtonDisabled = ref(false)
const messageDialogRef = ref<any>(null) const messageDialogRef = ref<any>(null)
// 消息视图引用 // 消息视图引用
const messageViewRef = ref<any>(null) const messageViewRef = ref<MessageViewExpose | null>(null)
// 滚动容器引用 // 滚动容器引用
const messageContentRef = ref<any>() const messageContentRef = ref<any>()
@@ -153,9 +159,7 @@ async function openMessageDialog() {
}, 600) }, 600)
// 等待对话框打开后恢复SSE连接 // 等待对话框打开后恢复SSE连接
nextTick(() => { nextTick(() => {
if (messageViewRef.value && typeof messageViewRef.value.resumeSSE === 'function') { messageViewRef.value?.resumeSSE?.()
messageViewRef.value.resumeSSE()
}
}) })
} }
@@ -203,16 +207,23 @@ function allLoggingUrl() {
// 发送消息 // 发送消息
async function sendMessage() { async function sendMessage() {
if (user_message.value) { const messageText = user_message.value.trim()
try { if (!messageText) {
sendButtonDisabled.value = true return
await api.post(`message/web?text=${user_message.value}`) }
user_message.value = ''
sendButtonDisabled.value = false try {
forceScrollToEnd() // 发送消息后强制滚动到底部 sendButtonDisabled.value = true
} catch (error) { await api.post(`message/web?text=${encodeURIComponent(messageText)}`)
console.error(error) user_message.value = ''
}
// 发送成功后主动同步最新一页消息,避免SSE短暂断流时界面停留在旧状态。
await messageViewRef.value?.refreshLatestMessages?.()
forceScrollToEnd() // 发送消息后强制滚动到底部
} catch (error) {
console.error(error)
} finally {
sendButtonDisabled.value = false
} }
} }
@@ -228,7 +239,7 @@ defineExpose({
// 监听消息对话框状态变化 // 监听消息对话框状态变化
watch(messageDialog, newValue => { watch(messageDialog, newValue => {
if (!newValue && messageViewRef.value && typeof messageViewRef.value.pauseSSE === 'function') { if (!newValue && messageViewRef.value?.pauseSSE) {
// 对话框关闭时暂停SSE连接 // 对话框关闭时暂停SSE连接
messageViewRef.value.pauseSSE() messageViewRef.value.pauseSSE()
} }
+93 -28
View File
@@ -17,6 +17,10 @@ const messages = ref<Message[]>([])
// 当前页数据 // 当前页数据
const currData = ref<Message[]>([]) const currData = ref<Message[]>([])
// 已加载消息的签名集合
// 使用消息内容签名去重,避免仅按秒级时间戳判断时误吞同一秒内的不同消息。
const messageKeys = new Set<string>()
// 是否完成加载 // 是否完成加载
const isLoaded = ref(false) const isLoaded = ref(false)
@@ -29,18 +33,72 @@ const page = ref(1)
// 存量消息最新时间 // 存量消息最新时间
const lastTime = ref('') const lastTime = ref('')
// 获取消息时间
function getMessageTime(message: Message) {
return message.reg_time || message.date || ''
}
// 生成消息签名
function getMessageKey(message: Message) {
return [
message.action ?? '',
message.userid ?? '',
message.reg_time ?? '',
message.date ?? '',
message.title ?? '',
message.text ?? '',
message.image ?? '',
message.link ?? '',
message.note ?? '',
].join('::')
}
// 排序消息列表,确保最新消息始终位于底部
function sortMessages(items: Message[]) {
return [...items].sort((a, b) => compareTime(getMessageTime(a), getMessageTime(b)))
}
// 记录最新消息时间
function updateLastTime(message: Message) {
const messageTime = getMessageTime(message)
if (messageTime && compareTime(messageTime, lastTime.value) > 0) {
lastTime.value = messageTime
}
}
// 合并消息到当前列表
function mergeMessages(items: Message[]) {
let hasNewMessage = false
for (const item of sortMessages(items)) {
const messageKey = getMessageKey(item)
if (messageKeys.has(messageKey)) {
continue
}
messageKeys.add(messageKey)
messages.value.push(item)
updateLastTime(item)
hasNewMessage = true
}
if (hasNewMessage) {
messages.value = sortMessages(messages.value)
}
return hasNewMessage
}
// SSE消息处理函数 // SSE消息处理函数
function handleSSEMessage(event: MessageEvent) { function handleSSEMessage(event: MessageEvent) {
const message = event.data const message = event.data
if (message) { if (message) {
const object = JSON.parse(message) const object = JSON.parse(message)
// 使用reg_time或date字段进行比较 if (mergeMessages([object])) {
const messageTime = object.reg_time || object.date nextTick(() => {
if (compareTime(messageTime, lastTime.value) <= 0) return emit('scroll') // 新消息到达时触发智能滚动
messages.value.push(object) })
nextTick(() => { }
emit('scroll') // 新消息到达时触发智能滚动
})
} }
} }
@@ -75,28 +133,10 @@ async function loadMessages({ done }: { done: any }) {
// 已加载过 // 已加载过
isLoaded.value = true isLoaded.value = true
if (currData.value.length > 0) { if (currData.value.length > 0) {
// 按时间排序,确保最新的消息在最后 const hasNewMessage = mergeMessages(currData.value)
currData.value.sort((a, b) => {
const timeA = a.reg_time || a.date || ''
const timeB = b.reg_time || b.date || ''
return compareTime(timeA, timeB)
})
// 取最后一条时间为存量消息最新时间
const lastMessage = currData.value[currData.value.length - 1]
lastTime.value = lastMessage.reg_time || lastMessage.date || ''
// 合并数据并重新排序
const allMessages = [...currData.value, ...messages.value]
allMessages.sort((a, b) => {
const timeA = a.reg_time || a.date || ''
const timeB = b.reg_time || b.date || ''
return compareTime(timeA, timeB)
})
messages.value = allMessages
// 首次加载时滚动到底部 // 首次加载时滚动到底部
if (page.value === 1) { if (page.value === 1 && hasNewMessage) {
nextTick(() => { nextTick(() => {
emit('scroll') emit('scroll')
}) })
@@ -118,6 +158,26 @@ async function loadMessages({ done }: { done: any }) {
} }
} }
// 主动刷新最新一页消息,作为SSE偶发丢流时的兜底
async function refreshLatestMessages() {
try {
const latestMessages = (await api.get('message/web', {
params: {
page: 1,
size: 20,
},
})) as Message[]
if (mergeMessages(latestMessages)) {
nextTick(() => {
emit('scroll')
})
}
} catch (error) {
console.error('刷新最新消息失败:', error)
}
}
// 比较yyyy-MM-dd HH:mm:ss时间大小 // 比较yyyy-MM-dd HH:mm:ss时间大小
function compareTime(time1: string, time2: string) { function compareTime(time1: string, time2: string) {
if (!time1 && !time2) return 0 if (!time1 && !time2) return 0
@@ -160,14 +220,19 @@ function pauseSSE() {
// 恢复SSE连接 // 恢复SSE连接
function resumeSSE() { function resumeSSE() {
if (manager) { if (manager) {
// 先移除再重建监听,确保恢复时拿到一条新的SSE连接。
manager.removeMessageListener('message-view')
manager.addMessageListener('message-view', handleSSEMessage) manager.addMessageListener('message-view', handleSSEMessage)
} }
refreshLatestMessages()
} }
// 暴露方法给父组件 // 暴露方法给父组件
defineExpose({ defineExpose({
pauseSSE, pauseSSE,
resumeSSE, resumeSSE,
refreshLatestMessages,
}) })
onMounted(() => { onMounted(() => {
@@ -194,7 +259,7 @@ onMounted(() => {
<div> <div>
<div <div
v-for="(msg, index) in messages" v-for="(msg, index) in messages"
:key="index" :key="getMessageKey(msg) || index"
class="chat-group d-flex mt-5 mb-8" class="chat-group d-flex mt-5 mb-8"
:class="msg.action == 1 ? 'flex-row align-start' : 'flex-row-reverse align-end'" :class="msg.action == 1 ? 'flex-row align-start' : 'flex-row-reverse align-end'"
> >