mirror of
https://github.com/kuaifan/dootask.git
synced 2026-10-29 22:10:42 +00:00
fix(ai-assistant): 打开 AI 助手不再卡顿,切换场景不丢会话
- 打开助手不再等待历史列表加载,列表后台加载 - 会话列表接口只返回最近 20 条 - 浮动按钮点击判定放宽到 500ms 内移动小于 5px - 每个场景保存时只保留最近 200 条会话,超出的连同图片一起清理 - 切换场景前立即保存当前会话;原场景还在生成的回复照常写入并保存
This commit is contained in:
parent
1c0228f7e0
commit
af5fa5d828
@ -430,6 +430,7 @@ class AssistantController extends AbstractController
|
||||
$sessions = AiAssistantSession::where('userid', $user->userid)
|
||||
->where('session_key', $sessionKey)
|
||||
->orderByDesc('updated_at')
|
||||
->limit(20) // 与前端 maxSessionsPerKey 保持一致,避免全量返回拖慢打开
|
||||
->get();
|
||||
|
||||
$list = [];
|
||||
@ -512,6 +513,19 @@ class AssistantController extends AbstractController
|
||||
], $session?->id);
|
||||
$session->save();
|
||||
|
||||
// 每个场景最多保留 200 条会话,超出的按更新时间清理(含图片)
|
||||
$staleSessions = AiAssistantSession::where('userid', $user->userid)
|
||||
->where('session_key', $sessionKey)
|
||||
->orderByDesc('updated_at')
|
||||
->orderByDesc('id')
|
||||
->skip(200)
|
||||
->take(100)
|
||||
->get();
|
||||
foreach ($staleSessions as $stale) {
|
||||
$this->deleteSessionImages($stale);
|
||||
$stale->delete();
|
||||
}
|
||||
|
||||
// 仅返回本次新增的图片URL
|
||||
$urls = [];
|
||||
foreach ($newImageUrls as $imageId => $path) {
|
||||
|
||||
@ -327,8 +327,8 @@ export default {
|
||||
this.savePosition();
|
||||
this.dragging = false;
|
||||
|
||||
// 判断是否为点击(移动距离小于5px 且 按下时间小于200ms)
|
||||
if (moveDistance < 5 && duration < 200) {
|
||||
// 判断是否为点击(移动距离小于5px 且 按下时间小于500ms)
|
||||
if (moveDistance < 5 && duration < 500) {
|
||||
this.onClick();
|
||||
}
|
||||
|
||||
@ -402,8 +402,8 @@ export default {
|
||||
|
||||
this.dragging = false;
|
||||
|
||||
// 判断是否为点击(移动距离小于5px 且 按下时间小于200ms)
|
||||
if (moveDistance < 5 && duration < 200) {
|
||||
// 判断是否为点击(移动距离小于5px 且 按下时间小于500ms)
|
||||
if (moveDistance < 5 && duration < 500) {
|
||||
if (this.record.startCollapsed) {
|
||||
// 从细条点击:仅展开,统一离边 12,启动 5s 自动收起
|
||||
this.position.collapsed = false;
|
||||
|
||||
@ -345,6 +345,7 @@ export default {
|
||||
sessionCacheKeyPrefix: 'aiAssistant.sessions',
|
||||
maxSessionsPerKey: 20,
|
||||
sessionStoreLoaded: false,
|
||||
sessionStoreLoadingKey: null,
|
||||
|
||||
// 欢迎提示词(防抖更新,避免场景切换时连续刷新导致闪屏)
|
||||
displayWelcomePrompts: [],
|
||||
@ -382,6 +383,8 @@ export default {
|
||||
this.displayWelcomePrompts = getWelcomePrompts(this.$store, this.$route?.params || {});
|
||||
}, 100);
|
||||
this.saveSessionStoreDebouncedMap = {};
|
||||
// 已切走场景的会话列表(按 sessionKey),供后台流式继续写入并落库
|
||||
this.sessionStoreCache = {};
|
||||
},
|
||||
mounted() {
|
||||
emitter.on('openAIAssistant', this.onOpenAIAssistant);
|
||||
@ -536,8 +539,8 @@ export default {
|
||||
this.renderHook = params.onRender || null;
|
||||
this.pendingAutoSubmit = !!params.autoSubmit;
|
||||
|
||||
// 会话管理(不再按场景分桶,统一对话池)
|
||||
await this.initSession(params.sessionKey);
|
||||
// 会话管理(历史列表后台加载,不阻塞弹窗打开)
|
||||
this.initSession(params.sessionKey);
|
||||
|
||||
this.showModal = true;
|
||||
this.fetchModelOptions();
|
||||
@ -1123,8 +1126,13 @@ export default {
|
||||
: null;
|
||||
return {entry, isCurrent: false, session};
|
||||
}
|
||||
// 场景已切换,存储已被替换,放弃归位避免写串到别的场景
|
||||
return {entry: null, isCurrent: false, session: null};
|
||||
// 场景已切换,写回该场景缓存的会话列表
|
||||
const store = this.sessionStoreCache[owner.sessionKey] || [];
|
||||
const session = store.find(s => s.id === owner.sessionId) || null;
|
||||
const entry = session
|
||||
? (session.responses || []).find(r => r.localId === owner.localId) || null
|
||||
: null;
|
||||
return {entry, isCurrent: false, session, sessionKey: owner.sessionKey};
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1170,7 +1178,7 @@ export default {
|
||||
this.saveCurrentSession();
|
||||
} else if (target.session) {
|
||||
target.session.updatedAt = Date.now();
|
||||
this.saveSessionStore(target.session.id);
|
||||
this.saveSessionStore(target.session.id, target.sessionKey);
|
||||
}
|
||||
},
|
||||
|
||||
@ -1590,11 +1598,20 @@ export default {
|
||||
* 加载会话列表(按 sessionKey 桶过滤;浮窗固定 'global' 桶,专用入口用各自的桶)
|
||||
*/
|
||||
async loadSessionStore(sessionKey) {
|
||||
sessionKey = sessionKey || this.currentSessionKey;
|
||||
if (this.sessionStoreLoadingKey === sessionKey) {
|
||||
return;
|
||||
}
|
||||
this.sessionStoreLoadingKey = sessionKey;
|
||||
try {
|
||||
const {data} = await this.$store.dispatch("call", {
|
||||
url: 'assistant/session/list',
|
||||
data: {session_key: sessionKey || this.currentSessionKey},
|
||||
data: {session_key: sessionKey},
|
||||
});
|
||||
// 加载期间已切到其他桶,丢弃过期结果
|
||||
if (this.currentSessionKey !== sessionKey) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach(session => {
|
||||
if (Array.isArray(session.responses)) {
|
||||
@ -1612,15 +1629,26 @@ export default {
|
||||
Object.assign(this.serverImageMap, session.images);
|
||||
}
|
||||
});
|
||||
this.sessionStore = data;
|
||||
} else {
|
||||
this.sessionStore = [];
|
||||
// 保留加载期间本地新产生、服务端尚未落库的会话
|
||||
// 仍在后台流式生成的会话以本地为准,避免被服务端的中间态覆盖
|
||||
const local = this.sessionStore || [];
|
||||
const streamingIds = new Set(this.activeSSEClients
|
||||
.filter(item => item.sessionKey === sessionKey)
|
||||
.map(item => item.sessionId));
|
||||
const serverIds = new Set(data.map(s => s.id));
|
||||
const localOnly = local.filter(s => !serverIds.has(s.id));
|
||||
const merged = data.map(s => streamingIds.has(s.id) ? (local.find(l => l.id === s.id) || s) : s);
|
||||
this.sessionStore = localOnly.concat(merged).slice(0, this.maxSessionsPerKey);
|
||||
}
|
||||
this.sessionStoreLoaded = true;
|
||||
} catch (e) {
|
||||
// 失败不标记已加载,下次打开重试
|
||||
console.warn('[AIAssistant] 加载会话失败:', e);
|
||||
this.sessionStore = [];
|
||||
} finally {
|
||||
if (this.sessionStoreLoadingKey === sessionKey) {
|
||||
this.sessionStoreLoadingKey = null;
|
||||
}
|
||||
}
|
||||
this.sessionStoreLoaded = true;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1640,9 +1668,12 @@ export default {
|
||||
/**
|
||||
* 持久化当前场景的会话数据
|
||||
*/
|
||||
async saveSessionStore(sessionId = this.currentSessionId) {
|
||||
async saveSessionStore(sessionId = this.currentSessionId, sessionKey = this.currentSessionKey) {
|
||||
if (!sessionId) return;
|
||||
const session = this.sessionStore.find(s => s.id === sessionId);
|
||||
const store = sessionKey === this.currentSessionKey
|
||||
? this.sessionStore
|
||||
: (this.sessionStoreCache[sessionKey] || []);
|
||||
const session = store.find(s => s.id === sessionId);
|
||||
if (!session) return;
|
||||
|
||||
// 收集本次需要上传的新图片(在 imageCache 中有 base64 但 serverImageMap 中没有的)
|
||||
@ -1662,7 +1693,7 @@ export default {
|
||||
url: 'assistant/session/save',
|
||||
method: 'post',
|
||||
data: {
|
||||
session_key: this.currentSessionKey,
|
||||
session_key: sessionKey,
|
||||
session_id: session.id,
|
||||
title: session.title || '',
|
||||
data: this.sanitizeResponsesForPersist(session.responses),
|
||||
@ -1720,7 +1751,7 @@ export default {
|
||||
* 切换 sessionKey 时切换桶并保存当前会话
|
||||
* 不再按 scene_key 自动恢复:同一桶内打开时保留当前 session_id,没有则建新会话
|
||||
*/
|
||||
async initSession(sessionKey) {
|
||||
initSession(sessionKey) {
|
||||
// 专用入口未传 sessionKey 的兼容路径(不启用会话管理)
|
||||
if (!sessionKey) {
|
||||
this.sessionEnabled = false;
|
||||
@ -1733,21 +1764,33 @@ export default {
|
||||
|
||||
// 切到不同的 sessionKey 桶(浮窗 ↔ 专用入口):先保存当前会话
|
||||
const switchingBucket = this.sessionEnabled && this.currentSessionKey !== sessionKey;
|
||||
if (switchingBucket && this.responses.length > 0) {
|
||||
this.saveCurrentSession();
|
||||
if (switchingBucket) {
|
||||
if (this.responses.length > 0) {
|
||||
this.saveCurrentSession();
|
||||
}
|
||||
// 防抖保存依赖当前桶的 sessionStore/currentSessionKey,切桶前立即落库
|
||||
this.flushSessionStoreSaves();
|
||||
}
|
||||
|
||||
this.sessionEnabled = true;
|
||||
|
||||
if (switchingBucket || this.currentSessionKey !== sessionKey || !this.sessionStoreLoaded) {
|
||||
if (switchingBucket || this.currentSessionKey !== sessionKey) {
|
||||
// 切桶:旧桶列表暂存(后台流式仍可写入落库),换上目标桶的缓存,再清空当前会话指针
|
||||
if (Array.isArray(this.sessionStore) && this.sessionStore.length > 0) {
|
||||
this.sessionStoreCache[this.currentSessionKey] = this.sessionStore;
|
||||
}
|
||||
this.currentSessionKey = sessionKey;
|
||||
await this.loadSessionStore(sessionKey);
|
||||
// 切桶后清空当前会话指针,下面再决定新建/恢复
|
||||
this.sessionStore = this.sessionStoreCache[sessionKey] || [];
|
||||
delete this.sessionStoreCache[sessionKey];
|
||||
this.sessionStoreLoaded = false;
|
||||
if (switchingBucket) {
|
||||
this.currentSessionId = null;
|
||||
this.responses = [];
|
||||
}
|
||||
}
|
||||
if (!this.sessionStoreLoaded) {
|
||||
this.loadSessionStore(sessionKey);
|
||||
}
|
||||
|
||||
// 已有当前会话则保留;否则建一个空的新会话
|
||||
if (!this.currentSessionId) {
|
||||
@ -1820,6 +1863,13 @@ export default {
|
||||
fn();
|
||||
},
|
||||
|
||||
/**
|
||||
* 立即执行所有待保存的防抖任务
|
||||
*/
|
||||
flushSessionStoreSaves() {
|
||||
Object.values(this.saveSessionStoreDebouncedMap || {}).forEach(fn => fn.flush());
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载指定会话
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user