From 70f1f48548d2e788ed39afcbab4a5152a98c2f11 Mon Sep 17 00:00:00 2001 From: Ethan-Zhang Date: Sat, 1 Nov 2025 02:49:38 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20=E5=AE=8C=E5=96=84=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E9=85=8D=E7=BD=AE&=E6=96=B0=E5=A2=9E=E6=80=9D=E7=BB=B4?= =?UTF-8?q?=E9=93=BE=E5=BC=80=E5=85=B3&=E5=AE=8C=E5=96=84=E5=88=87?= =?UTF-8?q?=E6=8D=A2app=E5=AF=B9=E8=AF=9D=E8=AE=B0=E5=BD=95=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/appCenter/type.ts | 13 +- src/apis/paths/account.ts | 2 + src/apis/paths/llm.ts | 5 +- src/apis/paths/model.ts | 1 + src/apis/paths/type.ts | 2 + src/assets/svgs/thinking_bulb_off.svg | 6 + src/components/ModelSelector.vue | 143 ++++++ src/examples/UserPreferencesExample.vue | 227 ++++++++++ src/i18n/lang/en.ts | 22 + src/i18n/lang/zh-cn.ts | 22 + src/store/conversation.ts | 59 ++- src/store/historySession.ts | 237 ++++++---- src/store/userPreferences.ts | 3 + src/utils/storage.ts | 138 ++++++ src/utils/userPreferences.ts | 169 +++++++ src/views/chat/index.vue | 7 + .../createapp/components/AgentAppConfig.vue | 69 ++- .../workFlowConfig/LLMNodeDrawer.vue | 26 +- src/views/createapp/index.vue | 1 + .../dialogue/components/DialogueAside.vue | 313 ++++++++++--- .../dialogue/components/DialogueSession.vue | 416 +++++++++++++++--- src/views/dialogue/components/TitleBar.vue | 4 +- src/views/dialogue/dialogueView.vue | 1 + src/views/settings/Model.vue | 177 +++++--- src/views/settings/components/AddModel.vue | 46 +- src/views/settings/components/ModelCard.vue | 48 +- .../components/SystemModelSettings.vue | 34 +- src/views/settings/index.vue | 2 - src/views/tools/index.vue | 74 +++- vite.config.ts | 65 ++- 30 files changed, 1971 insertions(+), 361 deletions(-) create mode 100644 src/assets/svgs/thinking_bulb_off.svg create mode 100644 src/components/ModelSelector.vue create mode 100644 src/examples/UserPreferencesExample.vue create mode 100644 src/utils/storage.ts create mode 100644 src/utils/userPreferences.ts diff --git a/src/apis/appCenter/type.ts b/src/apis/appCenter/type.ts index 95d12c7..242a0dd 100644 --- a/src/apis/appCenter/type.ts +++ b/src/apis/appCenter/type.ts @@ -273,7 +273,18 @@ export interface AppDetail { description: string; debug: boolean; }; - mcpService?: string[]; + mcpService?: Array<{ + id: string; + name?: string; + description?: string; + icon?: string; + }>; + llm?: { + llmId: string; + modelName?: string; + icon?: string; + }; + enableThinking?: boolean; model?: { provider: string; icon?: string; diff --git a/src/apis/paths/account.ts b/src/apis/paths/account.ts index 74bbdb6..55ef3f6 100644 --- a/src/apis/paths/account.ts +++ b/src/apis/paths/account.ts @@ -170,6 +170,7 @@ export const getUserPreferences = (): Promise< name?: string; }; chainOfThoughtPreference?: boolean; + autoExecutePreference?: boolean; }> | undefined ), @@ -188,6 +189,7 @@ export const updateUserPreferences = (preferences: { embeddingModelPreference?: any; rerankerPreference?: any; chainOfThoughtPreference?: boolean; + autoExecutePreference?: boolean; }): Promise<[any, FcResponse | undefined]> => { return put('/api/user/preferences', preferences); }; diff --git a/src/apis/paths/llm.ts b/src/apis/paths/llm.ts index 0a64684..923fc5a 100644 --- a/src/apis/paths/llm.ts +++ b/src/apis/paths/llm.ts @@ -27,9 +27,10 @@ const updateLLMList = ({ conversationId, llmId }) => { /** * 获取已添加模型列表 + * @param modelType 模型类型,可选值:'chat' | 'embedding' | 'reranker' 等 */ -const getAddedModels = () => { - return get('/api/llm'); +const getAddedModels = (modelType?: string) => { + return get('/api/llm', modelType ? { modelType } : {}); }; /** diff --git a/src/apis/paths/model.ts b/src/apis/paths/model.ts index b30e733..d01a438 100644 --- a/src/apis/paths/model.ts +++ b/src/apis/paths/model.ts @@ -82,6 +82,7 @@ const createOrUpdateModel = (params: { openaiApiKey: string; modelName: string; maxTokens: number; + type?: 'chat' | 'image' | 'video' | 'speech' | 'embedding' | 'reranker'; }) => { return put('/api/llm', params, { llmId: params.llmId }); }; diff --git a/src/apis/paths/type.ts b/src/apis/paths/type.ts index 3305110..0f0d832 100644 --- a/src/apis/paths/type.ts +++ b/src/apis/paths/type.ts @@ -224,6 +224,8 @@ export interface AddedModalList { openaiApiKey: string; modelName: string; maxTokens: string; + supportsThinking?: boolean; + canToggleThinking?: boolean; } /** * teamKnowledgeList, 获取teamKnowledgeList列表 diff --git a/src/assets/svgs/thinking_bulb_off.svg b/src/assets/svgs/thinking_bulb_off.svg new file mode 100644 index 0000000..556670e --- /dev/null +++ b/src/assets/svgs/thinking_bulb_off.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/ModelSelector.vue b/src/components/ModelSelector.vue new file mode 100644 index 0000000..f6f168c --- /dev/null +++ b/src/components/ModelSelector.vue @@ -0,0 +1,143 @@ + + + + + + diff --git a/src/examples/UserPreferencesExample.vue b/src/examples/UserPreferencesExample.vue new file mode 100644 index 0000000..6584fe2 --- /dev/null +++ b/src/examples/UserPreferencesExample.vue @@ -0,0 +1,227 @@ + + + + + + diff --git a/src/i18n/lang/en.ts b/src/i18n/lang/en.ts index 87d0402..3c1944e 100644 --- a/src/i18n/lang/en.ts +++ b/src/i18n/lang/en.ts @@ -31,6 +31,16 @@ export default { cancel: 'Cancel', }, }, + model: { + type: { + chat: 'Chat', + image: 'Image', + video: 'Video', + speech: 'Speech', + embedding: 'Embedding', + reranker: 'Reranker', + }, + }, settings: { model: 'Model', setting: 'Settings', @@ -39,6 +49,7 @@ export default { added_model: 'Added Models', model_provider: 'Providers', select_model: 'Model', + model_type: 'Model Type', new_api_key: 'New API Key', secret_key: 'Secret Key', secret_key_desc: 'Description', @@ -48,12 +59,21 @@ export default { api_key: 'Enter the API key.', model_name: 'Enter the model.', max_token: 'Enter the maximum number of tokens.', + model_type: 'Select model type', }, model_preferences_settings: 'Model Preferences Settings', reasoning_model_preference: 'Reasoning Model Preference', embedding_model_preference: 'Embedding Model Preference', reranker_preference: 'Reranker Preference', chain_of_thought_preference: 'Chain of Thought Preference', + auto_execute_preference: 'Auto Execute Preference', + save_success: 'Saved successfully', + save_failed: 'Save failed', + }, + thinking: { + enable_thinking: 'Enable thinking', + disable_thinking: 'Disable thinking', + always_thinking: 'This model always uses thinking', }, home: { name: 'openEuler Intelligence', @@ -201,6 +221,8 @@ export default { appDescription_input: 'Enter an agent description.', modelSelected: 'Model', modelSelected_input: 'Select a model.', + enable_thinking: 'Chain of Thought', + enable_thinking_tip: 'Enable thinking', multi_Dialogue: 'Multi-turn Conversation', multi_Dialogue_select: 'Number of Turns', ability_Configuration: 'Capabilities', diff --git a/src/i18n/lang/zh-cn.ts b/src/i18n/lang/zh-cn.ts index 378d4de..5fff832 100644 --- a/src/i18n/lang/zh-cn.ts +++ b/src/i18n/lang/zh-cn.ts @@ -30,6 +30,16 @@ export default { cancel: '取消', }, }, + model: { + type: { + chat: '对话', + image: '图像', + video: '视频', + speech: '语音', + embedding: '嵌入', + reranker: '重排', + }, + }, settings: { model: '模型', setting: '设置', @@ -38,6 +48,7 @@ export default { added_model: '已添加的模型', model_provider: '供应商', select_model: '输入模型', + model_type: '模型类型', new_api_key: '新建 API KEY', secret_key: '密钥', secret_key_desc: '密钥描述', @@ -47,12 +58,21 @@ export default { api_key: '请输入 API KEY', model_name: '请输入模型', max_token: '请输入最大 Token 数', + model_type: '请选择模型类型', }, model_preferences_settings: '模型偏好设置', reasoning_model_preference: '推理模型偏好', embedding_model_preference: 'Embedding模型偏好', reranker_preference: 'Reranker偏好', chain_of_thought_preference: '思维链偏好', + auto_execute_preference: '自动执行偏好', + save_success: '保存成功', + save_failed: '保存失败', + }, + thinking: { + enable_thinking: '启用思维链', + disable_thinking: '关闭思维链', + always_thinking: '该模型始终启用思维链', }, home: { name: 'openEuler 智能化解决方案', @@ -200,6 +220,8 @@ export default { appDescription_input: '请输入智能体描述', modelSelected: '模型选择', modelSelected_input: '请选择模型', + enable_thinking: '思维链', + enable_thinking_tip: '开启思维链', multi_Dialogue: '多轮对话', multi_Dialogue_select: '请选择对话轮次', ability_Configuration: '能力配置', diff --git a/src/store/conversation.ts b/src/store/conversation.ts index 7cb1027..2e0cd5b 100644 --- a/src/store/conversation.ts +++ b/src/store/conversation.ts @@ -69,6 +69,9 @@ export const useSessionStore = defineStore('conversation', () => { // 历史对话加载状态 const isLoadingHistory = ref(false); + + // 🔑 新增:防止并发加载的机制 + let currentLoadingConversationId: string | null = null; const currentTaskId = ref(null); @@ -305,7 +308,6 @@ export const useSessionStore = defineStore('conversation', () => { // 🔑 关键修复:检查暂停状态和生成状态,双重保险 if (isPaused.value || !isAnswerGenerating.value) { // 手动暂停输出或已停止生成 - console.log('🛑 [handleMsgDataShow] 检测到暂停状态或已停止生成,忽略消息处理'); isAnswerGenerating.value = false; return; } @@ -414,6 +416,9 @@ export const useSessionStore = defineStore('conversation', () => { groupId: params.groupId, language: langStore.language, question: params.question, + llmId: params.llmId, // 🔑 新增:包含模型ID + enableThinking: params.enableThinking, // 🔑 新增:思维链开关参数 + autoExecute: params.autoExecute, // 🔑 新增:自动执行开关参数 // record_id: params.qaRecordId, }), openWhenHidden: true, @@ -459,6 +464,9 @@ export const useSessionStore = defineStore('conversation', () => { language: langStore.language, groupId: params.groupId, question: params.question, + llmId: params.llmId, // 🔑 新增:包含模型ID + enableThinking: params.enableThinking, // 🔑 新增:思维链开关参数 + autoExecute: params.autoExecute, // 🔑 新增:自动执行开关参数 record_id: params.qaRecordId, }), openWhenHidden: true, @@ -484,6 +492,9 @@ export const useSessionStore = defineStore('conversation', () => { groupId: params.groupId, language: langStore.language, question: params.question, + llmId: params.llmId, // 🔑 新增:包含模型ID + enableThinking: params.enableThinking, // 🔑 新增:思维链开关参数 + autoExecute: params.autoExecute, // 🔑 新增:自动执行开关参数 record_id: params.qaRecordId, }), openWhenHidden: true, @@ -530,6 +541,9 @@ export const useSessionStore = defineStore('conversation', () => { groupId?: string; params?: any; type?: any; + llmId?: string; // 🔑 新增:模型ID参数 + enableThinking?: boolean; // 🔑 新增:思维链开关参数 + autoExecute?: boolean; // 🔑 新增:自动执行开关参数 }, ind?: number, waitType?: string, @@ -655,6 +669,9 @@ export const useSessionStore = defineStore('conversation', () => { type?: any, waitType?: string, isDebug?: boolean, + llmId?: string, + enableThinking?: boolean, + autoExecute?: boolean, ): Promise => { // 如果当前有对话正在生成,先停止它 if (isAnswerGenerating.value) { @@ -722,6 +739,9 @@ export const useSessionStore = defineStore('conversation', () => { question, qaRecordId, groupId, + llmId, // 🔑 新增:传递模型ID + enableThinking, // 🔑 新增:传递思维链开关参数 + autoExecute, // 🔑 新增:传递自动执行开关参数 }; if (user_selected_flow && user_selected_app) { getStreamParams = { @@ -795,7 +815,7 @@ export const useSessionStore = defineStore('conversation', () => { if (!question) { return; } - sendQuestion(groupId, question, user_selected_app, answerInd, recordId, ''); + sendQuestion(groupId, question, user_selected_app, answerInd, recordId, '', undefined, undefined, undefined, false, undefined); }; // #region ----------------------------------------< pagination >-------------------------------------- @@ -852,6 +872,18 @@ export const useSessionStore = defineStore('conversation', () => { * @param manageLoadingState 是否由此方法管理loading状态,默认为true */ const getConversation = async (conversationId: string, manageLoadingState: boolean = true): Promise => { + // 🔑 防止并发加载同一个对话 + if (currentLoadingConversationId === conversationId) { + return; + } + + // 🔑 如果正在加载其他对话,取消之前的加载 + if (currentLoadingConversationId) { + // 取消之前的加载 + } + + currentLoadingConversationId = conversationId; + // 如果需要管理loading状态,则设置为true if (manageLoadingState) { isLoadingHistory.value = true; @@ -917,11 +949,15 @@ export const useSessionStore = defineStore('conversation', () => { // 🔑 重要:确保DOM更新后再滚动到底部 await nextTick(); scrollToBottom(true); + + } else { + conversationList.value = []; } } catch (error) { - console.error('获取历史对话失败:', error); + conversationList.value = []; // 确保出错时也清空列表 } finally { // 无论成功或失败都重置加载状态 + currentLoadingConversationId = null; isLoadingHistory.value = false; } }; @@ -983,8 +1019,6 @@ export const useSessionStore = defineStore('conversation', () => { // 强制停止对话生成 - 用于页面销毁时清理 const forceStopGeneration = (): void => { - console.log('🛑 [forceStopGeneration] 开始强制停止对话生成,当前状态:', isAnswerGenerating.value); - if (isAnswerGenerating.value) { // 🔑 立即设置状态,阻止新的流式数据处理 isPaused.value = true; @@ -1002,23 +1036,16 @@ export const useSessionStore = defineStore('conversation', () => { if (lastItem.message && Array.isArray(lastItem.message)) { lastItem.message[lastItem.currentInd || 0] += ' [已中断]'; } - console.log('✅ [forceStopGeneration] 已标记最后一个对话项为完成状态'); } } // 🔑 新增:调用后端停止接口,使用Promise但不等待响应 if (currentTaskId.value) { - console.log('🔄 [forceStopGeneration] 调用后端停止接口,taskId:', currentTaskId.value); - // 使用api.stopGeneration但不等待响应 api.stopGeneration(currentTaskId.value).then(([error, response]) => { - if (!error && response?.code === 200) { - console.log('✅ [forceStopGeneration] 后端停止成功'); - } else { - console.warn('⚠️ [forceStopGeneration] 后端停止失败:', error); - } + // 停止成功或失败都静默处理 }).catch((err) => { - console.warn('⚠️ [forceStopGeneration] 后端停止接口调用异常:', err); + // 静默处理异常 }); // 降级方案:使用fetch with keepalive作为备用 @@ -1043,10 +1070,6 @@ export const useSessionStore = defineStore('conversation', () => { // 清理当前任务ID currentTaskId.value = null; - - console.log('✅ [forceStopGeneration] 强制停止完成,最终状态:', isAnswerGenerating.value); - } else { - console.log('ℹ️ [forceStopGeneration] 当前没有对话在生成,无需停止'); } }; diff --git a/src/store/historySession.ts b/src/store/historySession.ts index 7af0927..73a4776 100644 --- a/src/store/historySession.ts +++ b/src/store/historySession.ts @@ -11,11 +11,12 @@ import { api } from 'src/apis'; import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; import { storeToRefs } from 'pinia'; -import { useRouter, useRoute } from 'vue-router'; +// Router操作已移至组件中处理 import { useSessionStore } from '.'; import type { SessionItem } from 'src/components/sessionCard/type'; import { successMsg } from 'src/components/Message'; import i18n from 'src/i18n'; +import { getPreferredReasoningModel } from '@/utils/userPreferences'; export interface HistorySessionItem { conversationId: string; @@ -35,8 +36,18 @@ export interface HistorySessionItem { export const useHistorySessionStore = defineStore( 'sessionStore', () => { - // 历史会话列表 - const historySession = ref([]); + // 🔑 新架构:分层存储,每个 app 都有独立的对话记录 + // key: appId (空字符串表示无 app) + // value: 该 app 的对话记录数组 + const allHistorySessions = ref>({}); + + // 🔑 计算属性:根据当前 app 返回对应的对话列表 + // 这样切换 app 时不会触发 watch(只有数据真正变化才触发) + const historySession = computed(() => { + const currentAppId = user_selected_app.value || ''; + return allHistorySessions.value[currentAppId] || []; + }); + const params = ref(); const user_selected_app = ref(); const selectLLM = ref(); @@ -65,8 +76,6 @@ export const useHistorySessionStore = defineStore( try { // 🔑 关键修复:如果当前有对话正在生成,强制停止它 if (isAnswerGenerating.value) { - console.log('🛑 [changeSession] 检测到对话正在生成,强制停止...'); - // 立即设置暂停状态,阻止新的流式数据处理 forceStopGeneration(); @@ -75,12 +84,9 @@ export const useHistorySessionStore = defineStore( // 🔑 双重保险:再次检查并确保生成状态已停止 if (isAnswerGenerating.value) { - console.warn('⚠️ [changeSession] 第一次停止未完成,再次尝试停止...'); forceStopGeneration(); await new Promise(resolve => setTimeout(resolve, 200)); } - - console.log('✅ [changeSession] 对话生成已停止,当前状态:', isAnswerGenerating.value); } // 🔑 新增:清空当前对话列表,避免显示上一个会话的内容 @@ -91,60 +97,24 @@ export const useHistorySessionStore = defineStore( // 🔑 关键修复:根据当前对话的appId更新app选择状态 const currentSession = historySession.value.find(session => session.conversationId === conversationId); if (currentSession) { - console.log('🔍 [changeSession] 找到当前会话:', currentSession); - if (currentSession.appId && currentSession.appId.trim() !== '') { // 对话绑定了app,设置为对应的app - console.log('🔍 [changeSession] 对话绑定了app,设置app状态:', currentSession.appId); user_selected_app.value = currentSession.appId; } else { // 对话没有绑定app,清除app选择状态 - console.log('🔍 [changeSession] 对话未绑定app,清除app状态'); user_selected_app.value = ''; } } else { - console.warn('🔍 [changeSession] 未找到对应的会话记录:', conversationId); // 如果找不到会话记录,清除app状态 user_selected_app.value = ''; } - // 🔑 修复:同时更新路由,确保路由与session状态保持一致 - const router = useRouter(); - const route = useRoute(); - const currentQuery = { ...route.query }; - - // 添加conversationId到路由参数中 - currentQuery.conversationId = conversationId; - - // 🔑 新增:根据app状态更新路由参数 - if (user_selected_app.value && user_selected_app.value.trim() !== '') { - currentQuery.appId = user_selected_app.value; - // 尝试获取app名称 - const { appList } = storeToRefs(useSessionStore()); - const relatedApp = appList.value?.find(app => app.appId === user_selected_app.value); - if (relatedApp) { - currentQuery.name = relatedApp.name; - } - } else { - // 清除路由中的app参数 - delete currentQuery.appId; - delete currentQuery.name; + // 保留原有的DOM操作 + const a = document.getElementsByClassName('draw'); + for (const i of a) { + (i as HTMLElement).style.display = 'none'; } - - router.replace({ - path: route.path, - query: currentQuery - }); - - const { getConversation } = useSessionStore(); - await getConversation(currentSelectedSession.value, false).then(() => { - const a = document.getElementsByClassName('draw'); - for (const i of a) { - (i as HTMLElement).style.display = 'none'; - } - }); } catch (error) { - console.error('切换会话失败:', error); } finally { // 确保loading状态被重置(只有当我们设置了loading状态时才重置) if (setLoadingState) { @@ -222,6 +192,8 @@ export const useHistorySessionStore = defineStore( }; // 🔑 新增:防重复调用标志 let isLoadingSessionRecord = false; + // 🔑 新增:防止递归创建对话的标志 + let isCreatingSession = false; /** * 获取历史会话列表 @@ -230,7 +202,6 @@ export const useHistorySessionStore = defineStore( const getHistorySession = async (): Promise => { // 🔑 防重复调用:如果正在加载中,直接返回 if (isLoadingSessionRecord) { - console.log('🔄 [getHistorySession] 正在加载中,跳过重复调用'); return; } @@ -242,10 +213,7 @@ export const useHistorySessionStore = defineStore( const { conversationList } = storeToRefs(useSessionStore()); if (!err && res) { - // 🔑 关键修复:根据当前选择的app过滤对话记录 - const currentAppId = user_selected_app.value || ''; - console.log('🔍 [getHistorySession] 当前选择的app:', currentAppId); - + // 🔑 新架构:按 app 分组存储对话记录 const allConversations = res.result.conversations .reverse() .map((item: any) => ({ @@ -260,31 +228,31 @@ export const useHistorySessionStore = defineStore( })) .filter((item) => item.conversationId !== conversationId); - // 🔑 新增:按app分离对话记录 - historySession.value = allConversations.filter((item) => { - const itemAppId = item.appId || ''; - const matches = itemAppId === currentAppId; - return matches; + // 🔑 新架构:按 appId 分组存储 + const groupedSessions: Record = {}; + + allConversations.forEach((item) => { + const appId = item.appId || ''; + if (!groupedSessions[appId]) { + groupedSessions[appId] = []; + } + groupedSessions[appId].push(item); }); - // 🔑 修复:当当前app没有对话时,自动创建新对话(但避免递归) - if (historySession.value.length === 0) { - console.log('🔍 [getHistorySession] 当前app无对话记录,自动创建新对话,appId:', currentAppId); - // 🔑 重要修复:先释放锁,再创建新对话,避免死锁 - isLoadingSessionRecord = false; - await generateSession(false); - return; // 🔑 generateSession内部会调用getHistorySession,所以这里直接返回 - } + // 🔑 更新全局存储 + allHistorySessions.value = groupedSessions; - if (!currentSelectedSession.value) { - currentSelectedSession.value = - res.result.conversations[0]?.conversationId; - } + + // 🔑 移除自动创建对话的逻辑,统一由 DialogueAside.vue 的 watch 处理 + // 这样可以避免与 watch 的逻辑冲突,确保对话创建的时机正确 + + // 🔑 移除自动设置 currentSelectedSession 的逻辑 + // 现在由 DialogueAside.vue 的 watch 统一管理对话的选择 if (currentSelectedSession.value) { const sessionStore = useSessionStore(); const { isAnswerGenerating } = storeToRefs(sessionStore); - const { getConversation, forceStopGeneration } = sessionStore; + const { forceStopGeneration } = sessionStore; // 如果当前有对话正在生成,先停止它 if (isAnswerGenerating.value) { @@ -293,17 +261,18 @@ export const useHistorySessionStore = defineStore( await new Promise(resolve => setTimeout(resolve, 100)); } - await getConversation(currentSelectedSession.value); + // 🔑 修复:移除重复的getConversation调用,由DialogueSession.vue的watch统一处理 return; } - await createNewSession(); - if (res.result.conversations.length === 0) { - conversationList.value = []; - } + // 🔑 移除自动创建会话的逻辑,统一由 DialogueAside.vue 的 watch 处理 + // 这样可以避免与 watch 的逻辑冲突,确保对话创建的时机正确 + // await createNewSession(); + // if (res.result.conversations.length === 0) { + // conversationList.value = []; + // } } } catch (error) { - console.error('🔥 [getHistorySession] 获取历史会话失败:', error); } finally { // 🔑 确保无论成功失败都释放锁 isLoadingSessionRecord = false; @@ -355,27 +324,107 @@ export const useHistorySessionStore = defineStore( * 创建一个新的会话 */ const generateSession = async (isDebug): Promise => { - const appId = user_selected_app.value ?? ''; - console.log('🔍 [generateSession] 创建会话,appId:', appId, 'debug:', isDebug); - const [_, res] = await api.createSession({appId, debug: isDebug}); - if (!_ && res) { - currentSelectedSession.value = res.result.conversationId; + // 🔑 防止重复创建对话 + if (isCreatingSession) { + return; + } + + isCreatingSession = true; + + try { + const appId = user_selected_app.value ?? ''; + + // 🔑 新增:获取用户偏好的推理模型(可选,失败不影响创建对话) + let preferredLlmId = ''; + try { + const [llmError, llmRes] = await api.getLLMList(); + if (!llmError && llmRes && llmRes.code === 200) { + // 🔑 新增:只保留chat类型的模型 + const allModels = llmRes.result || []; + const chatModels = allModels.filter(model => + !model.type || model.type === 'chat' // 如果没有type字段则默认为chat类型 + ); + const preferredModel = getPreferredReasoningModel(chatModels); + if (preferredModel) { + preferredLlmId = preferredModel.llmId; + } else { + } + } else { + } + } catch (error) { + } + + + // 🔑 临时修复:简化参数,避免可能的参数问题 + const createParams: any = { + appId: appId || '', // 确保appId不是undefined + debug: isDebug || false + }; + + // 只有当llmId有值时才添加 + if (preferredLlmId && preferredLlmId.trim() !== '') { + createParams.llm_id = preferredLlmId; + } + + + const [error, res] = await api.createSession(createParams); + + if (error) { + // 提供更详细的错误信息 + if (error.response) { + throw new Error(`创建会话失败: HTTP ${error.response.status} - ${error.response.data?.message || error.response.statusText}`); + } else if (error.request) { + throw new Error('创建会话失败: 网络连接错误'); + } else { + throw new Error(`创建会话失败: ${error.message || error}`); + } + } + + if (!res) { + throw new Error('创建会话失败: 服务器无响应'); + } + + if (!res.result || !res.result.conversationId) { + throw new Error(`创建会话失败: 响应格式错误 - ${res.message || '未知错误'}`); + } + + if (res) { + const newConversationId = res.result.conversationId; + currentSelectedSession.value = newConversationId; + + // 🔑 修复:创建新会话时更新路由(在组件中处理) - // 🔑 修复:创建新会话时同时更新路由 - const router = useRouter(); - const route = useRoute(); - const currentQuery = { ...route.query }; + // 🔑 新架构:手动将新会话添加到 allHistorySessions + const currentAppId = appId || ''; + const newSession: HistorySessionItem = { + conversationId: newConversationId, + createdTime: new Date().toISOString(), + title: '新对话', + docCount: 0, + appId: currentAppId, + debug: isDebug || false, + kbList: [], + llm: { icon: '', modelName: '', llmId: preferredLlmId || '' }, + }; - // 添加conversationId到路由参数中 - currentQuery.conversationId = res.result.conversationId; + // 🔑 确保 allHistorySessions 有这个 app 的数组 + if (!allHistorySessions.value[currentAppId]) { + allHistorySessions.value[currentAppId] = []; + } - router.replace({ - path: route.path, - query: currentQuery - }); + // 🔑 将新会话添加到对应 app 的数组开头 + allHistorySessions.value[currentAppId].unshift(newSession); - // 🔑 优化:创建新会话后刷新历史记录,但要确保防重复机制生效 - await getHistorySession(); + + // 🔑 简化:不需要再调用 getHistorySession(),因为已经手动更新了数据 + // 重置防重复标志 + isLoadingSessionRecord = false; + } + } catch (error) { + throw error; // 重新抛出错误 + } finally { + // 🔑 确保无论成功失败都重置创建标志 + isCreatingSession = false; } }; diff --git a/src/store/userPreferences.ts b/src/store/userPreferences.ts index f771060..47dd8fa 100644 --- a/src/store/userPreferences.ts +++ b/src/store/userPreferences.ts @@ -45,6 +45,7 @@ export interface UserPreferences { embeddingModelPreference?: EmbeddingModelPreference; rerankerPreference?: RerankerModelPreference; chainOfThoughtPreference?: boolean; + autoExecutePreference?: boolean; } export const useUserPreferencesStore = defineStore('userPreferences', () => { @@ -54,6 +55,7 @@ export const useUserPreferencesStore = defineStore('userPreferences', () => { embeddingModelPreference: undefined, rerankerPreference: undefined, chainOfThoughtPreference: undefined, + autoExecutePreference: undefined, }); // 加载状态 @@ -101,6 +103,7 @@ export const useUserPreferencesStore = defineStore('userPreferences', () => { preferences.embeddingModelPreference = undefined; preferences.rerankerPreference = undefined; preferences.chainOfThoughtPreference = undefined; + preferences.autoExecutePreference = undefined; }; return { diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 0000000..9d8bc8a --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,138 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +/** + * 安全的存储访问工具,处理沙箱环境中的 SecurityError + */ + +// 检查是否可以访问 localStorage +function isLocalStorageAvailable(): boolean { + try { + const test = '__localStorage_test__'; + localStorage.setItem(test, test); + localStorage.removeItem(test); + return true; + } catch (e) { + return false; + } +} + +// 检查是否可以访问 sessionStorage +function isSessionStorageAvailable(): boolean { + try { + const test = '__sessionStorage_test__'; + sessionStorage.setItem(test, test); + sessionStorage.removeItem(test); + return true; + } catch (e) { + return false; + } +} + +// 内存存储作为后备方案 +const memoryStorage: Record = {}; + +export const safeLocalStorage = { + getItem(key: string): string | null { + if (isLocalStorageAvailable()) { + try { + return localStorage.getItem(key); + } catch (e) { + console.warn('localStorage.getItem failed:', e); + } + } + return memoryStorage[key] || null; + }, + + setItem(key: string, value: string): void { + if (isLocalStorageAvailable()) { + try { + localStorage.setItem(key, value); + return; + } catch (e) { + console.warn('localStorage.setItem failed:', e); + } + } + memoryStorage[key] = value; + }, + + removeItem(key: string): void { + if (isLocalStorageAvailable()) { + try { + localStorage.removeItem(key); + } catch (e) { + console.warn('localStorage.removeItem failed:', e); + } + } + delete memoryStorage[key]; + }, + + clear(): void { + if (isLocalStorageAvailable()) { + try { + localStorage.clear(); + } catch (e) { + console.warn('localStorage.clear failed:', e); + } + } + Object.keys(memoryStorage).forEach(key => delete memoryStorage[key]); + } +}; + +export const safeSessionStorage = { + getItem(key: string): string | null { + if (isSessionStorageAvailable()) { + try { + return sessionStorage.getItem(key); + } catch (e) { + console.warn('sessionStorage.getItem failed:', e); + } + } + return memoryStorage[`session_${key}`] || null; + }, + + setItem(key: string, value: string): void { + if (isSessionStorageAvailable()) { + try { + sessionStorage.setItem(key, value); + return; + } catch (e) { + console.warn('sessionStorage.setItem failed:', e); + } + } + memoryStorage[`session_${key}`] = value; + }, + + removeItem(key: string): void { + if (isSessionStorageAvailable()) { + try { + sessionStorage.removeItem(key); + } catch (e) { + console.warn('sessionStorage.removeItem failed:', e); + } + } + delete memoryStorage[`session_${key}`]; + }, + + clear(): void { + if (isSessionStorageAvailable()) { + try { + sessionStorage.clear(); + } catch (e) { + console.warn('sessionStorage.clear failed:', e); + } + } + Object.keys(memoryStorage).forEach(key => { + if (key.startsWith('session_')) { + delete memoryStorage[key]; + } + }); + } +}; diff --git a/src/utils/userPreferences.ts b/src/utils/userPreferences.ts new file mode 100644 index 0000000..a6dbfe4 --- /dev/null +++ b/src/utils/userPreferences.ts @@ -0,0 +1,169 @@ +/** + * 用户偏好设置管理工具 + * 用于在localStorage中存储和获取用户偏好设置,影响各个组件的默认选项 + */ + +export interface ModelPreference { + llmId: string; + modelName: string; + icon?: string; + [key: string]: any; +} + +export interface UserPreferences { + reasoningModelPreference?: ModelPreference; + embeddingModelPreference?: ModelPreference; + rerankerPreference?: ModelPreference; + chainOfThoughtPreference?: boolean; + autoExecutePreference?: boolean; +} + +const PREFERENCES_KEY = 'euler_user_preferences'; + +/** + * 获取用户偏好设置 + */ +export function getUserPreferences(): UserPreferences { + try { + const stored = localStorage.getItem(PREFERENCES_KEY); + if (stored) { + return JSON.parse(stored); + } + } catch (error) { + console.warn('获取用户偏好设置失败:', error); + } + return {}; +} + +/** + * 保存用户偏好设置 + */ +export function saveUserPreferences(preferences: UserPreferences): void { + try { + localStorage.setItem(PREFERENCES_KEY, JSON.stringify(preferences)); + } catch (error) { + console.error('保存用户偏好设置失败:', error); + } +} + +/** + * 更新特定的偏好设置 + */ +export function updateUserPreference( + key: K, + value: UserPreferences[K] +): void { + const preferences = getUserPreferences(); + preferences[key] = value; + saveUserPreferences(preferences); +} + +/** + * 清除用户偏好设置 + */ +export function clearUserPreferences(): void { + try { + localStorage.removeItem(PREFERENCES_KEY); + } catch (error) { + console.error('清除用户偏好设置失败:', error); + } +} + +/** + * 根据用户偏好匹配默认选项 + * 支持通过llmId和modelName进行匹配 + */ +export function findPreferredOption( + options: T[], + preference?: ModelPreference +): T | undefined { + if (!preference || !options.length) { + return undefined; + } + + // 首先尝试通过llmId精确匹配 + if (preference.llmId) { + const exactMatch = options.find(option => option.llmId === preference.llmId); + if (exactMatch) { + return exactMatch; + } + } + + // 如果llmId匹配失败,尝试通过modelName匹配 + if (preference.modelName) { + const nameMatch = options.find(option => + option.modelName === preference.modelName || + option.label === preference.modelName + ); + if (nameMatch) { + return nameMatch; + } + } + + return undefined; +} + +/** + * 获取推理模型的默认选项 + */ +export function getPreferredReasoningModel( + options: T[] +): T | undefined { + const preferences = getUserPreferences(); + return findPreferredOption(options, preferences.reasoningModelPreference); +} + +/** + * 获取嵌入模型的默认选项 + */ +export function getPreferredEmbeddingModel( + options: T[] +): T | undefined { + const preferences = getUserPreferences(); + return findPreferredOption(options, preferences.embeddingModelPreference); +} + +/** + * 获取重排序模型的默认选项 + */ +export function getPreferredRerankerModel( + options: T[] +): T | undefined { + const preferences = getUserPreferences(); + return findPreferredOption(options, preferences.rerankerPreference); +} + +/** + * 获取思维链偏好设置 + */ +export function getChainOfThoughtPreference(): boolean { + const preferences = getUserPreferences(); + return preferences.chainOfThoughtPreference ?? true; // 默认开启 +} + +/** + * 获取自动执行偏好设置 + */ +export function getAutoExecutePreference(): boolean { + const preferences = getUserPreferences(); + return preferences.autoExecutePreference ?? false; // 默认关闭 +} + +/** + * 监听用户偏好设置变化 + */ +export function watchUserPreferences(callback: (preferences: UserPreferences) => void): () => void { + const handleStorageChange = (e: StorageEvent) => { + if (e.key === PREFERENCES_KEY) { + const newPreferences = e.newValue ? JSON.parse(e.newValue) : {}; + callback(newPreferences); + } + }; + + window.addEventListener('storage', handleStorageChange); + + // 返回清理函数 + return () => { + window.removeEventListener('storage', handleStorageChange); + }; +} diff --git a/src/views/chat/index.vue b/src/views/chat/index.vue index e27a41b..53dd8c2 100644 --- a/src/views/chat/index.vue +++ b/src/views/chat/index.vue @@ -69,6 +69,9 @@ const { conversations, setConversations } = useConversations(); const { isStreaming, queryStream } = useStream(); +// 🔑 自动执行状态(chat页面默认不自动执行,因为没有工具调用) +const autoExecute = ref(false); + const chatContainerRef = ref(null); const { scrollToBottom } = useScrollBottom(chatContainerRef, { @@ -111,6 +114,10 @@ function useStream() { context_num: 2, max_tokens: 2048, }, + // 🔑 chat页面默认启用思维链(如果模型支持的话) + enableThinking: true, + // 🔑 传递自动执行设置(chat页面默认false,因为通常不涉及工具执行) + autoExecute: autoExecute.value, }; // 🔑 重要修复:添加AbortController来支持主动中断请求 diff --git a/src/views/createapp/components/AgentAppConfig.vue b/src/views/createapp/components/AgentAppConfig.vue index c545609..d2fab03 100644 --- a/src/views/createapp/components/AgentAppConfig.vue +++ b/src/views/createapp/components/AgentAppConfig.vue @@ -13,6 +13,7 @@ import type { Mcp } from './McpDrawer.vue'; import CustomLoading from '../../customLoading/index.vue'; import i18n from '@/i18n'; import defaultIcon from '@/assets/svgs/defaultIcon.webp'; +import { getPreferredReasoningModel, getChainOfThoughtPreference } from '@/utils/userPreferences'; const { t } = i18n.global; const route = useRoute(); @@ -26,6 +27,7 @@ interface AgentConfig { prompt: string; mcps: string[]; knowledge: any[]; + enableThinking: boolean; permission: { visibility: string; authorizedUsers: string[]; @@ -61,6 +63,7 @@ const createAppForm = reactive({ prompt: '', mcps: [], knowledge: [], + enableThinking: true, permission: { visibility: 'public', authorizedUsers: [], @@ -75,6 +78,14 @@ const selectedMcpService = computed(() => ), ); +const selectedModel = computed(() => + modelOptions.value.find((m) => m.llmId === createAppForm.model) +); + +const canToggleThinking = computed(() => + selectedModel.value?.canToggleThinking || false +); + const rules = reactive>({ name: [{ required: true, message: t('app.appName_input') }], description: [{ required: true, message: t('app.appDescription_input') }], @@ -99,7 +110,7 @@ async function queryAgentConfig() { createAppFormRef.value?.clearValidate(); if (res) { - const { name, description, permission, icon, mcpService, dialogRounds, llm } = + const { name, description, permission, icon, mcpService, dialogRounds, llm, enableThinking } = res.result; createAppForm.icon = icon || ''; createAppForm.name = name; @@ -108,14 +119,16 @@ async function queryAgentConfig() { createAppForm.dialogRounds = dialogRounds || 3; createAppForm.permission = permission || createAppForm.permission; // 如果有模型 - createAppForm.model = llm?.llmId; + createAppForm.model = llm?.llmId || ''; + // 如果有思维链配置 + createAppForm.enableThinking = enableThinking !== undefined ? enableThinking : true; } loading.value = false; } function onMcpServiceSelected(mcps: Mcp[]) { if (mcps) { - createAppForm.mcps = mcps.map((item) => item.mcpserviceId); + createAppForm.mcps = mcps.map((item) => item.mcpserviceId).filter(Boolean) as string[]; isMcpDrawerVisible.value = false; } @@ -172,11 +185,28 @@ async function queryUserList() { } async function queryModelList() { - const [, res] = await api.getAddedModels(); + const [, res] = await api.getAddedModels('chat'); if (res) { modelOptions.value = res.result; if (modelOptions.value.length > 0) { - createAppForm.model = modelOptions.value[0].llmId; + // 优先使用用户偏好的推理模型 + const preferredModel = getPreferredReasoningModel(modelOptions.value); + if (preferredModel) { + createAppForm.model = preferredModel.llmId; + } else { + // 如果没有偏好设置或匹配不到,使用第一个模型 + createAppForm.model = modelOptions.value[0].llmId; + } + + // 根据用户偏好和模型能力设置 enableThinking 的默认值 + const selectedModel = modelOptions.value.find(m => m.llmId === createAppForm.model); + if (selectedModel?.canToggleThinking) { + // 如果模型支持开关思维链,使用用户偏好设置 + createAppForm.enableThinking = getChainOfThoughtPreference(); + } else { + // 如果模型不支持开关,保持默认值 false + createAppForm.enableThinking = false; + } } } } @@ -198,6 +228,24 @@ async function queryMcpList() { } const createAppFormRef = ref(); + +// 监听模型切换,动态更新 enableThinking 的默认值 +watch( + () => createAppForm.model, + (newModelId) => { + if (!newModelId) return; + + const selectedModel = modelOptions.value.find(m => m.llmId === newModelId); + if (selectedModel?.canToggleThinking) { + // 如果新模型支持开关思维链,使用用户偏好设置 + createAppForm.enableThinking = getChainOfThoughtPreference(); + } else { + // 如果新模型不支持开关,设置为 false + createAppForm.enableThinking = false; + } + } +); + watch( () => createAppForm, async () => { @@ -354,6 +402,11 @@ onMounted(async () => { + + + + {{ t('app.enable_thinking_tip') }} + @@ -631,6 +684,12 @@ onMounted(async () => { } } + .thinking-tip { + margin-left: 12px; + font-size: 12px; + color: var(--o-text-color-tertiary); + } + .mcp-adder { width: 100%; .mcp-button { diff --git a/src/views/createapp/components/workFlowConfig/LLMNodeDrawer.vue b/src/views/createapp/components/workFlowConfig/LLMNodeDrawer.vue index ef91655..aaa14b3 100644 --- a/src/views/createapp/components/workFlowConfig/LLMNodeDrawer.vue +++ b/src/views/createapp/components/workFlowConfig/LLMNodeDrawer.vue @@ -300,6 +300,7 @@ import { api } from 'src/apis'; import VariableRichTextEditor from '@/components/VariableRichTextEditor.vue'; import { NodeType, getSrcIcon } from '../types'; + import { getPreferredReasoningModel } from '@/utils/userPreferences'; const visible = ref(true); const infoDisabled = ref(true); @@ -341,13 +342,13 @@ enable_temperature: false, enable_frequency_penalty: false, frequency_penalty: 0, - llmId: 'empty', + llmId: '', user_question: '', node_type: NodeType.LLM, selectedModel: { icon: '@/assets/images/logo-euler-copilot.png', modelName: 'default-model', - llmId: 'empty', + llmId: '', }, }); @@ -388,15 +389,28 @@ const getProviderLLM = async () => { const [_, res] = await api.getLLMList(); if (!_ && res && res.code === 200) { - if (res.result.length === 0) { + // 🔑 新增:只保留chat类型的模型 + const allModels = res.result || []; + const chatModels = allModels.filter(model => + !model.type || model.type === 'chat' // 如果没有type字段则默认为chat类型 + ); + + if (chatModels.length === 0) { ElMessage.error(i18n.global.t('feedback.no_model_error')); } - llmOptions.value.push(...res.result); - if (llmForm.llmId !== 'empty') { + llmOptions.value.push(...chatModels); + if (llmForm.llmId) { llmOptions.value.forEach((item) => { llmForm.selectedModel = item.llmId === llmForm.llmId ? item : llmForm.selectedModel; }); + } else { + // 如果是新节点,使用用户偏好的推理模型作为默认选项 + const preferredModel = getPreferredReasoningModel(chatModels); + if (preferredModel) { + llmForm.selectedModel = preferredModel; + llmForm.llmId = preferredModel.llmId; + } } } else { ElMessage.error(i18n.global.t('feedback.network_error')); @@ -408,7 +422,7 @@ { icon: '@/assets/images/logo-euler-copilot.png', modelName: 'default-model', - llmId: 'empty', + llmId: '', }, ]); diff --git a/src/views/createapp/index.vue b/src/views/createapp/index.vue index 45819b2..32559fa 100644 --- a/src/views/createapp/index.vue +++ b/src/views/createapp/index.vue @@ -147,6 +147,7 @@ const saveApp = async (type: 'agent' | 'flow') => { icon: formData.icon, name: formData.name, llm: formData.model, + enableThinking: formData.enableThinking, description: formData.description, dialogRounds: formData.dialogRounds, mcpService: formData.mcps, diff --git a/src/views/dialogue/components/DialogueAside.vue b/src/views/dialogue/components/DialogueAside.vue index 361cb48..98214cb 100644 --- a/src/views/dialogue/components/DialogueAside.vue +++ b/src/views/dialogue/components/DialogueAside.vue @@ -63,9 +63,9 @@ const deleteType = ref(true); const searchKey = ref(''); const activeNames = ref(['today', 'week', 'month', 'other']); const isCollapsed = ref(false); -const selectedAppId = ref(null); +const selectedAppId = ref(''); // -const apps = ref([]); +const apps = ref([]); const filteredHistorySessions = computed(() => { // filter by searchKey @@ -129,10 +129,13 @@ function checkDate(date: string | Date): string { return 'else'; } -onMounted(() => { +onMounted(async () => { // 🔑 优化:DialogueAside负责初始化历史记录,但要避免与DialogueSession重复调用 // 使用防重复机制确保只有一个组件成功加载 - getHistorySession(); + await getHistorySession(); + + // 🔑 标记历史记录已完成首次加载 + isHistoryLoaded.value = true; }); watch( @@ -180,6 +183,12 @@ const dialogVisible = ref(false); // 批量删除 const isBatchDeletion = ref(false); +// 🔑 新增:跟踪历史记录是否已完成首次加载 +const isHistoryLoaded = ref(false); + +// 🔑 新增:防止 watch 并发执行的标志 +const isWatchProcessing = ref(false); + /** * 删除会话记录 */ @@ -191,16 +200,21 @@ const deleteSession = async () => { const [, res] = await api.deleteSession({ conversationList }); if (res) { selectedSessionIds.value = []; + // 🔑 修复:删除时先清空当前选中的对话 currentSelectedSession.value = ''; successMsg(i18n.global.t('history.delete_successfully')); - if (isSelectedAll.value == true) { - historySession.value = []; - isBatchDeletion.value = false; - } else { - console.log('删除会话时候记录'); - getHistorySession(); - isBatchDeletion.value = false; - } + + // 🔑 修复:不需要锁定 watch,让它正常处理 + // 因为 getHistorySession() 会更新 historySession,自然触发 watch + + // 🔑 修复:无论是否全部删除,都重新获取历史记录 + // 这样可以确保数据的一致性 + await getHistorySession(); + isBatchDeletion.value = false; + + // 🔑 移除手动创建对话的逻辑 + // 对话的创建由watch统一处理,无需在这里手动创建 + // watch会检测到historySession变化后自动处理 } else { ElMessage.error(i18n.global.t('history.delete_failed')); } @@ -259,29 +273,58 @@ const toggleCollapse = () => { isCollapsed.value = !isCollapsed.value; }; +// 🔑 新增:防止重复创建对话的标志 +const isCreatingNewChat = ref(false); + const handleNewChat = _.debounce( async () => { - console.log('🔍 [DialogueAside] 新建对话,切换到无app模式'); - selectedAppId.value = ''; - user_selected_app.value = ''; - app.value.selectedAppId = ''; - app.value.appId = ''; + if (isCreatingNewChat.value) { + return; + } - // 🔑 新增:清空当前对话列表和选择状态 - const { conversationList } = storeToRefs(useSessionStore()); - conversationList.value = []; - currentSelectedSession.value = ''; + isCreatingNewChat.value = true; - // 🔑 修复:清除URL中的所有参数,确保完全切换到无app模式 - router.replace({ - path: '/', - query: {} - // 注意:清空所有query参数,包括conversationId,确保不会嫁接其他对话 - }); - - // 🔑 关键修复:直接创建新对话,而不是加载历史记录 - console.log('🔍 [DialogueAside] 直接创建新的无app对话'); - await generateSession(false); + try { + // 🔑 修复:新建对话时不清空 app 状态 + // 如果当前在某个 app 下,就创建该 app 的对话 + // 如果当前没有选中 app,就创建无 app 的对话 + + await generateSession(false); + + if (currentSelectedSession.value) { + const query: any = { conversationId: currentSelectedSession.value }; + + // 如果有选中的 app,保持 URL 中的 appId + if (user_selected_app.value) { + query.appId = user_selected_app.value; + const selectedApp = apps.value.find(app => app.appId === user_selected_app.value); + if (selectedApp) { + query.name = selectedApp.name; + } + } + + router.replace({ + path: '/', + query + }); + } + + } catch (error) { + let errorMessage = '未知错误'; + try { + if (error && typeof error === 'object' && 'message' in error) { + errorMessage = (error as any).message || error.toString() || '未知错误'; + } else if (typeof error === 'string') { + errorMessage = error; + } + } catch (e) { + errorMessage = '错误信息解析失败'; + } + + ElMessage.error('创建新对话失败,请重试'); + } finally { + isCreatingNewChat.value = false; + } }, 500, { leading: true, trailing: false }, @@ -289,78 +332,49 @@ const handleNewChat = _.debounce( const selectApp = async (id) => { if (selectedAppId.value === id) { - // 取消选择当前app,切换到无app模式 - console.log('🔍 [DialogueAside] 取消选择app,切换到无app模式'); + // 取消选择 app selectedAppId.value = ''; user_selected_app.value = ''; app.value.selectedAppId = ''; app.value.appId = ''; - // 🔑 新增:清空当前对话列表和选择状态 - const { conversationList } = storeToRefs(useSessionStore()); - conversationList.value = []; - currentSelectedSession.value = ''; + // 🔑 修复:不清空 currentSelectedSession,让 watch 决定是否需要切换 + // currentSelectedSession.value = ''; - // 🔑 修复:清除URL中的所有参数,确保完全切换到无app模式 router.replace({ path: '/', query: {} - // 注意:清空所有query参数,包括conversationId,确保不会嫁接app对话到无app模式 }); - // 🔑 修复:切换到无应用对话的最新conversation + // 🔑 修复:不需要锁定 watch + // 刷新历史会话列表,后续由 watch 处理对话选择/创建 await getHistorySession(); - const noAppSessions = historySession.value.filter(session => !session.appId || session.appId.trim() === ''); - if (noAppSessions.length > 0) { - // 找到最新的无应用对话 - const latestNoAppSession = noAppSessions.sort((a, b) => - new Date(b.createdTime).getTime() - new Date(a.createdTime).getTime() - )[0]; - currentSelectedSession.value = latestNoAppSession.conversationId; - - // 更新URL参数 - router.replace({ - path: '/', - query: { conversationId: latestNoAppSession.conversationId } - }); - } else { - // 没有无应用对话,创建新对话 - await generateSession(false); - } } else { - // 找到选中的应用信息 + // 选择新的 app const selectedApp = apps.value.find(app => app.appId === id); - // 检查应用是否已发布 if (selectedApp && !selectedApp.published) { - // 未发布应用不允许选择 - console.log('应用未发布,无法选择:', selectedApp); return; } - console.log('🔍 [DialogueAside] 选择app:', id); selectedAppId.value = id; user_selected_app.value = id; app.value.selectedAppId = id; app.value.appId = id; - // 🔑 新增:清空当前对话列表和选择状态 - const { conversationList } = storeToRefs(useSessionStore()); - conversationList.value = []; - currentSelectedSession.value = ''; + // 🔑 修复:不清空 currentSelectedSession,让 watch 决定是否需要切换 + // currentSelectedSession.value = ''; - // 🔑 修复:更新URL参数,清除conversationId确保不会嫁接无应用对话 router.replace({ path: '/', query: { appId: id, name: selectedApp?.name || '' - // 注意:不包含conversationId,确保完全切换到新app的独立历史 } }); - // 🔑 修复:重新加载该app的历史记录,如果没有记录会自动创建新对话 - console.log('🔍 [DialogueAside] 重新加载app历史记录'); + // 🔑 修复:不需要锁定 watch + // 刷新历史会话列表,后续由 watch 处理对话选择/创建 await getHistorySession(); } }; @@ -421,6 +435,148 @@ watch( }, ); +// 监听 user_selected_app 和 historySession 变化,自动创建或选择对话 +watch( + [user_selected_app, historySession], + async ([appId, sessions]) => { + + // 🔑 防止并发执行 + if (isWatchProcessing.value) { + return; + } + + // 🔑 关键修复:等待历史记录首次加载完成 + if (!isHistoryLoaded.value) { + return; + } + + // 如果没有选中的 app,不处理(由另一个 watch 处理无 app 模式) + if (!appId) { + return; + } + + // 等待 sessions 加载完成 + if (sessions === null || sessions === undefined) { + return; + } + + // 🔑 设置处理中标志 + isWatchProcessing.value = true; + + try { + // 🔑 关键修复:如果当前已经选中了属于该 app 的对话,不做任何操作 + if (currentSelectedSession.value) { + const currentSession = sessions.find( + s => s.conversationId === currentSelectedSession.value + ); + + // 当前对话已经属于该 app,无需切换或创建 + if (currentSession && currentSession.appId === appId) { + return; + } + } + + // 🔑 新架构:sessions 已经是过滤后的当前 app 的对话了,不需要再次过滤 + // historySession 是一个 computed 属性,根据 user_selected_app 自动返回对应 app 的对话 + + if (sessions.length > 0) { + // 有现有对话,切换到最新对话 + const latestAppSession = sessions.sort((a, b) => + new Date(b.createdTime).getTime() - new Date(a.createdTime).getTime() + )[0]; + + currentSelectedSession.value = latestAppSession.conversationId; + + // 更新路由,包含 conversationId + const selectedApp = apps.value.find(app => app.appId === appId); + router.replace({ + path: '/', + query: { + appId: appId, + name: selectedApp?.name || '', + conversationId: latestAppSession.conversationId + } + }); + } else { + // 该 app 没有现有对话,创建新对话 + await generateSession(false); + } + } finally { + // 🔑 确保标志被重置 + isWatchProcessing.value = false; + } + }, + { deep: true } +); + +// 监听无 app 模式(取消选择 app)时的对话处理 +watch( + [user_selected_app, historySession], + async ([appId, sessions]) => { + + // 🔑 防止并发执行 + if (isWatchProcessing.value) { + return; + } + + // 🔑 关键修复:等待历史记录首次加载完成 + if (!isHistoryLoaded.value) { + return; + } + + // 只处理无 app 的情况 + if (appId) { + return; + } + + // 等待 sessions 加载完成 + if (sessions === null || sessions === undefined) { + return; + } + + // 🔑 设置处理中标志 + isWatchProcessing.value = true; + + try { + // 🔑 关键修复:如果当前已经选中了无 app 的对话,不做任何操作 + if (currentSelectedSession.value) { + const currentSession = sessions.find( + s => s.conversationId === currentSelectedSession.value + ); + + // 当前对话已经是无 app 对话,无需切换或创建 + if (currentSession && (!currentSession.appId || currentSession.appId.trim() === '')) { + return; + } + } + + // 🔑 新架构:sessions 已经是过滤后的当前上下文(无 app)的对话了,不需要再次过滤 + // historySession 是一个 computed 属性,根据 user_selected_app 自动返回对应的对话 + + if (sessions.length > 0) { + // 有无 app 的现有对话,切换到最新对话 + const latestNoAppSession = sessions.sort((a, b) => + new Date(b.createdTime).getTime() - new Date(a.createdTime).getTime() + )[0]; + + currentSelectedSession.value = latestNoAppSession.conversationId; + + router.replace({ + path: '/', + query: { conversationId: latestNoAppSession.conversationId } + }); + } else { + // 没有无 app 的对话,创建新对话 + await generateSession(false); + } + } finally { + // 🔑 确保标志被重置 + isWatchProcessing.value = false; + } + }, + { deep: true } +); + // 监听路由参数变化,同步应用选择状态 watch( () => route.query.appId, @@ -459,6 +615,23 @@ watch( }, { immediate: true } ); + +// 🔑 新增:监听historySession变化,确保新创建的对话能立即在UI中显示 +watch( + () => historySession.value, + (newSessions, oldSessions) => { + // 检查是否有新的对话被添加 + if (newSessions.length > (oldSessions?.length || 0)) { + // 如果当前选中的对话在新的列表中,确保UI状态正确 + if (currentSelectedSession.value) { + const selectedExists = newSessions.some(session => + session.conversationId === currentSelectedSession.value + ); + } + } + }, + { deep: true } +);