@@ -825,61 +825,11 @@ import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener.
|
||||
|
||||
const isThinking = ref(false)
|
||||
const isSelfSent = ref(false) // 是否当前设备发起的 AI 对话(用于区分弹窗/仅锁输入)
|
||||
const programInputLocked = ref(false)
|
||||
const isUploading = ref(false)
|
||||
const isLoadingMessages = ref(false) // 消息加载中状态(仅会话变化时显示)
|
||||
const uploadedFiles = ref([])
|
||||
const streamingMessageId = ref(null) // 流式消息的临时 ID,用于实时更新 AI 回复
|
||||
|
||||
const ensureStreamingBubble = () => {
|
||||
if (streamingMessageId.value) return
|
||||
streamingMessageId.value = 'streaming-' + Date.now()
|
||||
allmessages.value = [...allmessages.value, {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
_streaming: true,
|
||||
_id: streamingMessageId.value
|
||||
}]
|
||||
}
|
||||
|
||||
const removeStreamingBubble = () => {
|
||||
if (!streamingMessageId.value) return
|
||||
allmessages.value = allmessages.value.filter(m => m._id !== streamingMessageId.value)
|
||||
streamingMessageId.value = null
|
||||
}
|
||||
|
||||
const beginAiTurn = (selfSent = false) => {
|
||||
programInputLocked.value = false
|
||||
isThinking.value = true
|
||||
socketStore.messageString = ''
|
||||
socketStore.isThinking = true
|
||||
isSelfSent.value = selfSent
|
||||
ensureStreamingBubble()
|
||||
}
|
||||
|
||||
const cleanupAiTurn = async (options = {}) => {
|
||||
programInputLocked.value = false
|
||||
isThinking.value = false
|
||||
isSelfSent.value = false
|
||||
removeStreamingBubble()
|
||||
if (options.refresh !== false) {
|
||||
await takeConversationMessages()
|
||||
}
|
||||
}
|
||||
|
||||
const finishAiTurn = async (options = {}) => {
|
||||
await cleanupAiTurn(options)
|
||||
socketStore.isThinking = false
|
||||
}
|
||||
|
||||
const keepProgramInputLocked = () => {
|
||||
programInputLocked.value = true
|
||||
isThinking.value = true
|
||||
socketStore.isThinking = true
|
||||
isSelfSent.value = false
|
||||
ensureStreamingBubble()
|
||||
}
|
||||
|
||||
// 消息详情弹窗
|
||||
const showMessageModal = ref(false)
|
||||
const detailLinks = ref([])
|
||||
@@ -1086,9 +1036,6 @@ import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener.
|
||||
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (isUploading.value) return
|
||||
if (ChatType.value === 0 && isThinking.value) return
|
||||
|
||||
const message = textMessage.value.trim()
|
||||
if (!message && previewFileArray.value.length === 0) return
|
||||
|
||||
@@ -1249,18 +1196,27 @@ import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener.
|
||||
"conversation_id": currentSessionId.value,
|
||||
"content": messageContent
|
||||
}
|
||||
beginAiTurn(true)
|
||||
await socketStore.send(data)
|
||||
socketStore.send(data)
|
||||
isThinking.value = true
|
||||
isSelfSent.value = true
|
||||
// // 立即添加用户消息到列表(使用完整的 messageContent,包含文件信息)
|
||||
// allmessages.value = [...allmessages.value, {
|
||||
// role: 'user',
|
||||
// content: messageContent
|
||||
// }]
|
||||
// 添加流式消息气泡(加载中状态)
|
||||
streamingMessageId.value = 'streaming-' + Date.now()
|
||||
allmessages.value = [...allmessages.value, {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
_streaming: true,
|
||||
_id: streamingMessageId.value
|
||||
}]
|
||||
textMessage.value = ''
|
||||
uploadedFiles.value = []
|
||||
scrollToBottom()
|
||||
} catch (error) {
|
||||
await finishAiTurn({ refresh: false })
|
||||
isThinking.value = false
|
||||
uni.showToast({
|
||||
title: `用户发送消息失败${error}`,
|
||||
icon: 'error'
|
||||
@@ -1280,7 +1236,16 @@ import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener.
|
||||
} catch (err) {
|
||||
console.error('中断指令发送失败:', err)
|
||||
}
|
||||
await finishAiTurn({ refresh: false })
|
||||
// 移除流式消息占位
|
||||
if (streamingMessageId.value) {
|
||||
allmessages.value = allmessages.value.filter(m => m._id !== streamingMessageId.value)
|
||||
streamingMessageId.value = null
|
||||
}
|
||||
// 立即关闭弹窗,清除超时
|
||||
// clearTimeout(aiResponseTimer)
|
||||
isThinking.value = false
|
||||
socketStore.isThinking = false
|
||||
isSelfSent.value = false
|
||||
textMessage.value = ''
|
||||
uploadedFiles.value = []
|
||||
// takeConversationMessages 由后端中断确认驱动(见 handleBusinessMessage)
|
||||
@@ -1299,14 +1264,28 @@ import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener.
|
||||
console.log('AI开始回复(跨设备同步),锁定输入框');
|
||||
isThinking.value = true
|
||||
// 非本设备发起时,也需要添加流式气泡
|
||||
ensureStreamingBubble()
|
||||
if (!streamingMessageId.value) {
|
||||
streamingMessageId.value = 'streaming-' + Date.now()
|
||||
allmessages.value = [...allmessages.value, {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
_streaming: true,
|
||||
_id: streamingMessageId.value
|
||||
}]
|
||||
}
|
||||
return
|
||||
}
|
||||
// true → false:AI 回复结束或中断,解锁输入框并刷新消息列表
|
||||
if (oldVal && !newVal) {
|
||||
console.log('AI回复结束(正常/中断),解锁并刷新消息列表');
|
||||
// 移除流式消息气泡,从服务器加载完整消息
|
||||
cleanupAiTurn()
|
||||
if (streamingMessageId.value) {
|
||||
allmessages.value = allmessages.value.filter(m => m._id !== streamingMessageId.value)
|
||||
streamingMessageId.value = null
|
||||
}
|
||||
takeConversationMessages()
|
||||
isThinking.value = false
|
||||
isSelfSent.value = false
|
||||
textMessage.value = ''
|
||||
uploadedFiles.value = []
|
||||
}
|
||||
|
||||
@@ -161,21 +161,6 @@ export const useSocketStore = defineStore('socket', () => {
|
||||
// 调试日志:明确输出 type 和 data
|
||||
console.log(`[handleMessage] type = ${type}, data = ${JSON.stringify(data)}`);
|
||||
|
||||
const eventConversationId =
|
||||
conversation_id ||
|
||||
data?.conversation_id ||
|
||||
data?.page?.conversation?.conversation_id ||
|
||||
data?.page?.conversation?.id ||
|
||||
''
|
||||
if (
|
||||
eventConversationId &&
|
||||
config.value.conversationId &&
|
||||
String(eventConversationId) !== String(config.value.conversationId)
|
||||
) {
|
||||
addLog('info', `忽略非当前会话消息: ${eventConversationId}`)
|
||||
return
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'auth_ok':
|
||||
addLog('success', '连接成功')
|
||||
@@ -191,42 +176,6 @@ export const useSocketStore = defineStore('socket', () => {
|
||||
addLog('warn', 'AI成功收到用户发送的消息')
|
||||
break
|
||||
|
||||
case 'forbid_input':
|
||||
addLog('warn', '收到封框指令')
|
||||
isThinking.value = true
|
||||
break
|
||||
|
||||
case 'relieve_input':
|
||||
addLog('warn', '收到解框指令')
|
||||
isThinking.value = false
|
||||
break
|
||||
|
||||
case 'turn_status': {
|
||||
const status = String(messageData.status || data?.status || '').toLowerCase()
|
||||
if (status === 'locked') {
|
||||
addLog('warn', '后端确认程序封框')
|
||||
isThinking.value = true
|
||||
} else if (status === 'finished' || status === 'cancelled' || status === 'unlocked' || status === 'relieved') {
|
||||
addLog('warn', `后端确认当前轮结束/解框: ${status}`)
|
||||
isThinking.value = false
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'prompt':
|
||||
if ((messageData.info || data?.info) === '结束等待') {
|
||||
addLog('warn', '收到结束等待信号')
|
||||
isThinking.value = false
|
||||
}
|
||||
break
|
||||
|
||||
case 'refresh':
|
||||
if (messageData.page?.waiting === false || data?.page?.waiting === false) {
|
||||
addLog('warn', 'refresh 确认 waiting=false,释放输入框')
|
||||
isThinking.value = false
|
||||
}
|
||||
break
|
||||
|
||||
case 'render':
|
||||
addLog('warn', 'AI正在回复信息')
|
||||
handleBusinessMessage(data)
|
||||
@@ -234,8 +183,6 @@ export const useSocketStore = defineStore('socket', () => {
|
||||
|
||||
case 'pc_offline':
|
||||
addLog('warn', 'PC 端已离线')
|
||||
isThinking.value = false
|
||||
authFailReason.value = 'pc offline'
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -282,46 +229,6 @@ export const useSocketStore = defineStore('socket', () => {
|
||||
// 处理 data 为对象的情况
|
||||
if (!data || typeof data !== 'object') return
|
||||
|
||||
const msgType = String(data.type || '').trim()
|
||||
if (msgType === 'forbid_input') {
|
||||
console.log("收到后端封框指令");
|
||||
isThinking.value = true
|
||||
return
|
||||
}
|
||||
if (msgType === 'relieve_input') {
|
||||
console.log("收到后端解框指令");
|
||||
isThinking.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (data.cmd === 'turn_status') {
|
||||
const status = String(data.status || '').toLowerCase()
|
||||
if (status === 'locked') {
|
||||
console.log("后端确认程序封框");
|
||||
isThinking.value = true
|
||||
return
|
||||
}
|
||||
if (status === 'finished' || status === 'cancelled' || status === 'unlocked' || status === 'relieved') {
|
||||
console.log("后端确认当前轮结束/解框:", status);
|
||||
isThinking.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (data.cmd === 'prompt' && data.info === '结束等待') {
|
||||
console.log("收到结束等待信号");
|
||||
isThinking.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// refresh 也会在流式输出和工具中间消息落盘时出现,不能直接当成最终完成。
|
||||
// 只有后端明确 page.waiting=false 时,才作为漏发结束信号的兜底释放。
|
||||
if (data.cmd === 'refresh' && data.page?.waiting === false) {
|
||||
console.log("refresh 确认 waiting=false,释放输入框");
|
||||
isThinking.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 中断确认:后端返回 cmd=event_callback 表示中断成功
|
||||
if (data.cmd === 'event_callback' && data.info === '中断成功') {
|
||||
console.log("后端确认中断成功");
|
||||
|
||||
Reference in New Issue
Block a user