From 87dd07ef230e87caba5aca4b728786bee5effcc6 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Fri, 16 Jan 2026 08:49:25 +0000 Subject: [PATCH] =?UTF-8?q?feat(ai-assistant):=20=E5=9F=BA=E4=BA=8E?= =?UTF-8?q?=E5=9C=BA=E6=99=AF=E6=A0=87=E8=AF=86=E7=AE=A1=E7=90=86=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 getSceneKey 函数,根据路由和实体生成唯一场景标识 - 会话初始化改为按 sceneKey 匹配历史记录,相同场景恢复会话 - 统一全局 AI 助手打开方式,manage.vue 通过事件触发 float-button - resumeSession 超时时间统一为 86400 秒(1天) --- .../components/AIAssistant/float-button.vue | 10 ++- .../js/components/AIAssistant/index.vue | 41 ++++++----- .../js/components/AIAssistant/page-context.js | 70 +++++++++++++++++++ resources/assets/js/components/SearchBox.vue | 2 +- resources/assets/js/pages/manage.vue | 7 +- 5 files changed, 102 insertions(+), 28 deletions(-) diff --git a/resources/assets/js/components/AIAssistant/float-button.vue b/resources/assets/js/components/AIAssistant/float-button.vue index 30b307959..f79591669 100644 --- a/resources/assets/js/components/AIAssistant/float-button.vue +++ b/resources/assets/js/components/AIAssistant/float-button.vue @@ -26,7 +26,7 @@ import {mapState} from "vuex"; import emitter from "../../store/events"; import {withLanguagePreferencePrompt} from "../../utils/ai"; -import {getPageContext} from "./page-context"; +import {getPageContext, getSceneKey} from "./page-context"; export default { name: 'AIAssistantFloatButton', @@ -146,10 +146,12 @@ export default { mounted() { this.loadPosition(); window.addEventListener('resize', this.onResize); + emitter.on('openAIAssistantGlobal', this.onClick); }, beforeDestroy() { window.removeEventListener('resize', this.onResize); + emitter.off('openAIAssistantGlobal', this.onClick); document.removeEventListener('mousemove', this.onMouseMove); document.removeEventListener('mouseup', this.onMouseUp); document.removeEventListener('contextmenu', this.onContextMenu); @@ -358,10 +360,14 @@ export default { * 点击按钮 */ onClick() { + const routeParams = this.$route?.params || {}; + const sceneKey = getSceneKey(this.$store, routeParams); + emitter.emit('openAIAssistant', { displayMode: 'chat', sessionKey: 'global', - resumeSession: 300, + sceneKey, + resumeSession: 86400, showApplyButton: false, onBeforeSend: this.handleBeforeSend, }); diff --git a/resources/assets/js/components/AIAssistant/index.vue b/resources/assets/js/components/AIAssistant/index.vue index c309f3cf5..215166607 100644 --- a/resources/assets/js/components/AIAssistant/index.vue +++ b/resources/assets/js/components/AIAssistant/index.vue @@ -217,6 +217,7 @@ export default { sessionStore: {}, currentSessionKey: 'default', currentSessionId: null, + currentSceneKey: null, sessionCacheKey: 'aiAssistant.sessions', maxSessionsPerKey: 20, } @@ -343,7 +344,7 @@ export default { this.pendingAutoSubmit = !!params.autoSubmit; // 会话管理 - this.initSession(params.sessionKey, params.resumeSession); + this.initSession(params.sessionKey, params.sceneKey, params.resumeSession); this.showModal = true; this.fetchModelOptions(); @@ -1067,45 +1068,45 @@ export default { /** * 初始化会话 * @param {string} sessionKey - 会话场景标识,不传则不启用会话管理 - * @param {boolean|number} resumeSession - 恢复上次会话:true 总是恢复,数字表示秒数阈值(上次会话在该时间内则恢复) + * @param {string} sceneKey - 场景标识,用于判断是否恢复会话 + * @param {number} resumeTimeout - 恢复超时时间(秒),默认1天 */ - initSession(sessionKey, resumeSession = false) { + initSession(sessionKey, sceneKey = null, resumeTimeout = 86400) { // 保存当前会话 if (this.responses.length > 0) { this.saveCurrentSession(); } this.sessionEnabled = !!sessionKey; + this.currentSceneKey = sceneKey; if (this.sessionEnabled) { this.currentSessionKey = sessionKey; - if (resumeSession) { + // 如果传入了 sceneKey,从历史中查找相同场景的最新会话 + if (sceneKey) { const sessions = this.getSessionList(sessionKey); - if (sessions.length > 0) { - const lastSession = sessions[0]; - // 如果是数字,检查时间阈值 - if (typeof resumeSession === 'number') { - const elapsed = (Date.now() - lastSession.updatedAt) / 1000; - if (elapsed > resumeSession) { - // 超过阈值,创建新会话 - this.currentSessionId = this.generateSessionId(); - this.responses = []; - return; - } + // 找到相同场景标识的最新一条记录 + const matchedSession = sessions.find(s => s.sceneKey === sceneKey); + if (matchedSession) { + const elapsed = (Date.now() - matchedSession.updatedAt) / 1000; + // 在超时时间内则恢复 + if (elapsed <= resumeTimeout) { + this.currentSessionId = matchedSession.id; + this.responses = JSON.parse(JSON.stringify(matchedSession.responses)); + this.syncResponseSeed(); + return; } - this.currentSessionId = lastSession.id; - this.responses = JSON.parse(JSON.stringify(lastSession.responses)); - this.syncResponseSeed(); - return; } } + // 无匹配会话、超时或无 sceneKey,创建新会话 this.currentSessionId = this.generateSessionId(); this.responses = []; } else { this.currentSessionKey = 'default'; this.currentSessionId = null; + this.currentSceneKey = null; this.responses = []; } }, @@ -1142,6 +1143,7 @@ export default { id: this.currentSessionId, title: this.generateSessionTitle(this.responses), responses: JSON.parse(JSON.stringify(this.responses)), + sceneKey: this.currentSceneKey, createdAt: existingIndex > -1 ? sessions[existingIndex].createdAt : Date.now(), updatedAt: Date.now(), }; @@ -1172,6 +1174,7 @@ export default { this.saveCurrentSession(); } this.currentSessionId = session.id; + this.currentSceneKey = session.sceneKey || null; this.responses = JSON.parse(JSON.stringify(session.responses)); this.syncResponseSeed(); this.scrollResponsesToBottom(); diff --git a/resources/assets/js/components/AIAssistant/page-context.js b/resources/assets/js/components/AIAssistant/page-context.js index 96f158879..1599e7926 100644 --- a/resources/assets/js/components/AIAssistant/page-context.js +++ b/resources/assets/js/components/AIAssistant/page-context.js @@ -313,3 +313,73 @@ function getDefaultContext() { systemPrompt: '', }; } + +/** + * 获取当前场景的唯一标识 + * 用于判断打开 AI 助手时是否需要新建会话 + * 场景相同则恢复上次会话,场景不同则新建会话 + * + * @param {Object} store - Vuex store 实例 + * @param {Object} routeParams - 路由参数 + * @returns {string} 场景标识,格式如 "routeName/entityType:entityId" + */ +export function getSceneKey(store, routeParams = {}) { + const routeName = store.state.routeName; + const parts = [routeName || 'unknown']; + + switch (routeName) { + case 'manage-project': { + const project = store.getters.projectData; + if (project?.id) { + parts.push(`project:${project.id}`); + } + break; + } + case 'manage-messenger': { + const dialogId = store.state.dialogId; + if (dialogId) { + parts.push(`dialog:${dialogId}`); + } + break; + } + case 'single-task': + case 'single-task-content': { + if (routeParams.taskId) { + parts.push(`task:${routeParams.taskId}`); + } + break; + } + case 'single-dialog': { + if (routeParams.dialogId) { + parts.push(`dialog:${routeParams.dialogId}`); + } + break; + } + case 'single-file': { + if (routeParams.codeOrFileId) { + parts.push(`file:${routeParams.codeOrFileId}`); + } + break; + } + case 'single-file-task': { + if (routeParams.fileId) { + parts.push(`file:${routeParams.fileId}`); + } + break; + } + case 'single-report-edit': { + if (routeParams.reportEditId) { + parts.push(`report:${routeParams.reportEditId}`); + } + break; + } + case 'single-report-detail': { + if (routeParams.reportDetailId) { + parts.push(`report:${routeParams.reportDetailId}`); + } + break; + } + } + + return parts.join('/'); +} diff --git a/resources/assets/js/components/SearchBox.vue b/resources/assets/js/components/SearchBox.vue index b18999f99..6ceab48fe 100755 --- a/resources/assets/js/components/SearchBox.vue +++ b/resources/assets/js/components/SearchBox.vue @@ -581,7 +581,7 @@ export default { const keyword = this.searchKey.trim(); emitter.emit('openAIAssistant', { sessionKey: 'ai-search', - resumeSession: 300, + resumeSession: 86400, title: this.$L('AI 搜索'), value: keyword, placeholder: this.$L('请描述你想搜索的内容...'), diff --git a/resources/assets/js/pages/manage.vue b/resources/assets/js/pages/manage.vue index 090a03861..877319fac 100644 --- a/resources/assets/js/pages/manage.vue +++ b/resources/assets/js/pages/manage.vue @@ -1094,12 +1094,7 @@ export default { }, onOpenAIAssistant() { - emitter.emit('openAIAssistant', { - displayMode: 'chat', - sessionKey: 'global', - resumeSession: 300, - showApplyButton: false, - }); + emitter.emit('openAIAssistantGlobal'); }, onAddShow() {