feat(ai-assistant): 基于场景标识管理会话恢复

- 新增 getSceneKey 函数,根据路由和实体生成唯一场景标识
  - 会话初始化改为按 sceneKey 匹配历史记录,相同场景恢复会话
  - 统一全局 AI 助手打开方式,manage.vue 通过事件触发 float-button
  - resumeSession 超时时间统一为 86400 秒(1天)
This commit is contained in:
kuaifan 2026-01-16 08:49:25 +00:00
parent 0cefb7eaff
commit 87dd07ef23
5 changed files with 102 additions and 28 deletions

View File

@ -26,7 +26,7 @@
import {mapState} from "vuex"; import {mapState} from "vuex";
import emitter from "../../store/events"; import emitter from "../../store/events";
import {withLanguagePreferencePrompt} from "../../utils/ai"; import {withLanguagePreferencePrompt} from "../../utils/ai";
import {getPageContext} from "./page-context"; import {getPageContext, getSceneKey} from "./page-context";
export default { export default {
name: 'AIAssistantFloatButton', name: 'AIAssistantFloatButton',
@ -146,10 +146,12 @@ export default {
mounted() { mounted() {
this.loadPosition(); this.loadPosition();
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
emitter.on('openAIAssistantGlobal', this.onClick);
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener('resize', this.onResize); window.removeEventListener('resize', this.onResize);
emitter.off('openAIAssistantGlobal', this.onClick);
document.removeEventListener('mousemove', this.onMouseMove); document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.onMouseUp); document.removeEventListener('mouseup', this.onMouseUp);
document.removeEventListener('contextmenu', this.onContextMenu); document.removeEventListener('contextmenu', this.onContextMenu);
@ -358,10 +360,14 @@ export default {
* 点击按钮 * 点击按钮
*/ */
onClick() { onClick() {
const routeParams = this.$route?.params || {};
const sceneKey = getSceneKey(this.$store, routeParams);
emitter.emit('openAIAssistant', { emitter.emit('openAIAssistant', {
displayMode: 'chat', displayMode: 'chat',
sessionKey: 'global', sessionKey: 'global',
resumeSession: 300, sceneKey,
resumeSession: 86400,
showApplyButton: false, showApplyButton: false,
onBeforeSend: this.handleBeforeSend, onBeforeSend: this.handleBeforeSend,
}); });

View File

@ -217,6 +217,7 @@ export default {
sessionStore: {}, sessionStore: {},
currentSessionKey: 'default', currentSessionKey: 'default',
currentSessionId: null, currentSessionId: null,
currentSceneKey: null,
sessionCacheKey: 'aiAssistant.sessions', sessionCacheKey: 'aiAssistant.sessions',
maxSessionsPerKey: 20, maxSessionsPerKey: 20,
} }
@ -343,7 +344,7 @@ export default {
this.pendingAutoSubmit = !!params.autoSubmit; this.pendingAutoSubmit = !!params.autoSubmit;
// //
this.initSession(params.sessionKey, params.resumeSession); this.initSession(params.sessionKey, params.sceneKey, params.resumeSession);
this.showModal = true; this.showModal = true;
this.fetchModelOptions(); this.fetchModelOptions();
@ -1067,45 +1068,45 @@ export default {
/** /**
* 初始化会话 * 初始化会话
* @param {string} sessionKey - 会话场景标识不传则不启用会话管理 * @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) { if (this.responses.length > 0) {
this.saveCurrentSession(); this.saveCurrentSession();
} }
this.sessionEnabled = !!sessionKey; this.sessionEnabled = !!sessionKey;
this.currentSceneKey = sceneKey;
if (this.sessionEnabled) { if (this.sessionEnabled) {
this.currentSessionKey = sessionKey; this.currentSessionKey = sessionKey;
if (resumeSession) { // sceneKey
if (sceneKey) {
const sessions = this.getSessionList(sessionKey); const sessions = this.getSessionList(sessionKey);
if (sessions.length > 0) { //
const lastSession = sessions[0]; const matchedSession = sessions.find(s => s.sceneKey === sceneKey);
// if (matchedSession) {
if (typeof resumeSession === 'number') { const elapsed = (Date.now() - matchedSession.updatedAt) / 1000;
const elapsed = (Date.now() - lastSession.updatedAt) / 1000; //
if (elapsed > resumeSession) { if (elapsed <= resumeTimeout) {
// this.currentSessionId = matchedSession.id;
this.currentSessionId = this.generateSessionId(); this.responses = JSON.parse(JSON.stringify(matchedSession.responses));
this.responses = []; this.syncResponseSeed();
return; return;
}
} }
this.currentSessionId = lastSession.id;
this.responses = JSON.parse(JSON.stringify(lastSession.responses));
this.syncResponseSeed();
return;
} }
} }
// sceneKey
this.currentSessionId = this.generateSessionId(); this.currentSessionId = this.generateSessionId();
this.responses = []; this.responses = [];
} else { } else {
this.currentSessionKey = 'default'; this.currentSessionKey = 'default';
this.currentSessionId = null; this.currentSessionId = null;
this.currentSceneKey = null;
this.responses = []; this.responses = [];
} }
}, },
@ -1142,6 +1143,7 @@ export default {
id: this.currentSessionId, id: this.currentSessionId,
title: this.generateSessionTitle(this.responses), title: this.generateSessionTitle(this.responses),
responses: JSON.parse(JSON.stringify(this.responses)), responses: JSON.parse(JSON.stringify(this.responses)),
sceneKey: this.currentSceneKey,
createdAt: existingIndex > -1 ? sessions[existingIndex].createdAt : Date.now(), createdAt: existingIndex > -1 ? sessions[existingIndex].createdAt : Date.now(),
updatedAt: Date.now(), updatedAt: Date.now(),
}; };
@ -1172,6 +1174,7 @@ export default {
this.saveCurrentSession(); this.saveCurrentSession();
} }
this.currentSessionId = session.id; this.currentSessionId = session.id;
this.currentSceneKey = session.sceneKey || null;
this.responses = JSON.parse(JSON.stringify(session.responses)); this.responses = JSON.parse(JSON.stringify(session.responses));
this.syncResponseSeed(); this.syncResponseSeed();
this.scrollResponsesToBottom(); this.scrollResponsesToBottom();

View File

@ -313,3 +313,73 @@ function getDefaultContext() {
systemPrompt: '', 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('/');
}

View File

@ -581,7 +581,7 @@ export default {
const keyword = this.searchKey.trim(); const keyword = this.searchKey.trim();
emitter.emit('openAIAssistant', { emitter.emit('openAIAssistant', {
sessionKey: 'ai-search', sessionKey: 'ai-search',
resumeSession: 300, resumeSession: 86400,
title: this.$L('AI 搜索'), title: this.$L('AI 搜索'),
value: keyword, value: keyword,
placeholder: this.$L('请描述你想搜索的内容...'), placeholder: this.$L('请描述你想搜索的内容...'),

View File

@ -1094,12 +1094,7 @@ export default {
}, },
onOpenAIAssistant() { onOpenAIAssistant() {
emitter.emit('openAIAssistant', { emitter.emit('openAIAssistantGlobal');
displayMode: 'chat',
sessionKey: 'global',
resumeSession: 300,
showApplyButton: false,
});
}, },
onAddShow() { onAddShow() {