Explorar el Código

[Fix R5] 第五轮审计整改:恢复色谱仪报表真实接口去mock + iframe sandbox + 空令牌头 + 资源/清理收口

- M-1(🔴)/M-11: bundleSpy 恢复 getbundleSpyInfoList 真实调用,删除写死的 10-02 mock 报表(含真实矿测点数据)与新增调试 console.log;reportVersion 走 setReportVersion
- M-2: WebPreviewPanel iframe 加 sandbox(allow-scripts/same-origin/forms/popups/downloads)
- M-3: streamPost 空令牌不再发送 X-Access-Token『null』字符串头,令牌可选语义不变
- M-4: fire.png 717KB/1080x1528 → 4.3KB/85x120(保持 RGBA 透明,唯一引用点不变)
- M-5: zhijiangSVG 搅拌 interval 增加 visibilitychange 暂停/恢复,监听对称移除
- M-7/M-8/M-9/M-12: 2D 模型非监测页签隐藏、green 轮询与融合预警补 catch、DwgPreview 缓存提升模块级带上限
- docs: docs/ai-changes/20261019-audit-r5-fix.md(§九 八章节)

基线 upstream/master @ e3d4295c;未推 master、未部署。
lizuo hace 2 semanas
padre
commit
1450a0535b

+ 134 - 0
docs/ai-changes/20261019-audit-r5-fix.md

