Преглед изворни кода

[Mod 0000] 修改AI助手关于二维面板的预加载逻辑

wangkeyi пре 4 дана
родитељ
комит
a7352d3248
1 измењених фајлова са 84 додато и 32 уклоњено
  1. 84 32
      src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

+ 84 - 32
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -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>