fix(proxy): 治本 —— 陈旧 dsh-auth cookie 回写清理(档案 98,修 431 → Failed to load plugins)

根因(2026-09-14 实测):dsh **每次实例 (重)启动都换** `dsh-auth-<随机后缀>` 的 **cookie 名**;平台此前
只在**转发给实例时**丢弃旧的(`mergeCookieHeader`),**从不回写浏览器** ⇒ 浏览器 cookie jar **只增不减**
⇒ `Cookie` 请求头越涨越长 ⇒ 实测 **12.8 KB 放行 / 19.1 KB 就被 431**(且 19 KB 时连 `/` 也 431)
⇒ 被 Cloudflare / 本机 nginx 直接拒掉,**请求根本到不了平台** ⇒ 那条 11 MB 合并脚本取不到
⇒ 客户端报 `bundle script … failed to load` ⇒ 界面「Failed to load plugins」。
(旁证全对:平台日志无该请求 ✓/无痕正常=空 cookie 罐 ✓/服务器侧一切正常 ✓)

修法(只动 proxy.ts 一处):在响应头块里对**陈旧名字**回写 `Set-Cookie: <name>=; Path=/; Max-Age=0`。
· **只在「本次响应确实下发了 dsh-auth-*」时才动手** —— 那时才确知当前有效名字,绝不误删在用的那个;
· 其余情形什么都不做;不带 `Domain=`(dsh 下发的是 host-only cookie);
· 即使判断有误,档案 51 的「401 → 取新 cookie 透明重放」也会自愈。

验证:
· 本机 `npm run verify` EXIT=0;服务器 `ci.sh` CI OK(48 pass / 0 fail);
· `scripts/verify-inject.cjs` 新增防回退断言「会清理陈旧 dsh-auth cookie」;
· **端到端实测**:带 2 个假 `dsh-auth-*` 请求 `/?token=…` → 303 响应含 **3 条 Set-Cookie,其中 2 条 Max-Age=0**
  正是那两个假名字 ✅

⚠️ 它**救不了已经 431 的当下**(那时请求在 CF/nginx 就被拒、到不了平台)⇒ 用户需**先手动清一次**
(只删 `dsh-auth-*`、保留 `sid`),此后由本机制维持小 jar。⚠️ 根因仍在(官方每重启换名)⇒ **少重启**仍是要点。
This commit is contained in:
admin committed 2026-09-14 23:01:04 +08:00
1 parent cc1dc5c9a8
commit ed0c3c0ad5
2 files changed
+40

No files matched your search

+9
View File
@@ -84,6 +84,15 @@ if (fs.existsSync(PROXY_SRC)) {
} else {
console.log(' ✓ proxy.ts 有 /plugins/ ETag + 304 短路(省掉每次 11 MB 重下)')
}
// 档案 98:陈旧 dsh-auth cookie 必须**回写浏览器删除**(否则 Cookie 头只增不减,
// 涨到 ~15 KB 就被 431 拒在 CF/nginx,请求到不了平台 —— 见档案 98 实测)。
const hasStale = /const staleNames =/.test(src) && /Max-Age=0/.test(src) && /startsWith\('dsh-auth-'\)/.test(src)
if (!hasStale) {
console.log(' ✗ proxy.ts 缺「陈旧 dsh-auth cookie 回写清理」(Cookie 头会只增不减 ⇒ 431)')
bad++
} else {
console.log(' ✓ proxy.ts 会清理陈旧 dsh-auth cookie(防 Cookie 头无限增长)')
}
}
console.log(bad ? '结论:' + bad + ' 项不合格 ❌ —— 别推上线' : '结论:全部合格 ✅')
+31
View File
@@ -376,6 +376,37 @@ function proxyHttp(
return
}
// 重放成功后把新 cookie 交给浏览器:之后(含 SSE 自动重连)不再需要重放。
// ── 档案 98(治本):把**陈旧的 `dsh-auth-*` cookie 从浏览器里清掉** ──────────
// 由来(2026-09-14 实测):dsh **每次实例 (重)启动都换** `dsh-auth-<随机后缀>` 的 cookie 名
// (见本文件头部注释)。浏览器把每一个旧名都留着,而平台此前只在**转发给实例时**丢弃
// 旧的(`mergeCookieHeader`),**从不回写浏览器** ⇒ jar **只增不减** ⇒ `Cookie` 请求头
// 越涨越长 ⇒ 涨到 ~15 KB(实测 12.8 KB 放行 / 19.1 KB 已被 431)就被 Cloudflare /
// 本机 nginx 直接 **431** 拒掉 —— 请求**根本到不了平台** ⇒ 那条 11 MB 合并脚本取不到
// ⇒ 客户端报 `bundle script … failed to load`(界面「Failed to load plugins」)。
// ⚠️ 所以这一块**救不了已经 431 的当下**(那时请求进不来),它的作用是**防复发**:
// 用户手动清一次之后,jar 不会再长回去。
// 为什么只在「本次响应确实下发了 dsh-auth」时才动手:只有这时我们**确知当前有效的名字**,
// 才能安全清掉其余旧名;否则(例如普通 API 响应不带该 cookie)宁可什么都不做。
{
const scNow = headers['set-cookie']
const scList = Array.isArray(scNow) ? scNow : scNow === undefined ? [] : [String(scNow)]
const freshNames = scList
.map((c) => /^\s*(dsh-auth-[^=;\s]*)\s*=/.exec(String(c))?.[1] ?? '')
.filter((n) => n !== '')
if (freshNames.length > 0) {
const staleNames = (request.headers.cookie ?? '')
.split(';')
.map((x) => (x.split('=')[0] ?? '').trim())
.filter((n) => n.startsWith('dsh-auth-') && !freshNames.includes(n))
if (staleNames.length > 0) {
headers['set-cookie'] = [
...scList,
...staleNames.map((n) => `${n}=; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT`),
]
}
}
}
if (authCookie !== undefined && replayCookies.length > 0) {
const prev = headers['set-cookie']
headers['set-cookie'] = [