Files
dsh_shenxian/scripts/verify-inject.cjs
T
admin e18eaa2b64 fix(proxy): HTML 外壳也下发 no-cache —— 修「Failed to load plugins」根因(档案 95)
症状:用户页面报
  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)。
2026-09-14 21:50:28 +08:00

79 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* verify-inject.cjs —— 注入脚本校验(**档案 81 · R1 起改为校验独立文件**)
*
* 历史(2026-09-13 事故):注入脚本原先是 `proxy.ts` 里的 **TS 模板字面量**,里面的 `\n` 会在
* 模板求值时先被转义 ⇒ 注入浏览器的那段 JS 变 SyntaxError ⇒ **整段脚本静默不执行**(浮层/自愈/助手全废)。
* 当时"校验"只抽原始文本跑 new Function,**跳过了求值** ⇒ 假绿。
*
* 现在(R1):脚本已外置到 `assets/inject/*.js`(纯 JS),本脚本负责:
* ① 断言这些文件存在、非空、且能通过 `node --check`(等价于浏览器解析);
* ② 断言 `src/supervisor/proxy.ts` **不再**把注入脚本内联成模板字面量(防回退);
* ③ 断言运行时串里不含 `<script` / `</script>`(会提前结束注入的 script 标签)。
*
* 用法:node scripts/verify-inject.cjs 退出码 0=合格 / 1=不合格
*/
const fs = require('fs')
const os = require('os')
const path = require('path')
const cp = require('child_process')
const ROOT = path.join(__dirname, '..')
const DIR = path.join(ROOT, 'assets', 'inject')
const PROXY_SRC = path.join(ROOT, 'src', 'supervisor', 'proxy.ts')
let bad = 0
const files = fs.existsSync(DIR) ? fs.readdirSync(DIR).filter((f) => f.endsWith('.js')) : []
console.log('=== 注入脚本(' + DIR + ',' + files.length + ' 个)===')
if (files.length === 0) { console.log(' ✗ assets/inject 下没有 .js(档案 81 R1 要求注入脚本外置)'); bad++ }
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'vinj-'))
for (const f of files) {
const abs = path.join(DIR, f)
const code = fs.readFileSync(abs, 'utf8')
if (code.trim().length < 100) { console.log(' ✗ ' + f + ':内容过短,疑似被清空'); bad++; continue }
const t = path.join(tmp, f)
fs.writeFileSync(t, code)
const r = cp.spawnSync(process.execPath, ['--check', t], { encoding: 'utf8' })
if (r.status !== 0) {
console.log(' ✗ ' + f + ':**语法失败**(浏览器里会静默失效)→ ' + String(r.stderr || '').split('\n').slice(0, 2).join(' '))
bad++
} else {
const danger = ['</script', '<script'].filter((k) => code.includes(k))
if (danger.length) { console.log(' ✗ ' + f + ':含 ' + danger.join('/') + '(会提前结束注入的 script 标签)'); bad++ }
else console.log(' ✓ ' + f + ' 语法通过(' + code.length + ' 字符)')
}
}
fs.rmSync(tmp, { recursive: true, force: true })
// 防回退:proxy.ts 不得再内联注入脚本
if (fs.existsSync(PROXY_SRC)) {
const src = fs.readFileSync(PROXY_SRC, 'utf8')
const inlined = /const SESSION_[A-Z_]+ = `/g.test(src)
if (inlined) { console.log(' ✗ proxy.ts 仍把注入脚本内联成模板字面量(应改为 loadInject 读 assets/inject)'); bad++ }
else {
const loads = (src.match(/loadInject\('([^']+)'\)/g) || []).length
console.log(' ✓ proxy.ts 已走 loadInject(' + loads + ' 处)')
if (loads < files.length) { console.log(' ⚠️ loadInject 处数(' + loads + ') < 文件数(' + files.length + '),确认是否漏用'); }
}
}
// 防回退(档案 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)