@@ -0,0 +1,134 @@
+# 第五轮审计整改 · mky-vent-base(M-1 必修 + M-2/3/4/5 建议改 + M-7/8/9/12 低优收口)
+
+## ① 头部
+
+- **对照网址**:`http://39.97.59.228:8013/hrx/mky-vent-base/compare/master...fix/audit-r5-20261019`
+- **依据文档**:
+  - `tools/verify/代码审核-第五轮-mky-vent-base-20261017.md`(§1 分级台账 M-1~M-18、§2 三张清单)
+  - `AGENTS.md` §7.2(基座口径)/§7.4(资源与全局最优)/§九(改动说明随分支提交)
+- **分支**:`fix/audit-r5-20261019`(工作区直改,由 Lead 统一评审提交)
+- **基线**:`upstream/master` = `e3d4295c`(2026-10-16「AI助手增加打开网页链接面板」)
+- **日期**:2026-10-19(Asia/Shanghai)
+- **交付版本**:本分支首个提交(代码 + 本说明同一提交;hash 以 `git log` 为准,不写死自指 hash)
+- **状态**:**已本地提交**;推送 fork / 交付/浏览器验收由 Lead 统一执行;未部署
+
+## ② 提交清单
+
+| # | 类型 | 内容 |
+|---|---|---|
+| 1 | 功能提交 | 第五轮审计整改:M-1(🔴 恢复色谱仪报表真实接口、删除写死 mock)/M-2/M-3/M-4/M-5/M-7/M-8/M-9/M-11/M-12 + 本说明 |
+
+> 基线即 `upstream/master @ e3d4295c`,本分支无 merge 提交;`git log upstream/master..HEAD` 可核。
+
+## ③ 改动面
+
+| # | 文件 | 对应项 | 改了什么 |
+|---|---|---|---|
+| 1 | `src/views/vent/bundleSpy/bundleSpyTable/index.vue` | M-1/M-11 | 删除写死的 10-02 mock 报表对象(含真实矿测点数据),恢复 `getbundleSpyInfoList({ type: 'bundleSpy', ...params })` 真实调用;`reportVersion` 改由 `setReportVersion(res)` 解析(删除 `reportVersion.value = res.reportVersion ?? null` 写死分支与 else 分支);删除 `setReportVersion` 内本轮新增的两条调试 `console.log`(`:96/:98`);`:207` 既有 `console.log(posMonitor.value)` **保留不动** |
+| 2 | `src/views/ventAI/manageAssistent/components/resource/WebPreviewPanel.vue` | M-2 | iframe 加 `sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads"`;文件头注释由"不加 sandbox…要收紧的话…"更新为"已按此收紧"及未放行的越权项说明 |
+| 3 | `src/views/ventAI/manageAssistent/api/streamPost.ts` | M-3 | `'X-Access-Token': getToken()` 改为 `...(getToken() ? { 'X-Access-Token': getToken() } : {})`,令牌为空不再发送 `"null"` 字符串头;注释保留"令牌可选、未登录不拦截"语义 |
+| 4 | `src/assets/images/fire.png` | M-4 | 1080×1528 / 717,080 B → 85×120 / 4,278 B(保持 RGBA 透明,sharp 等比缩放 + pngquant 量化,视觉观感核对通过);唯一引用点 `FiberTempChart.vue` 经 `@/assets/images/fire.png` 引入,路径不变 |
+| 5 | `src/views/vent/monitorManager/groutMonitor/components/zhijiangSVG.vue` | M-5 | 320ms 搅拌 interval 增加页面可见性暂停:`visibilitychange` 隐藏时记录运行态并 `stopMixer()`,回前台且此前在运行则 `startMixer()`;`onMounted` 注册监听、`onUnmounted` 对称 `removeEventListener` + `stopMixer()`;默认行为(挂载即转)不变 |
+| 6 | `src/views/vent/monitorManager/gasPumpMonitor/index.vue` | M-7 | 2D 分支 `<component v-if="globalConfig?.is2DModel" …>` 增加 `v-show="activeKey == 'monitor'"`,与 3D 容器一致在非监测页签隐藏;**未**改 `v-if`,避免重挂载闪动 |
+| 7 | `src/views/vent/home/configurable/green/vent-green.vue` | M-8 | 轮询 `list({ types }).then(updateData)` 补 `.catch`(`console.error` 记日志,不弹全局错误) |
+| 8 | `src/views/vent/home/configurable/green/dust-green.vue` | M-8 | 轮询 `getDisHome({ dataList }).then(updateData)` 补 `.catch`(同上) |
+| 9 | `src/views/vent/home/configurable/green/fire-green.vue` | M-8 | 轮询 `getDisHome({ dataList }).then(updateData)` 补 `.catch`(同上) |
+| 10 | `src/views/vent/home/configurable/green/fusion-warn-green.vue` | M-9 | `setTimeout(async () => { try { await getCenterList(); } finally {...} })` 的 try 与 finally 之间补 `catch`(`console.error` 记日志),finally 重排逻辑不变 |
+| 11 | `src/views/ventAI/manageAssistent/components/resource/DwgPreview.vue` | M-12 | `svgUrlCache` 由组件实例级提升为**模块级**(新增独立 `<script lang="ts">` 块),带上限 20 条 FIFO(超出丢最旧,重复键刷新位置),键仍为 `file.id`;写入收口到 `cacheSvgUrl()`,注释更新为实际行为(只缓存服务端静态地址、绝不缓存 blob 地址;`releaseSvgBlob` 只释放当前实例临时 blob,二者互不干扰) |
+
+**明确未改(保持原样)**:M-6(M3 入口"可见但只读",产品决策)、M-10/M-13~M-18(仅记录级)、自动登录/免密冻结面、`.env`、`public/js/config.js`、`/modelreq` 相关——`git status --porcelain` 已核仅上述 11 个文件 + 本说明文档。
+
+## ④ 逐项说明
+
+### M-1(🔴 必修)色谱仪报表首屏恢复真实接口、删除 mock 报表
+
+- **原来的行为**:`65743bff0` 把 `getTableList()` 的真实接口注释,函数体返回写死的「10月2日色谱分析报表」对象(14 条布尔台采空区真实测点 CO/O₂/CH₄ 等数据,`reportVersion` 固定 `'2.0'`)。首屏默认加载走该函数;`getAllFile()` 失败/为空/慢时页面长期显示这份假数据且无错误提示;真实矿测点数据随前端源码与 dist 分发。
+- **现在的行为**:恢复 `const res = await getbundleSpyInfoList({ type: 'bundleSpy', ...params });`(与注释掉的调用及 `bundleSpy-table.api.ts` 签名一致),删除整个 mock 对象;`reportVersion` 由既有 `setReportVersion(res)` 解析(支持 `reportVersion/report_version/version` 三键,未返回时保留原值),不再写死 2.0、布尔台列定义与后端实际版本重新挂钩。
+- **为什么这么改**:mock 是"修复"临时绕过被误提交,属功能退化 + 业务敏感数据入库;真实接口恢复后首屏数据即真实数据。
+- **与原来的差别**:仅首屏数据来源变化(mock → 真实接口);`getTableList` 余下逻辑(content 解析、计数、百分比、图表)一字未动。`setReportVersion` 的两条调试 `console.log`(M-11)一并删除;`:207` 的既有 `console.log(posMonitor.value)` 按指示保留。
+
+### M-2(🟡)网页预览 iframe 加 sandbox
+
+- **原来的行为**:`<iframe>` 无 `sandbox`,在基座可信 UI 内直接加载模型输出/外部检索的任意 http(s) 链接(点击劫持/界面伪装面);文件头注释自述"先不做限制、要收紧加 sandbox 即可"。
+- **现在的行为**:按团队注释给定的收紧口加 `sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads"`。保留 `allow-same-origin` 是因地址多为内网系统,iframe 地址栏读取(`currentHref`)、登录态、下载、弹窗依赖它;未放 `allow-top-navigation` 等越权项。X-Frame-Options/CSP 拒绝时的白屏兜底("新窗口打开")不受影响。
+- **为什么这么改**:收紧第三方内容在基座内的能力面,成本一行、回归面仅限预览面板。
+- **与原来的差别**:仅 iframe 多一个 sandbox 属性 + 头注释同步为实际行为。
+
+### M-3(🟡)空令牌不再发送 `"null"` 字符串头
+
+- **原来的行为**:`'X-Access-Token': getToken()` 无条件写入;`getToken()` 无值返回 `null`,fetch 的 Headers 把 `null` 序列化成字符串 `"null"` 发出(不是缺省该头)。
+- **现在的行为**:`...(getToken() ? { 'X-Access-Token': getToken() } : {})`,令牌为空时该头缺省;**仍照常发请求**(后端有兜底账号)。
+- **为什么这么改**:避免后端把 `"null"` 当非法令牌判 401,导致"未登录走匿名兜底"场景(M1 设计语义)不可用。
+- **与原来的差别**:仅空令牌时的请求头差异;**保留"令牌可选、未登录不拦截"注释语义,未加任何登录拦截**。
+
+### M-4(🟡)fire.png 717KB 大图压缩
+
+- **原来的行为**:1080×1528 / 717,080 B 的 `fire.png` 仅作光纤测温图表 60×60 markPoint 图标(`FiberTempChart.vue:87`),无用大图进 dist(带宽 + 构建 imagemin 耗时)。
+- **现在的行为**:sharp 等比缩至 85×120(≤120px 目标内,2× 适配高分屏)+ palette 量化,4,278 B,RGBA 透明保留、观感核对通过;引用路径不变,无需改代码。
+- **为什么这么改**:消除 §7.4 资源专项里唯一"否"项,收益零成本。
+- **与原来的差别**:仅二进制资产变化;本机无 ImageMagick/pngquant CLI,sharp 为临时目录 `npm install --no-save`(`/tmp/pngtest`),未污染仓库依赖。
+
+### M-5(🟡)注浆 2D SVG 搅拌 interval 可见性暂停
+
+- **原来的行为**:320ms `setInterval` 常驻逐相切 DOM,页面切后台仍跑(CSS 流动动画后台自动节流的收益被抵消)。
+- **现在的行为**:`document.hidden` 时记录"隐藏前是否在运行"并 `stopMixer()`;回前台且此前在运行则 `startMixer()`。监听在 `onMounted` 注册、`onUnmounted` 对称 `removeEventListener` + `stopMixer()`。记录运行态是为了不顶掉外部 `animate({ running: false })` 的主动暂停。
+- **为什么这么改**:后台零收益的主线程开销直接清零,回归面仅限该组件动画。
+- **与原来的差别**:仅页面隐藏期间搅拌头停在第 1 相(`stopMixer` 原有语义);默认"挂载即转"不变。
+
+### M-7(🟢)gasPumpMonitor 2D 分支非监测页签隐藏
+
+- **原来的行为**:2D 分支无显隐控制,切到"历史/操作记录/故障记录"页签后 SVG 流动画仍在底层运行;3D 分支有 `v-show="activeKey == 'monitor'"`。
+- **现在的行为**:2D 分支加 `v-show="activeKey == 'monitor'"`,与 3D 对齐。
+- **为什么这么改**:页签一致性 + 历史页签下不为不可见的动画耗主线程。
+- **与原来的差别**:仅非监测页签下 2D SVG 不可见;**用 `v-show` 而非改 `v-if`**,无重挂载闪动。
+
+### M-8(🟢)green 系列轮询补 `.catch`
+
+- **原来的行为**:`vent-green`(60s 轮询)、`dust-green`/`fire-green`(2s 轮询)的 `.then(updateData)` 无 catch,接口失败产生 unhandled rejection。
+- **现在的行为**:补 `.catch`,内只 `console.error` 记日志,**不弹全局错误**(避免 dust/fire 2s 一轮的失败弹窗风暴);轮询间隔、清理(`stopInterval`/`unmounted`)逻辑不变。
+
+### M-9(🟢)fusion-warn-green 重试链补 catch
+
+- **原来的行为**:`setTimeout(async () => { try { await getCenterList(); } finally { getMonitor(false) } })` 失败时 unhandled rejection(重试链靠 finally 续排,本身不断)。
+- **现在的行为**:try 与 finally 之间补 `catch`(`console.error` 记日志);**finally 重排逻辑一字未动**,失败仍按 20s 续下一轮。
+
+### M-12(🟢)DwgPreview svgUrlCache 提升为模块级带上限 Map
+
+- **原来的行为**:`svgUrlCache` 在 setup 内为实例级,注释声称"关掉再打开复用"不成立(面板实例随标签切换重建,同图纸重开重新上传转换)。
+- **现在的行为**:缓存提升为模块级(独立 `<script lang="ts">` 块),上限 20 条 FIFO 丢最旧(重复键刷新插入序位置,防热点图纸被误淘汰),键仍为 `file.id`。blob 生命周期:缓存**只存服务端静态地址**(`buildStaticFileUrl(result.svgFile)`),绝不存 blob 地址——blob 由具体实例 `URL.createObjectURL` 创建、随 `releaseSvgBlob`(换文件/卸载)释放,二者互不干扰,既不产生悬空 URL 也不泄漏。
+- **为什么这么改**:同图纸关开/切标签省掉"重新上传 + 重新转换"(转换接口超时已放宽到 120s,是真实昂贵开销);上限 20 条静态 URL 字符串的内存代价可忽略。
+- **与原来的差别**:同一份 dwg 在会话内第二次打开命中缓存直接取回渲染;`load(force=true)` 重试仍绕开缓存。
+
+## ⑤ 验收证据
+
+1. **代码层**:`git diff e3d4295c -- src/views/vent/bundleSpy/bundleSpyTable/index.vue` 逐 hunk 核对——mock 对象(原 `:136-147` 共 12 行含 1 行超长 content)整段删除,恢复单行真实调用;`grep -n "10月2日\|2109495072969183233" src/views/vent/bundleSpy/bundleSpyTable/index.vue` **0 命中**(mock 数据与真实矿测点数据清零)。
+2. **对称性核对**:`zhijiangSVG.vue` 新增 `visibilitychange` 监听注册/移除一一对应(`onMounted`/`onUnmounted`);green 系列、fusion-warn 未新增任何定时器/监听。
+3. **fire.png**:`identify`(sharp metadata)核对 85×120 RGBA,`read_image` 目检观感正常;`grep -rn "fire.png" src` 仅 `FiberTempChart.vue:23` 一处引用,路径不变。
+4. **未浏览器实测**:本仓为工作区整改,未构建未部署(8092 构建按约定在 88 机,由 Lead 安排);sandbox iframe 的实际加载行为(内网系统登录态)建议部署后抽一条 AI 网页链接目验。
+5. 验收中**未发现**需回修的自身缺陷。
+
+## ⑥ 静态检查
+
+| 检查 | 命令 | 结果 |
+|---|---|---|
+| ESLint(ventAI 3 文件) | `pnpm exec eslint streamPost.ts DwgPreview.vue WebPreviewPanel.vue` | **0 问题,退出码 0** |
+| ESLint(zhijiangSVG.vue) | `pnpm exec eslint zhijiangSVG.vue` | **0 问题,退出码 0** |
+| ESLint(其余 5 个 vue) | `pnpm exec eslint bundleSpyTable/index.vue gasPumpMonitor/index.vue vent-green.vue dust-green.vue fire-green.vue fusion-warn-green.vue` | 大量 **既有** 报错(6 文件合计 709 条 prettier/prettier 缩进类 + `vue/multi-word-component-names` + `gasPumpMonitor/index.vue:145` TSX 风格类型断言解析错误),**基线(stash 后)同口径 721 条,报错类别与文件范围完全一致**——团队代码整体不按该 eslint 配置缩进,属既有配置错配;改动行只涉及其中 prettier 换行类(vent-green `:62-65`、fusion-warn `:52-64`),为与文件内既有风格保持一致刻意未按 eslint 重排(改了反而扩大 diff、与邻行风格断裂)。**无新增错误类别** |
+| vue-tsc | `pnpm exec vue-tsc --noEmit`(590s 上限) | **未能完成**:改动态与基线态两次尝试均在约 10 分钟处被 SIGABRT(与项目记忆"本机 16G 跑不动全量 TS 检查/构建"一致,属环境内存限制,非本次改动引入);基线对照因 git index.lock 并发冲突未跑成。**替代验证**:改动文件全部通过 ESLint(含 TS 语法解析,`gasPumpMonitor` 的解析错误为基线既有);改动均为局部、类型收敛型(`...(cond ? {...} : {})`、Map 泛型显式、无新 any),未新增 TS 错误的风险极低 |
+
+**手工核对**:改动文件无新增硬编码敏感信息(无口令/内网 IP/token);无新增未清理定时器/监听(`visibilitychange` 对称移除);无新增调试 `console.log`(仅 M-1 要求删除的两条已删,新增的都是 M-8/M-9 要求的 `console.error` 错误日志)。
+
+## ⑦ 已知取舍
+
+- **M-4 若被问"为何本轮能改"**:本机原无 ImageMagick/pngquant/optipng/sharp(CLI 探测全空),但 pnpm store 里有 `pngquant-bin`/`optipng-bin` 二进制,且 `/tmp` 临时 `npm install sharp --no-save` 可用,故按"有工具"路径执行了压缩;未动仓库依赖清单。
+- **M-6 未改**:M3 管理入口"可见但只读"是团队产品决策(写操作仍禁用、后端 403 兜底仍在),按指示保持原样,需用户知悉。
+- **M-10/M-13~M-18 未改**:均为仅记录级(未知预警等级显示 `-`、Enter 换行、技能详情停用、初始化交错、任务归并启发式、MutationObserver 单例、既有 echarts 不 dispose、camera hooks 近重复),按指示不提案不施工。
+- **M-3 后端行为未实测**:空令牌不发头对 TfAgents 是**纯收敛**(后端此前收到 `"null"` 字符串,语义更差),无论后端原行为如何都不会劣化;后端对无头请求的实际处理仍以 88 环境实测为准。
+- **gasPumpMonitor 2D 隐藏方式**:按指示用 `v-show`,历史页签下 2D SVG DOM 仍在但不可见(与 3D 容器同语义);如后续要彻底停止其内部动画,需 2D 组件侧支持(本轮不做)。
+- **未跑 `pnpm build`**:按项目记忆(NAS 16G 悬崖边、8092 全量构建 OOM)明确禁止本机构建;构建验证在 88 机进行。
+
+## ⑧ 交付状态与上游同步
+
+- **交付状态**:**已本地提交**(代码 + 本说明同一提交);由 Lead 推送 fork 分支;**未推送/合并 master、未交付团队仓、未部署**。
+- **上游同步**:分支基线 = `upstream/master @ e3d4295c`,behind=0;本轮未改动任何与上游冲突的面。
+- 部署与浏览器验收(AI 网页链接 sandbox 实际加载、色谱仪报表首屏真实数据、green 首页失败静默)留待后续在 88 环境安排。

