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 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,
});

View File

@ -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();

View File

@ -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('/');
}

View File

@ -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('请描述你想搜索的内容...'),

View File

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