Compare commits

...

2 Commits

Author SHA1 Message Date
hy
a3bab0784d Merge branch 'main' of https://gitea.yuxindazhineng.com/YiLin/phone-app------test1- 2026-08-03 17:09:31 +08:00
hy
9965f2772d 封窗 2026-08-03 17:09:22 +08:00
2 changed files with 154 additions and 40 deletions

View File

@@ -825,11 +825,61 @@ 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([])
@@ -1036,6 +1086,9 @@ 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
@@ -1196,27 +1249,18 @@ import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener.
"conversation_id": currentSessionId.value,
"content": messageContent
}
socketStore.send(data)
isThinking.value = true
isSelfSent.value = true
beginAiTurn(true)
await socketStore.send(data)
// // 立即添加用户消息到列表(使用完整的 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) {
isThinking.value = false
await finishAiTurn({ refresh: false })
uni.showToast({
title: `用户发送消息失败${error}`,
icon: 'error'
@@ -1236,16 +1280,7 @@ import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener.
} catch (err) {
console.error('中断指令发送失败:', err)
}
// 移除流式消息占位
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
await finishAiTurn({ refresh: false })
textMessage.value = ''
uploadedFiles.value = []
// takeConversationMessages 由后端中断确认驱动(见 handleBusinessMessage
@@ -1264,28 +1299,14 @@ import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener.
console.log('AI开始回复跨设备同步锁定输入框');
isThinking.value = true
// 非本设备发起时,也需要添加流式气泡
if (!streamingMessageId.value) {
streamingMessageId.value = 'streaming-' + Date.now()
allmessages.value = [...allmessages.value, {
role: 'assistant',
content: '',
_streaming: true,
_id: streamingMessageId.value
}]
}
ensureStreamingBubble()
return
}
// true → falseAI 回复结束或中断,解锁输入框并刷新消息列表
if (oldVal && !newVal) {
console.log('AI回复结束正常/中断),解锁并刷新消息列表');
// 移除流式消息气泡,从服务器加载完整消息
if (streamingMessageId.value) {
allmessages.value = allmessages.value.filter(m => m._id !== streamingMessageId.value)
streamingMessageId.value = null
}
takeConversationMessages()
isThinking.value = false
isSelfSent.value = false
cleanupAiTurn()
textMessage.value = ''
uploadedFiles.value = []
}
@@ -2016,4 +2037,4 @@ import { openFile as openFileNative, downloadAndOpen } from '@/utils/fileOpener.
color: #fff;
font-weight: 500;
}
</style>
</style>

View File

@@ -161,6 +161,21 @@ 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', '连接成功')
@@ -176,6 +191,42 @@ 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)
@@ -183,6 +234,8 @@ export const useSocketStore = defineStore('socket', () => {
case 'pc_offline':
addLog('warn', 'PC 端已离线')
isThinking.value = false
authFailReason.value = 'pc offline'
break
default:
@@ -229,6 +282,46 @@ 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("后端确认中断成功");
@@ -303,4 +396,4 @@ export const useSocketStore = defineStore('socket', () => {
clearLogs,
getStatusText
}
})
})