BIN
src/assets/images/fire.png


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 9
src/views/vent/bundleSpy/bundleSpyTable/index.vue


+ 8 - 3
src/views/vent/home/configurable/green/dust-green.vue

@@ -66,9 +66,14 @@
     if (unmounted) return Promise.resolve();
     const dataList = devicesTypes.value.concat('dustAllMineWarn').join(',');
     if (!dataList) return Promise.resolve();
-    return getDisHome({ dataList }).then((res) => {
-      if (!unmounted) updateData(res);
-    });
+    return getDisHome({ dataList })
+      .then((res) => {
+        if (!unmounted) updateData(res);
+      })
+      .catch((e) => {
+        // 轮询失败只记日志,不弹全局错误,避免每轮弹窗
+        console.error('[dust-green] 轮询实时数据失败:', e);
+      });
   }
 
   function stopInterval() {

+ 8 - 3
src/views/vent/home/configurable/green/fire-green.vue

@@ -96,9 +96,14 @@
     if (unmounted) return Promise.resolve();
     const dataList = devicesTypes.value.concat('fireAllMineWarn').join(',');
     if (!dataList) return Promise.resolve();
-    return getDisHome({ dataList }).then((res) => {
-      if (!unmounted) updateData(res);
-    });
+    return getDisHome({ dataList })
+      .then((res) => {
+        if (!unmounted) updateData(res);
+      })
+      .catch((e) => {
+        // 轮询失败只记日志,不弹全局错误,避免每轮弹窗
+        console.error('[fire-green] 轮询实时数据失败:', e);
+      });
   }
 
   function stopInterval() {

+ 3 - 0
src/views/vent/home/configurable/green/fusion-warn-green.vue

@@ -53,6 +53,9 @@
       if (unmounted) return;
       try {
         await getCenterList();
+      } catch (e) {
+        // 失败只记日志,不重排逻辑不变:finally 仍会续上下一轮
+        console.error('[fusion-warn-green] 获取中心数据失败:', e);
       } finally {
         if (!unmounted) {
           getMonitor(false);

+ 4 - 1
src/views/vent/home/configurable/green/vent-green.vue

@@ -59,7 +59,10 @@
   function fetchListData() {
     const types = getTypes();
     if (!types || unmounted) return Promise.resolve();
-    return list({ types }).then(updateData);
+    return list({ types }).then(updateData).catch((e) => {
+      // 轮询失败只记日志,不弹全局错误,避免每分钟一轮弹窗
+      console.error('[vent-green] 轮询实时数据失败:', e);
+    });
   }
 
   function stopInterval() {

+ 9 - 2
src/views/vent/monitorManager/gasPumpMonitor/index.vue

@@ -1,8 +1,15 @@
 <template>
   <div class="bg" style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; overflow: hidden">
     <a-spin :spinning="loading" />
-    <!-- 二维 SVG 模型(瓦斯抽采泵站,按泵运行状态点亮管路) -->
-    <component v-if="globalConfig?.is2DModel" ref="modelRef" :is="modelComponent" :device-type="currentDeviceType" style="z-index: 2" />
+    <!-- 二维 SVG 模型(瓦斯抽采泵站,按泵运行状态点亮管路);与 3D 一致,非监测页签时隐藏(不重挂载,只藏) -->
+    <component
+      v-if="globalConfig?.is2DModel"
+      ref="modelRef"
+      :is="modelComponent"
+      :device-type="currentDeviceType"
+      v-show="activeKey == 'monitor'"
+      style="z-index: 2"
+    />
     <!-- 三维模型容器(由 index.vue 的 mountedThree 初始化) -->
     <div v-else id="gasPump3D" v-show="activeKey == 'monitor'" style="width: 100%; height: 100%; position: absolute; overflow: hidden"> </div>
     <div

+ 14 - 0
src/views/vent/monitorManager/groutMonitor/components/zhijiangSVG.vue

@@ -3630,6 +3630,18 @@
     }
   }
 
+  // 页面切到后台时暂停搅拌 interval(后台无意义逐相改 DOM,CSS 动画本身浏览器也会节流);
+  // 回前台时若隐藏前在运行则恢复。记录隐藏前状态,避免把外部 animate({running:false}) 的暂停顶掉
+  let mixerRunningBeforeHidden = false;
+  function onMixerVisibilityChange() {
+    if (document.hidden) {
+      mixerRunningBeforeHidden = mixerTimer !== null;
+      if (mixerRunningBeforeHidden) stopMixer();
+    } else if (mixerRunningBeforeHidden) {
+      startMixer();
+    }
+  }
+
   // 初始化元素引用(管路 CSS 动画默认已在播放,这里把搅拌头也转起来)
   onMounted(() => {
     elementInfo.forEach((__, elementId) => {
@@ -3638,10 +3650,12 @@
         animationElements.set(elementId, el as HTMLElement);
       }
     });
+    document.addEventListener('visibilitychange', onMixerVisibilityChange);
     startMixer();
   });
 
   onUnmounted(() => {
+    document.removeEventListener('visibilitychange', onMixerVisibilityChange);
     stopMixer();
   });
 

+ 3 - 2
src/views/ventAI/manageAssistent/api/streamPost.ts

@@ -48,9 +48,10 @@ export const streamPost = async <E extends SseEventLike>(
       method: 'POST',
       body: payload,
       headers: {
-        // 令牌是「可选」语义:getToken() 为空时仍照常发请求(后端有兜底账号)。
+        // 令牌是「可选」语义:getToken() 为空时不放 X-Access-Token 头(空值会被序列化成字符串 "null"),
+        // 仍照常发请求(后端有兜底账号)。
         // 不要改成「未登录就拦截」——那会把未登录场景直接变成不可用。
-        'X-Access-Token': getToken(),
+        ...(getToken() ? { 'X-Access-Token': getToken() } : {}),
         ...contentType,
       },
       signal: options.signal,

+ 22 - 6
src/views/ventAI/manageAssistent/components/resource/DwgPreview.vue

@@ -18,6 +18,25 @@
   </div>
 </template>
 
+<script lang="ts">
+  // 模块级缓存(跨面板实例复用):已转换出的 svg 静态地址按文件 id 缓存。
+  // 面板实例会随标签切换重建(RightPanelTabs 没加 :key),同一份图纸关掉再打开很常见,
+  // 每次重开都重新上传转换一次没有意义——本次会话内转换结果是稳定的。
+  // 只缓存服务端静态地址,绝不缓存 blob 地址:blob 地址由具体实例创建并随其释放,
+  // 进了缓存就是悬空 URL;实例里的 releaseSvgBlob 只会释放这种未被缓存的临时 blob 地址。
+  const SVG_URL_CACHE_LIMIT = 20;
+  const svgUrlCache = new Map<string, string>();
+  const cacheSvgUrl = (id: string, url: string) => {
+    // 已存在则先删再插,刷新它在插入序里的位置,避免热点图纸被 FIFO 误淘汰
+    if (svgUrlCache.has(id)) svgUrlCache.delete(id);
+    svgUrlCache.set(id, url);
+    if (svgUrlCache.size > SVG_URL_CACHE_LIMIT) {
+      const oldest = svgUrlCache.keys().next().value as string | undefined;
+      if (oldest !== undefined) svgUrlCache.delete(oldest);
+    }
+  };
+</script>
+
 <script setup lang="ts">
   import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
   import { getToken } from '/@/utils/auth/index';
@@ -54,13 +73,9 @@
     return { transform: `translate(${tx}px, ${ty}px) scale(${k})` };
   });
 
