From e18eaa2b6434085d2c652221ac1d41cd25ac8fae Mon Sep 17 00:00:00 2001 From: maogeigei Date: Mon, 14 Sep 2026 21:50:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(proxy):=20HTML=20=E5=A4=96=E5=A3=B3?= =?UTF-8?q?=E4=B9=9F=E4=B8=8B=E5=8F=91=20no-cache=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E4=BF=AE=E3=80=8CFailed=20to=20load=20plugins=E3=80=8D?= =?UTF-8?q?=E6=A0=B9=E5=9B=A0=EF=BC=88=E6=A1=A3=E6=A1=88=2095=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 症状:用户页面报 client-modules: bundle script /plugins/??…&rev=… failed to load ⇒ 界面「Failed to load plugins」 根因(实测三条对照): · `GET /`(外壳 HTML)**没有任何缓存头**(无 Cache-Control/ETag/Last-Modified/Expires)⇒ 浏览器启发式缓存 · 外壳内嵌带**内容哈希 `rev`** 的插件 bundle URL;`rev`/模块列表与实例当前状态**必须完全一致**: 原样 200(11.17 MB)|只改 rev → **404**|rev 对但少一个模块 → **404** · 于是「改了插件 / 重启了实例」之后,旧外壳永远去请求**已不存在的 rev** ⇒ 404 ⇒ 报错, 且**普通刷新会命中缓存的外壳 ⇒ 复现不消失**(2026-09-14 事故:当天连铺 4 次插件 + 3 次重启 dshs) 修法:把既有的 `no-cache` 治理(2026-09-12 只覆盖 `/plugins/`、`/assets/`)**扩到 HTML 外壳**—— `String(headers['content-type']).includes('text/html')` 也下发 `Cache-Control: no-cache`。 实例端仍是唯一事实源,平台只加缓存头,不改 rev(R2)。 验证: · 服务器 `bash scripts/ci.sh` → CI OK(48 pass / 0 fail) · `scripts/verify-inject.cjs` 新增防回退断言「/plugins/ 与 text/html 都必须 no-cache」 · 经 nginx 公网路径实测 `GET /` → **cache-control: no-cache**(改前为空) · 端到端:取各用户页面里**自身**的 bundle URL 回拉 → admin/guest 均 200(11.69 / 11.13 MB) ⚠️ 本提交不能回溯治愈「已经坏在用户浏览器里的那份旧外壳」——用户需**强刷一次** (Ctrl/Cmd+Shift+R,或 DevTools 勾 Disable cache,或无痕窗口)。 ⚠️ 同批未提交(属别人 lane,见档案 95 §六):`BRIEF.md` / `DEPLOY-本部署.md` 的配额口径同步 (我改了它们的配额数字,但那两个文件本就有别人未提交的改动 ⇒ 不跟提,宁可保持 dirty)。 --- scripts/verify-inject.cjs | 16 ++++++++++++++++ src/supervisor/proxy.ts | 15 ++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/scripts/verify-inject.cjs b/scripts/verify-inject.cjs index 8cccb5d..29d451f 100644 --- a/scripts/verify-inject.cjs +++ b/scripts/verify-inject.cjs @@ -58,5 +58,21 @@ if (fs.existsSync(PROXY_SRC)) { } } +// 防回退(档案 95):proxy.ts 必须给**模块路径**与**HTML 外壳**都下发 `no-cache`。 +// 由来:`/` 原先不带任何缓存头 ⇒ 浏览器启发式缓存外壳;而外壳内嵌带内容哈希 `rev` 的 +// bundle URL ⇒ 插件集合一变/实例一重启,旧外壳就一直去请求**已不存在的 rev** ⇒ 实例按契约 404 +// ⇒ 界面「Failed to load plugins」,**普通刷新命中缓存的外壳、复现不消失**(2026-09-14 真实事故)。 +{ + const src = fs.readFileSync(PROXY_SRC, 'utf8') + const hasPlugins = /targetPath\.startsWith\('\/plugins\/'\)/.test(src) + const hasHtml = /includes\('text\/html'\)[\s\S]{0,200}cache-control/.test(src) || /text\/html[\s\S]{0,120}'no-cache'/.test(src) + if (!hasPlugins || !hasHtml) { + console.log(` ✗ proxy.ts 缓存治理不完整(模块路径 no-cache=${hasPlugins} / HTML 外壳 no-cache=${hasHtml})⇒ 插件一改用户就会"Failed to load plugins"`) + bad++ + } else { + console.log(' ✓ proxy.ts 缓存治理完整(/plugins/ 与 text/html 均 no-cache)') + } +} + console.log(bad ? '结论:' + bad + ' 项不合格 ❌ —— 别推上线' : '结论:全部合格 ✅') process.exit(bad ? 1 : 0) diff --git a/src/supervisor/proxy.ts b/src/supervisor/proxy.ts index 2de5e7f..0d9d633 100644 --- a/src/supervisor/proxy.ts +++ b/src/supervisor/proxy.ts @@ -379,8 +379,21 @@ function proxyHttp( // **允许缓存,但每次必须回源校验**(配合上游 ETag/Last-Modified 走 304,开销极小)。 // ⇒ bundle 一变,浏览器下一次请求就拿到新的,**用户无需清缓存/换无痕**。 // + // 2026-09-14(档案 95 · 追加):**HTML 外壳同样必须 no-cache**。 + // 实测根因:`/` 原先**不带任何缓存头**(无 Cache-Control / ETag / Last-Modified) + // ⇒ 浏览器对它走**启发式缓存**;而外壳里内嵌了带**内容哈希 `rev`** 的插件 bundle URL + // (见下方 `if` 之上那段注释)。插件集合一变、或实例重启重算 rev, + // 旧外壳就会一直去请求**已不存在的 rev** ⇒ 实例按契约返回 **404** + // ⇒ dsh 客户端报 `client-modules: bundle script … failed to load` ⇒ 界面「Failed to load plugins」, + // 且**普通刷新会命中缓存的外壳、复现不消失**(2026-09-14 真实事故:当天连铺 4 次插件 + 2 次重启平台)。 + // ⇒ 外壳加 no-cache 后,浏览器每次都会回源取到**当前**的外壳与 rev。 + // // ⚠️ 勿删:这是 UI 改动能否被验收的前置机制(`06-工作台UI规范 §6.5`)。 - if (targetPath.startsWith('/plugins/') || targetPath.startsWith('/assets/')) { + if ( + targetPath.startsWith('/plugins/') || + targetPath.startsWith('/assets/') || + String(headers['content-type'] ?? '').includes('text/html') + ) { headers['cache-control'] = 'no-cache' } // local mode only: DSH builds absolute URLs from the loopback Host we