From f944dd680c22e5ba0111fc8ff299367f448da871 Mon Sep 17 00:00:00 2001 From: cirry <812852553@qq.com> Date: Sun, 12 Apr 2026 10:55:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=B9=E8=AF=9D=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/App.vue | 4 + src/renderer/main.js | 4 +- src/renderer/stores/app.js | 129 ++++++++++++++++++++++++++- src/renderer/views/chat/ChatView.vue | 118 +++++++++++++----------- 4 files changed, 200 insertions(+), 55 deletions(-) diff --git a/src/renderer/App.vue b/src/renderer/App.vue index d10b529..6530f86 100644 --- a/src/renderer/App.vue +++ b/src/renderer/App.vue @@ -146,6 +146,10 @@ async function startService() { isServiceRunning.value = info.running; // 更新 baseUrl 供 http 层使用 if (info.url) window.__opencodeBaseUrl = info.url; + // 服务启动成功后,初始化 SSE 连接 + if (info.running) { + appStore.initSSE(); + } } catch (err) { console.error('启动服务失败:', err); } diff --git a/src/renderer/main.js b/src/renderer/main.js index 9eb8901..cacd61d 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -18,7 +18,9 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) { // 注册自定义图标组件 app.component('AppIcon', AppIcon); app.component('LucideIcon', LucideIcon); -app.use(createPinia()); + +const pinia = createPinia(); +app.use(pinia); app.use(router); app.use(ElementPlus); diff --git a/src/renderer/stores/app.js b/src/renderer/stores/app.js index cc83544..b22dfad 100644 --- a/src/renderer/stores/app.js +++ b/src/renderer/stores/app.js @@ -1,13 +1,138 @@ import { defineStore } from 'pinia'; -import { ref } from 'vue'; +import { ref, computed } from 'vue'; +import { sseManager } from '@/http/sse.js'; export const useAppStore = defineStore('app', () => { const title = ref('智聚超脑'); const collapsed = ref(false); + // SSE 相关状态 + const sseConnected = ref(false); + const currentSessionEvents = ref([]); + const assistantMessageIds = ref(new Set()); + function toggleSidebar() { collapsed.value = !collapsed.value; } - return { title, collapsed, toggleSidebar }; + /** + * 初始化 SSE 连接 + * 在连接 OpenCode 成功后调用 + */ + function initSSE() { + if (sseConnected.value) { + console.log('[AppStore] SSE 已连接,跳过初始化'); + return; + } + + console.log('[AppStore] 初始化 SSE 连接...'); + + // 监听连接状态 + sseManager.on('sse.error', () => { + sseConnected.value = false; + }); + + // 监听消息部分更新事件 + sseManager.on('message.part.updated', (data) => { + const props = data.properties || {}; + const part = props.part; + if (!part || part.type !== 'text') return; + + currentSessionEvents.value.push({ + type: 'message.part.updated', + data: props, + timestamp: Date.now(), + }); + }); + + // 监听消息更新事件 + sseManager.on('message.updated', (data) => { + const props = data.properties || {}; + const info = props.info; + if (info && info.role === 'assistant') { + assistantMessageIds.value.add(info.id); + } + + currentSessionEvents.value.push({ + type: 'message.updated', + data: props, + timestamp: Date.now(), + }); + }); + + // 监听会话空闲事件 + sseManager.on('session.idle', (data) => { + const props = data.properties || {}; + currentSessionEvents.value.push({ + type: 'session.idle', + data: props, + timestamp: Date.now(), + }); + }); + + // 建立连接 + sseManager.connect(); + sseConnected.value = true; + } + + /** + * 关闭 SSE 连接 + */ + function closeSSE() { + sseManager.disconnect(); + sseConnected.value = false; + } + + /** + * 重新连接 SSE + */ + function reconnectSSE() { + sseManager.reconnect(); + sseConnected.value = true; + } + + /** + * 清空当前会话的事件 + */ + function clearSessionEvents() { + currentSessionEvents.value = []; + } + + /** + * 检查消息 ID 是否是助手消息 + */ + function isAssistantMessage(messageId) { + return assistantMessageIds.value.has(messageId); + } + + /** + * 添加助手消息 ID + */ + function addAssistantMessageId(messageId) { + assistantMessageIds.value.add(messageId); + } + + /** + * 清空助手消息 ID 集合 + */ + function clearAssistantMessageIds() { + assistantMessageIds.value.clear(); + } + + return { + title, + collapsed, + toggleSidebar, + // SSE + sseConnected, + currentSessionEvents, + assistantMessageIds, + initSSE, + closeSSE, + reconnectSSE, + clearSessionEvents, + isAssistantMessage, + addAssistantMessageId, + clearAssistantMessageIds, + }; }); diff --git a/src/renderer/views/chat/ChatView.vue b/src/renderer/views/chat/ChatView.vue index 4699cd1..fa37016 100644 --- a/src/renderer/views/chat/ChatView.vue +++ b/src/renderer/views/chat/ChatView.vue @@ -47,17 +47,19 @@ import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'; import { useRoute } from 'vue-router'; import { ElMessage } from 'element-plus'; import { ChatDotRound, ArrowRight, ArrowDown } from '@element-plus/icons-vue'; -import { createEventSource } from '@/http/api.js'; +import { useAppStore } from '@/stores/app.js'; +import { sseManager } from '@/http/sse.js'; import axios from 'axios'; const route = useRoute(); +const appStore = useAppStore(); const isSending = ref(false); const inputText = ref(''); const messages = ref([]); const messagesRef = ref(null); const currentSessionId = ref(null); -const assistantMessageIds = new Set(); -let eventSource = null; +const localAssistantMessageIds = new Set(); +let unsubscribeCallbacks = []; // 从路由参数中获取 sessionId const routeSessionId = computed(() => route.query.sessionId); @@ -73,7 +75,8 @@ async function loadHistoryMessages(sessionId) { // 清空当前消息 messages.value = []; - assistantMessageIds.clear(); + localAssistantMessageIds.clear(); + appStore.clearAssistantMessageIds(); // 处理历史消息 messagesData.forEach((item) => { @@ -116,7 +119,8 @@ async function loadHistoryMessages(sessionId) { // 记录 assistant 消息 ID if (info.role === 'assistant') { - assistantMessageIds.add(info.id); + localAssistantMessageIds.add(info.id); + appStore.addAssistantMessageId(info.id); } } }); @@ -136,9 +140,9 @@ watch( if (newSessionId) { currentSessionId.value = newSessionId; loadHistoryMessages(newSessionId); - // 确保 SSE 连接已建立(如果之前断开) - if (!eventSource) { - connectSSE(); + // 确保 SSE 连接已建立 + if (!appStore.sseConnected) { + appStore.initSSE(); } } }, @@ -163,51 +167,56 @@ function upsertAssistantBubble(msgId, text) { scrollToBottom(); } -function connectSSE() { - if (eventSource) { - eventSource.close(); - eventSource = null; - } - console.log('[connectSSE] 建立 SSE 连接...'); - eventSource = createEventSource(); +/** + * 注册 SSE 事件监听器 + */ +function registerSSEListeners() { + // 监听消息部分更新事件 + const unsubscribePartUpdated = sseManager.on('message.part.updated', (data) => { + const props = data.properties || {}; + const part = props.part; + if (!part || part.type !== 'text') return; + if (part.sessionID !== currentSessionId.value) return; + // 通过 messageID 前缀区分用户/助手消息:只渲染 assistant 消息的 part + // assistant 消息的 messageID 会在 message.updated 事件中记录,用 localAssistantMessageIds 集合过滤 + if (!localAssistantMessageIds.has(part.messageID) && !appStore.isAssistantMessage(part.messageID)) return; + upsertAssistantBubble(part.messageID, part.text || ''); + }); - eventSource.onmessage = (e) => { - try { - const data = JSON.parse(e.data); - // 打印所有 SSE 事件,便于调试事件结构 - console.log('[SSE]', data.type, JSON.stringify(data)); - const props = data.properties || {}; + // 监听消息更新事件 + const unsubscribeMessageUpdated = sseManager.on('message.updated', (data) => { + const props = data.properties || {}; + const info = props.info; + // 记录 assistant 消息的 ID,供 message.part.updated 过滤使用 + if (info && info.role === 'assistant' && info.sessionID === currentSessionId.value) { + localAssistantMessageIds.add(info.id); + appStore.addAssistantMessageId(info.id); + } + }); - if (data.type === 'message.part.updated') { - const part = props.part; - if (!part || part.type !== 'text') return; - if (part.sessionID !== currentSessionId.value) return; - // 通过 messageID 前缀区分用户/助手消息:只渲染 assistant 消息的 part - // assistant 消息的 messageID 会在 message.updated 事件中记录,用 assistantMessageIds 集合过滤 - if (!assistantMessageIds.has(part.messageID)) return; - upsertAssistantBubble(part.messageID, part.text || ''); - } + // 监听会话空闲事件 + const unsubscribeSessionIdle = sseManager.on('session.idle', (data) => { + const props = data.properties || {}; + // session.idle 表示 AI 响应已全部完成,重置发送状态 + if (props.sessionID === currentSessionId.value) { + isSending.value = false; + } + }); - if (data.type === 'message.updated') { - const info = props.info; - // 记录 assistant 消息的 ID,供 message.part.updated 过滤使用 - if (info && info.role === 'assistant' && info.sessionID === currentSessionId.value) { - assistantMessageIds.add(info.id); - } - } + // 保存取消订阅函数 + unsubscribeCallbacks.push(unsubscribePartUpdated, unsubscribeMessageUpdated, unsubscribeSessionIdle); +} - if (data.type === 'session.idle') { - // session.idle 表示 AI 响应已全部完成,重置发送状态 - if (props.sessionID === currentSessionId.value) { - isSending.value = false; - } - } - } catch (_) {} - }; - - eventSource.onerror = () => { - isSending.value = false; - }; +/** + * 注销 SSE 事件监听器 + */ +function unregisterSSEListeners() { + unsubscribeCallbacks.forEach((unsubscribe) => { + if (typeof unsubscribe === 'function') { + unsubscribe(); + } + }); + unsubscribeCallbacks = []; } async function send() { @@ -240,12 +249,17 @@ async function send() { } onMounted(() => { - // 组件挂载时建立 SSE 连接 - connectSSE(); + // 组件挂载时注册 SSE 监听器 + registerSSEListeners(); + // 确保全局 SSE 连接已建立 + if (!appStore.sseConnected) { + appStore.initSSE(); + } }); onUnmounted(() => { - if (eventSource) eventSource.close(); + // 组件卸载时注销 SSE 监听器 + unregisterSSEListeners(); });