-  // 已转换出的 svg 静态地址按文件缓存:面板实例会随标签切换重建(RightPanelTabs 没加 :key),
-  // 同一份图纸关掉再打开很常见,每次重开都重新上传转换一次没有意义——本次会话内转换结果是稳定的。
-  const svgUrlCache = new Map<string, string>();
-
   // 当前文件的上传体:转 SVG(首次与兜底)都要用,留着避免重复构造
   let uploadSource: File | null = null;
-  // 交给 <img> 的 blob 地址(svg 正文所在),换文件/卸载时必须释放
+  // 交给 <img> 的 blob 地址(svg 正文所在,临时产物,不进模块级缓存):换文件/卸载时必须释放
   let svgBlobUrl = '';
   // 请求序号:换文件或重试后,在途请求的响应回来时状态已经属于另一个文件,必须丢弃
   let loadToken = 0;
@@ -129,7 +144,8 @@
       if (token !== loadToken) return;
       const url = result?.svgFile ? buildStaticFileUrl(result.svgFile) : '';
       if (!url) throw new Error('转换结果缺少 svg 地址');
-      svgUrlCache.set(file.id, url);
+      // svgUrlCache 里只放服务端静态地址(见模块级注释),与 releaseSvgBlob 释放的临时 blob 地址互不干扰
+      cacheSvgUrl(file.id, url);
       loading.value = false;
       await loadSvgFromUrl(url, token);
     } catch (err) {

+ 11 - 4
src/views/ventAI/manageAssistent/components/resource/WebPreviewPanel.vue

@@ -18,7 +18,14 @@
     <div ref="stageRef" class="web-stage">
       <div v-if="loading" class="web-loading">正在加载页面…</div>
       <div class="web-viewport" :class="{ 'is-loading': loading }" :style="viewportStyle">
-        <iframe ref="frameRef" class="web-frame" :src="url" frameborder="0" @load="loading = false"></iframe>
+        <iframe
+          ref="frameRef"
+          class="web-frame"
+          :src="url"
+          frameborder="0"
+          sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads"
+          @load="loading = false"
+        ></iframe>
       </div>
     </div>
   </div>
@@ -32,9 +39,9 @@
   /**
    * 右侧面板的网页预览:http(s) 地址直接 iframe 嵌入(判定见 lib/webLink.ts,当前不区分站内站外)。
    *
-   * 不加 sandbox:地址多为内网系统,加了(尤其不带 allow-same-origin 时)会让它变成不透明来源,
-   * 登录态、下载、弹窗等能力都受影响。要收紧的话给 iframe 加
-   * sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads" 即可。
+   * 已按团队预留的收紧口给 iframe 加了 sandbox:地址多为内网系统,故保留
+   * allow-same-origin(登录态、地址可读)与 allow-scripts/forms/popups/downloads
+   * (页面功能、弹窗、下载),未放 allow-top-navigation 等越权项。
    */
   const props = defineProps<{ url: string }>();
 

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio