mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-22 17:58:11 +00:00
feat(ai-assistant): 基于场景标识管理会话恢复
- 新增 getSceneKey 函数,根据路由和实体生成唯一场景标识 - 会话初始化改为按 sceneKey 匹配历史记录,相同场景恢复会话 - 统一全局 AI 助手打开方式,manage.vue 通过事件触发 float-button - resumeSession 超时时间统一为 86400 秒(1天)
This commit is contained in:
parent
0cefb7eaff
commit
87dd07ef23
@ -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,
|
||||
});
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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('/');
|
||||
}
|
||||
|
||||
@ -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('请描述你想搜索的内容...'),
|
||||
|
||||
@ -1094,12 +1094,7 @@ export default {
|
||||
},
|
||||
|
||||
onOpenAIAssistant() {
|
||||
emitter.emit('openAIAssistant', {
|
||||
displayMode: 'chat',
|
||||
sessionKey: 'global',
|
||||
resumeSession: 300,
|
||||
showApplyButton: false,
|
||||
});
|
||||
emitter.emit('openAIAssistantGlobal');
|
||||
},
|
||||
|
||||
onAddShow() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user