diff --git a/scripts/verify-inject.cjs b/scripts/verify-inject.cjs index 29d451f..6cfb7c3 100644 --- a/scripts/verify-inject.cjs +++ b/scripts/verify-inject.cjs @@ -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 + ' 项不合格 ❌ —— 别推上线' : '结论:全部合格 ✅') diff --git a/src/supervisor/proxy.ts b/src/supervisor/proxy.ts index 0d9d633..72dc897 100644 --- a/src/supervisor/proxy.ts +++ b/src/supervisor/proxy.ts @@ -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