fix(proxy): /plugins/ 合并脚本补 ETag + 304 短路 —— 省掉每次 11 MB 重下(档案 97)
问题(实测 2026-09-14):官方 `@deepseek-ai/dsh-client-modules` 把全部客户端插件拼成**一条 11,172,365 字节的脚本**(combo URL `/plugins/??<模块列表>&rev=<revision>`);我们给它下发 `no-cache`(档案 95,为"改了 UI 就能看到"),而实例**既不给 ETag 也不给 Last-Modified** ⇒ 浏览器"回源校验"退化成**每次全量重下 11 MB** —— 经 Cloudflare 实测 **114.87 秒** (弱网/手机上直接表现为页面加载不出来)。 修法(只动 proxy.ts 一处,不改官方、不动 rev): · 按 URL 派生强校验器 `ETag = sha1(targetPath)`,**只对 GET/HEAD + /plugins/ 且 URL 含 `rev=` 生效** (无 rev 一律跳过)。依据是官方契约:「**已公告响应不可变;未知组合或 revision 返回 404**」 ⇒ (模块组合, rev) 唯一决定内容 ⇒ 同 URL 必同字节,故按 URL 派生 ETag 是安全的。 · 命中 `If-None-Match` 时 **直接回 304、完全不回源**(省的正是那 11 MB)。 · 未命中时行为与原先一致,只在既有缓存头区块多透出一个 `ETag`。 验证: · 本机 `npm run verify` EXIT=0;服务器 `ci.sh` CI OK(48 pass / 0 fail); · `scripts/verify-inject.cjs` 新增防回退断言「proxy.ts 有 /plugins/ ETag + 304 短路」; · **端到端实测**:① 首次 200 / 11,172,365 B + `ETag="034a459a92…"`;② 带 If-None-Match → **304 / 0 B** ✅ ⚠️ 顺带记录:本仓库 `core.autocrlf` 会在 git 触碰后把工作区文件改成 CRLF ⇒ 脚本化改文件必须 **行尾自适应**(本轮 `proxy.ts` 的多行匹配因此失配过一次)。
This commit is contained in:
1 parent
53b8eff870
commit
cc1dc5c9a8
2 files changed
+40
No files matched your search
@@ -72,6 +72,18 @@ if (fs.existsSync(PROXY_SRC)) {
|
||||
} else {
|
||||
console.log(' ✓ proxy.ts 缓存治理完整(/plugins/ 与 text/html 均 no-cache)')
|
||||
}
|
||||
// 档案 97:/plugins/ 必须有**由 URL 派生的 ETag + 304 短路** ——
|
||||
// 那条合并脚本 11 MB、未压缩,只靠 no-cache 会让浏览器**每次全量重下**
|
||||
// (实测经 CF 114.87 s)。rev 即修订标识 ⇒ 用 URL 派生 ETag 安全。
|
||||
const hasEtag = /const pluginEtag =/.test(src) && /createHash\('sha1'\).update\(targetPath\)/.test(src)
|
||||
const hasShort = /writeHead\(304, \{ etag: pluginEtag/.test(src)
|
||||
const etagOut = /headers\.etag = pluginEtag/.test(src)
|
||||
if (!hasEtag || !hasShort || !etagOut) {
|
||||
console.log(` ✗ proxy.ts 缺 /plugins/ 的 ETag 条件请求短路(etag=${hasEtag} 304短路=${short} 透出=${etagOut})`.replace('${short}', String(hasShort)))
|
||||
bad++
|
||||
} else {
|
||||
console.log(' ✓ proxy.ts 有 /plugins/ ETag + 304 短路(省掉每次 11 MB 重下)')
|
||||
}
|
||||
}
|
||||
|
||||
console.log(bad ? '结论:' + bad + ' 项不合格 ❌ —— 别推上线' : '结论:全部合格 ✅')
|
||||
|
||||
@@ -15,6 +15,7 @@ import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Agent, request as httpRequest, type IncomingHttpHeaders, type IncomingMessage } from 'node:http'
|
||||
import { connect } from 'node:net'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { hashSessionToken, parseCookie } from '../web/auth.js'
|
||||
import { requireAuth } from '../web/middleware/authn.js'
|
||||
import type { Endpoint } from './spawner.js'
|
||||
@@ -275,6 +276,31 @@ function proxyHttp(
|
||||
let authRetryUsed = false
|
||||
let replayCookies: string[] = []
|
||||
|
||||
// ── 档案 97:`/plugins/` 合并脚本的**条件请求短路**(ETag → 304)──────────────
|
||||
// 背景(实测 2026-09-14):那条把全部客户端插件拼起来的脚本 **11,172,365 B、未压缩**,
|
||||
// 而我们对它下发 `no-cache`(档案 95,为"改了 UI 就能看到");实例既不给 `ETag`
|
||||
// 也不给 `Last-Modified` ⇒ 浏览器"回源校验"退化成**每次全量重下 11 MB**
|
||||
// (经 Cloudflare 实测 **114.87 s**;弱网/手机上直接表现为页面加载不出来)。
|
||||
// 本块 = 平台侧补一个**由 URL 派生**的强校验器(不改官方、不改实例、不动 rev):
|
||||
// · 官方 `@deepseek-ai/dsh-client-modules` 的契约是
|
||||
// 「**已公告响应不可变;未知组合或 revision 返回 404**」⇒ `(模块组合, rev)` 唯一决定内容,
|
||||
// 同一 URL 永远同一份字节 ⇒ 用 URL 派生 ETag 是**安全**的;
|
||||
// · 只对 **GET/HEAD + `/plugins/` 且 URL 含 `rev=`** 生效(无 rev 的一律跳过,绝不冒险);
|
||||
// · 命中 `If-None-Match` 时**直接回 304、完全不回源**(省掉的正是那 11 MB)。
|
||||
// · 未命中时行为与原先一致,只多一个 `ETag` 响应头(见响应头区块)。
|
||||
const pluginEtag =
|
||||
(reqMethod === 'GET' || reqMethod === 'HEAD') && targetPath.startsWith('/plugins/') && targetPath.includes('rev=')
|
||||
? '"' + createHash('sha1').update(targetPath).digest('hex') + '"'
|
||||
: undefined
|
||||
if (pluginEtag !== undefined) {
|
||||
const inm = request.headers['if-none-match']
|
||||
if (typeof inm === 'string' && inm.split(',').some((v) => v.trim() === pluginEtag)) {
|
||||
reply.raw.writeHead(304, { etag: pluginEtag, 'cache-control': 'no-cache' })
|
||||
reply.raw.end()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const attempt = (connRetry: boolean, authCookie?: string): void => {
|
||||
// 档案 50 修正(实测定位):**HTML 导航请求必须向上游要 identity**。
|
||||
// 原因:dsh 实例对带 Accept-Encoding 的请求会 **gzip 压缩 HTML**(浏览器就带),
|
||||
@@ -396,6 +422,8 @@ function proxyHttp(
|
||||
) {
|
||||
headers['cache-control'] = 'no-cache'
|
||||
}
|
||||
// 档案 97:把派生 ETag 一并透出,浏览器下次才能用 If-None-Match 换 304。
|
||||
if (pluginEtag !== undefined) headers.etag = pluginEtag
|
||||
// local mode only: DSH builds absolute URLs from the loopback Host we
|
||||
// forward; rewrite any 127.0.0.1 Location to the real origin so the
|
||||
// browser doesn't jump to the user's own machine. k8s mode keeps its
|
||||
|
||||
Reference in new issue
Block a user