fix(preview+pipeline): 资产键编码归一——7 张全角名图片预览 404 修复;证据文档叙述修正

spec 复审发现(浏览器可见缺陷):manifest 资产行 exact_key 有 7 例为裸形
(文件名含全角括号/顿号/间隔号)——html/redirect 键源数据侧已编码、asset
键来自文件名本体是裸形;浏览器对裸非 ASCII 一律百分号编码后发请求,预览
按请求字节逐字匹配 → 裸键永不命中 → 两页 3 张图破图(dist 文件在盘;
生产面 nginx 先解码不受影响)。修补 pipeline.toManifestRoute:exact_key
归一为 URL 线形(可打印 ASCII 原样、其余 UTF-8 百分号编码大写 hex),
target_file 仍盘上裸名、DB/渲染快照域不变。

回归三层:build-pipeline 注入裸全角资产行(红验证);preview-server 2c
非 ASCII redirect → Location → 资产终端 200;e2e 断言 ④c(7 条全查
跟随到终端 200)+ ⑦c(manifest 2772 键全 ASCII 不变量)。spec 复审复跑
(新库全链 job e059bf41)23/23 PASS,浏览器实测 3 张原破图全部加载。

证据文档:§5.2 失败计数修正(5×500+2×mojibake=7 例)、§2 ①c 措辞对齐
断言实义(抽前 3 标题)、§5.5 修补记录、计时/产物/job 面更新至复跑值。
验证:vitest 382 + 主题 26 全绿(408);typecheck 过;lint 裸跑零告警。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-22 15:31:14 +08:00
parent c64f6ac48a
commit 236f3dc4d3
6 changed files with 306 additions and 70 deletions
+32 -1
View File
@@ -608,6 +608,28 @@ async function cmdChain(opts) {
JSON.stringify(a4),
a4.every((x) => x.status === 301 && x.locationMatch),
)
// spec 复审修补回归:非 ASCII redirect 跟随到资产终端——Location 编码形请求须命中
// manifest 资产行(exact_key 已归一为同一编码线形)→ 200 + 图片 MIME(浏览器真实路径)
const nonAsciiRedirects = redirectRows.filter((r) => /\P{ASCII}/u.test(r.location ?? ''))
const a4c = []
for (const row of nonAsciiRedirects) {
const hop = await client.get(row.exact_key)
const terminalKey = asciiLocationMirror(row.location ?? '')
const terminal = await client.get(terminalKey)
a4c.push({
request: row.exact_key,
location: terminalKey,
hopStatus: hop.status,
terminalStatus: terminal.status,
terminalContentType: terminal.headers['content-type'] ?? null,
})
}
record(
`④c 非 ASCII redirect → 资产终端(${String(a4c.length)} 条全查)`,
'301 → 跟随 Location 编码形 → 终端 200 + 图片 MIME(exact_key 编码线形命中)',
JSON.stringify(a4c),
a4c.length > 0 && a4c.every((x) => x.hopStatus === 301 && x.terminalStatus === 200 && String(x.terminalContentType).startsWith('image/')),
)
// ===== 断言 ⑤ sitemap/robots(盘读) =====
console.log('[assert] ⑤ sitemap/robots …')
@@ -654,6 +676,15 @@ async function cmdChain(opts) {
`ok=${String(redirectResults.length - redirectFailures.length)}/${String(redirectRows.length)}${redirectFailures.length === 0 ? '' : ` 首失败=${JSON.stringify(redirectFailures.slice(0, 5))}`}`,
redirectFailures.length === 0 && redirectRows.length === 1746,
)
// spec 复审修补不变量:manifest 全键编码线形(纯 ASCII)——裸非 ASCII 键永不匹配
// 浏览器请求形(浏览器对裸非 ASCII 一律百分号编码后发送)
const nonAsciiKeys = manifest.routes.filter((r) => /\P{ASCII}/u.test(r.exact_key)).map((r) => r.exact_key)
record(
'⑦manifest 键线形',
`全 ${String(manifest.routes.length)} 键纯 ASCII(编码线形——裸非 ASCII 键 = 预览永不命中)`,
nonAsciiKeys.length === 0 ? 'nonAsciiKeys=0' : `nonAsciiKeys=${JSON.stringify(nonAsciiKeys.slice(0, 5))}`,
nonAsciiKeys.length === 0,
)
// ===== 断言 ⑨ 门禁全绿(validation_runs PASS) =====
console.log('[assert] ⑨ 门禁 …')
@@ -698,7 +729,7 @@ async function cmdChain(opts) {
sqlCounts,
timings: Object.fromEntries(Object.entries(timings).map(([k, v]) => [k, Math.round(v)])),
assertions: rows,
detail: { a1, a2, a3, a4, smoke: { htmlTotal: htmlRows.length, htmlFailures: htmlFailures.length, redirectTotal: redirectRows.length, redirectFailures: redirectFailures.length } },
detail: { a1, a2, a3, a4, a4c, smoke: { htmlTotal: htmlRows.length, htmlFailures: htmlFailures.length, redirectTotal: redirectRows.length, redirectFailures: redirectFailures.length } },
buildReport: {
distPath: buildReport.artifacts.distPath,
distFileCount: buildReport.artifacts.distFileCount,