|
|
@@ -203,6 +203,7 @@
|
|
|
import { getScheduleList } from '/@/views/ventAI/manageAssistent/api';
|
|
|
import type { AttachedFile, Message, ScheduleTask, SubAppRoute, Task } from '/@/views/ventAI/manageAssistent/types/index';
|
|
|
import { buildCapsuleFiles, buildCapsuleLinks, buildCapsuleAgents } from '/@/views/ventAI/manageAssistent/lib/capsule';
|
|
|
+ import { extractMarkdownLinks } from '/@/views/ventAI/manageAssistent/lib/index';
|
|
|
import { parseModalPreviewLink, isFileDownloadLink, downloadLinkFile } from '/@/views/ventAI/manageAssistent/lib/index';
|
|
|
import { locateDevice, locateDeviceReport, locateTunnel, onLocateResult } from '/@/views/ventAI/manageAssistent/lib/modalPreviewBridge';
|
|
|
import { useChatStream } from '../hooks/useChatStream';
|
|
|
@@ -321,15 +322,13 @@
|
|
|
route: SubAppRoute | null;
|
|
|
}
|
|
|
|
|
|
- // 标签关闭/切换到其他面板时模型只隐藏;超过该时长未再次显示才真正销毁
|
|
|
- const MODEL_PREVIEW_IDLE_MS = 5 * 60 * 1000;
|
|
|
+ // 模型在当前对话任务内常驻:标签切换、面板切换都只隐藏,切走任务或关闭助手时才销毁
|
|
|
const modelPreview = reactive<ModelPreviewCache>({
|
|
|
loaded: false,
|
|
|
ownerSessionId: '',
|
|
|
modelId: '',
|
|
|
route: null,
|
|
|
});
|
|
|
- let modelPreviewIdleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
let modelPreviewTransition: Promise<void> | null = null;
|
|
|
let pendingModelPreviewRoute: SubAppRoute | null = null;
|
|
|
let modelPreviewEpoch = 0;
|
|
|
@@ -349,17 +348,9 @@
|
|
|
return active?.type === 'model2d';
|
|
|
});
|
|
|
|
|
|
- const clearModelPreviewIdleTimer = () => {
|
|
|
- if (modelPreviewIdleTimer !== null) {
|
|
|
- clearTimeout(modelPreviewIdleTimer);
|
|
|
- modelPreviewIdleTimer = null;
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
const destroyModelPreview = () => {
|
|
|
modelPreviewEpoch++;
|
|
|
pendingModelPreviewRoute = null;
|
|
|
- clearModelPreviewIdleTimer();
|
|
|
modelPreview.loaded = false;
|
|
|
modelPreview.modelId = '';
|
|
|
modelPreview.route = null;
|
|
|
@@ -367,24 +358,21 @@
|
|
|
lastModelLocateSignature = '';
|
|
|
};
|
|
|
|
|
|
- const scheduleModelPreviewIdleDestroy = () => {
|
|
|
- clearModelPreviewIdleTimer();
|
|
|
- if (!modelPreview.loaded) return;
|
|
|
- modelPreviewIdleTimer = setTimeout(() => {
|
|
|
- modelPreviewIdleTimer = null;
|
|
|
- destroyModelPreview();
|
|
|
- }, MODEL_PREVIEW_IDLE_MS);
|
|
|
- };
|
|
|
-
|
|
|
/**
|
|
|
* 同一模型下只做定位,不重载 iframe:
|
|
|
* tunid → 巷道定位,locationId → 设备定位,locationName → 报表定位。
|
|
|
*/
|
|
|
+ const locateSignature = (route: SubAppRoute | null): string => {
|
|
|
+ const query = route?.query || {};
|
|
|
+ const modelId = query.model_id || modelPreview.modelId;
|
|
|
+ return [modelId, query.tunid || '', query.locationId || '', query.locationName || '', query.deviceType || ''].join('|');
|
|
|
+ };
|
|
|
+
|
|
|
const applyModelLocate = (route: SubAppRoute | null): boolean => {
|
|
|
const query = route?.query || {};
|
|
|
const modelId = query.model_id || modelPreview.modelId;
|
|
|
if (!modelId) return false;
|
|
|
- const signature = [modelId, query.tunid || '', query.locationId || '', query.locationName || '', query.deviceType || ''].join('|');
|
|
|
+ const signature = locateSignature(route);
|
|
|
if (signature === lastModelLocateSignature) return false;
|
|
|
lastModelLocateSignature = signature;
|
|
|
|
|
|
@@ -412,7 +400,7 @@
|
|
|
if (modelPreview.loaded && modelPreview.modelId === modelId) {
|
|
|
modelPreview.ownerSessionId = currentSessionKey();
|
|
|
// 同一个模型只做定位;定位参数也没变时界面不会有任何变化,明确提示一下
|
|
|
- if (!applyModelLocate(route)) message.info('当前显示的已是该模型');
|
|
|
+ applyModelLocate(route);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -427,12 +415,11 @@
|
|
|
|
|
|
if (modelPreview.loaded && modelPreview.modelId === targetModelId) {
|
|
|
modelPreview.ownerSessionId = currentSessionKey();
|
|
|
- if (!applyModelLocate(target)) message.info('当前显示的已是该模型');
|
|
|
+ applyModelLocate(target);
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
const epoch = modelPreviewEpoch;
|
|
|
- clearModelPreviewIdleTimer();
|
|
|
modelPreview.loaded = false;
|
|
|
// 先让旧 iframe 真正卸载,再挂载新模型,避免同帧复用组件导致旧模型残留
|
|
|
await nextTick();
|
|
|
@@ -446,7 +433,7 @@
|
|
|
|
|
|
// 子应用只靠模型 id 不会主动加载模型;首次打开必须补发一次链接里的定位消息,
|
|
|
// 由子应用在 locate 处理里完成“加载模型 + 定位”。等 ModalPreview 挂载/注册桥接后再发,
|
|
|
- // 未 ready 时桥接会自动排队,ready 后补发。
|
|
|
+ // 未 ready 时桥接会自动排队,ready 后补发。预加载时不发,等用户点链接再定位。
|
|
|
await nextTick();
|
|
|
applyModelLocate(target);
|
|
|
}
|
|
|
@@ -470,6 +457,9 @@
|
|
|
};
|
|
|
|
|
|
const openModelPreview = (route: SubAppRoute, title?: string) => {
|
|
|
+ // 面板本来就显示着同一模型、而且定位参数也没变 → 点下去界面上不会有任何变化,提示一下
|
|
|
+ const alreadyShowing = modelPreviewVisible.value && modelPreview.loaded && locateSignature(route) === lastModelLocateSignature;
|
|
|
+
|
|
|
const tab = openModalPreviewTab({ title: title || '二维模型', subAppRoute: route });
|
|
|
if (!tab) {
|
|
|
// 没有当前会话时开不了标签页,不提示的话点击就是“没反应”
|
|
|
@@ -477,19 +467,82 @@
|
|
|
return;
|
|
|
}
|
|
|
void ensureModelPreview(route);
|
|
|
+ if (alreadyShowing) message.info('当前显示的已是该模型');
|
|
|
};
|
|
|
|
|
|
+ /**
|
|
|
+ * 预加载二维模型:在后台把子应用和模型加载好,不打开标签页,用户点链接时直接可用。
|
|
|
+ * 必须连同定位一起发:子应用不是靠地址里的模型 id 主动加载,而是收到定位消息后才去加载模型,
|
|
|
+ * 只挂 iframe 不定位的话,真正点开时还是得等模型下载。
|
|
|
+ */
|
|
|
+ const preloadModelPreview = (route: SubAppRoute | null) => {
|
|
|
+ if (!props.visible || !route || !route.query?.model_id) return;
|
|
|
+ void ensureModelPreview(route);
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从已完成的 AI 回答里找模型预览链接(实时回答和历史消息都走这里)。
|
|
|
+ * 优先选带巷道参数的链接:设备链接的编号有可能本身就对不上(子应用会定位失败),
|
|
|
+ * 拿巷道链接预加载更稳,模型能确保真正加载起来。
|
|
|
+ */
|
|
|
+ const scanModelLinkRoute = (): SubAppRoute | null => {
|
|
|
+ const list = messages.value;
|
|
|
+ let fallback: SubAppRoute | null = null;
|
|
|
+ for (let i = list.length - 1; i >= 0; i--) {
|
|
|
+ const msg = list[i];
|
|
|
+ if (msg.type !== 'ai' || msg.isLoading) continue;
|
|
|
+ for (const link of extractMarkdownLinks(msg.content || '')) {
|
|
|
+ const route = parseModalPreviewLink(link.href);
|
|
|
+ if (!route || !route.query.model_id) continue;
|
|
|
+ if (route.query.tunid) return route;
|
|
|
+ if (!fallback) fallback = route;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return fallback;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 预加载防抖:流式输出时链接可能还不完整,等回答稳定下来再扫
|
|
|
+ let preloadTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
+ const scheduleModelPreload = () => {
|
|
|
+ if (!props.visible) return;
|
|
|
+ if (preloadTimer) clearTimeout(preloadTimer);
|
|
|
+ preloadTimer = setTimeout(() => {
|
|
|
+ preloadTimer = null;
|
|
|
+ preloadModelPreview(scanModelLinkRoute());
|
|
|
+ }, 1200);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 实时对话:最后一条回答内容变化;历史对话:消息加载完(条数变化);重新打开助手时也补一次
|
|
|
+ watch(
|
|
|
+ () => (messages.value.length ? messages.value[messages.value.length - 1].content : ''),
|
|
|
+ () => scheduleModelPreload()
|
|
|
+ );
|
|
|
+ watch(
|
|
|
+ () => messages.value.length,
|
|
|
+ () => scheduleModelPreload()
|
|
|
+ );
|
|
|
+ watch(
|
|
|
+ () => props.visible,
|
|
|
+ (visible) => {
|
|
|
+ if (visible) scheduleModelPreload();
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
// 子应用没能定位(例如链接里的设备在模型里找不到)时界面不会有任何变化,必须提示用户,
|
|
|
// 否则表现就是“点了没反应”
|
|
|
const offLocateResult = onLocateResult((result) => {
|
|
|
if (result.ok) return;
|
|
|
+ // 预加载时模型面板还没显示,失败不打扰用户;等用户真的点开(面板可见)再提示
|
|
|
+ if (!modelPreviewVisible.value) return;
|
|
|
message.warning(result.message || '未能在模型中找到该位置,请检查链接参数');
|
|
|
});
|
|
|
|
|
|
- // 隐藏后开始 5 分钟空闲倒计时;重新显示立即取消
|
|
|
- watch(modelPreviewVisible, (visible) => {
|
|
|
- if (visible) clearModelPreviewIdleTimer();
|
|
|
- else scheduleModelPreviewIdleDestroy();
|
|
|
+ // 切走当前对话任务时销毁模型(只保留当前任务的模型常驻);标签/面板切换只隐藏,不销毁
|
|
|
+ watch(currentTaskId, () => {
|
|
|
+ if (!modelPreview.loaded) return;
|
|
|
+ if (modelPreview.ownerSessionId && modelPreview.ownerSessionId !== currentSessionKey()) {
|
|
|
+ destroyModelPreview();
|
|
|
+ }
|
|
|
});
|
|
|
|
|
|
watch(
|
|
|
@@ -919,7 +972,7 @@
|
|
|
if (task.sessionId) {
|
|
|
await deleteSession(task.sessionId);
|
|
|
}
|
|
|
- // 删除的是当前已加载模型所属的会话:直接销毁模型(而不是等空闲超时)
|
|
|
+ // 删除的是当前已加载模型所属的会话:直接销毁模型
|
|
|
const ownerKey = task.sessionId || task.id;
|
|
|
if (modelPreview.ownerSessionId && modelPreview.ownerSessionId === ownerKey) {
|
|
|
destroyModelPreview();
|
|
|
@@ -1180,6 +1233,7 @@
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
offLocateResult();
|
|
|
+ if (preloadTimer) clearTimeout(preloadTimer);
|
|
|
// 移除挂载空壳,避免残留
|
|
|
portalWrapper.value?.remove();
|
|
|
portalWrapper.value = null;
|
|
|
@@ -1190,8 +1244,6 @@
|
|
|
activeFileReader.value.abort();
|
|
|
activeFileReader.value = null;
|
|
|
}
|
|
|
- // 清掉二维模型的空闲销毁定时器,避免卸载后仍回调
|
|
|
- clearModelPreviewIdleTimer();
|
|
|
});
|
|
|
</script>
|
|
|
|