feat(C-01):路由/代理前缀冲突修复

SPA冲突路由改名+旧路径Navigate重定向(/api-docs→/open-api-docs、/api-detail→/open-api-detail、/article→/post、/user→/user-home);vite代理宽前缀改精确正则白名单;nginx.conf同步同口径;新增86路由回归脚本+Playwright 3项(86/86全绿)。
This commit is contained in:
chunyu
2026-09-15 15:25:59 +08:00
parent 26feb3d277
commit 24f200bfa6
18 changed files with 539 additions and 137 deletions
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env node
/* C-01 路由回归脚本:73 条 SPA 路由直访检查。
* 检测三件套:① Django 404/JSON 劫持 ② #root 空(白屏)③ 正文长度。
* 用法:node scripts/check_routes.mjs --base http://127.0.0.1:5173
* 期望:PASS 73/73,hijacked=0,blank=0
*/
import { readFileSync } from 'node:fs'
const args = process.argv.slice(2)
const baseIdx = args.indexOf('--base')
const BASE = baseIdx >= 0 && args[baseIdx + 1] ? args[baseIdx + 1].replace(/\/$/, '') : 'http://127.0.0.1:5173'
// 与 src/App.tsx 的 <Route path> 一一对应(不含 * 兜底)
const ROUTES = [
'/', '/settings', '/history', '/favorites',
'/open-api-docs', '/open-api-directory',
'/profile', '/profile/email', '/profile/phone', '/profile/change-password',
'/use', '/document', '/open-api', '/utility', '/learn', '/articles', '/bug',
'/search', '/messages', '/chat',
'/open-api-detail/1', '/tool-detail', '/course-learn', '/post/1',
'/privacy', '/agreement', '/manual', '/system-message',
'/user-home/1', '/user-home', '/console',
'/article-manage', '/bug-detail', '/article-editor', '/learn-manage', '/learn-editor',
'/baidu-translate',
'/wallet/points', '/wallet/coins', '/wallet/coins/recharge', '/task-center', '/wallet/invite',
'/invite/abc123', '/forgot-password', '/register',
'/utility/json-formatter', '/utility/base64', '/utility/timestamp', '/utility/regex',
'/utility/code-runner', '/utility/text-diff', '/utility/encoding-converter',
'/utility/charset-converter', '/utility/color-converter', '/utility/code-formatter',
'/color-picker', '/qrcode-generator', '/utility/image-compressor', '/utility/calculator',
'/utility/date-calculator', '/utility/image-editor', '/changelog',
'/ip-location', '/currency-detail',
'/open-api-detail/currency', '/open-api-detail/baidu-translate',
'/open-api/air-quality-details', '/open-api/weather-details',
'/open-api/qrcode-generator', '/open-api/ip-location', '/open-api/password-generator',
'/shorturl-detail',
// 旧路径重定向(应返回 SPA,由前端 <Navigate> 承接,不应被代理劫持)
'/api-docs', '/api-directory', '/api', '/api-detail/1',
'/api-detail/currency', '/api-detail/baidu-translate',
'/api/air-quality-details', '/api/weather-details',
'/api/qrcode-generator', '/api/ip-location', '/api/password-generator',
'/article/1', '/user', '/user/1',
]
function isHijacked(status, text) {
if (status === 404 && /Django|Not Found|Page not found/i.test(text)) return true
const t = text.trim()
// 后端 JSON 特征:{"code":...} / {"detail":...} 且无前端根节点
if (/^\s*\{[\s\S]*"(code|detail|message)"[\s\S]*\}\s*$/.test(t.slice(0, 2000)) && !text.includes('id="root"')) return true
return false
}
// dev 模式下 index.html 的 #root 恒为空(客户端水合前),
// 因此"白屏"只能断言"是否返回了 SPA 外壳":含 id="root" 即算 SPA 到达。
// 真正的渲染白屏由 Playwright 抽查覆盖(见 PROGRESS)。
function isSpaShell(text) {
return text.includes('id="root"')
}
let pass = 0, hijacked = 0, blank = 0
const bad = []
for (const r of ROUTES) {
const url = BASE + r
try {
const res = await fetch(url, { redirect: 'manual' })
const text = await res.text()
const hj = isHijacked(res.status, text)
const bl = !hj && !isSpaShell(text)
if (hj) hijacked++
if (bl) blank++
if (!hj && !bl) { pass++; console.log(`ok ${r}`) }
else { bad.push(r); console.log(`${hj ? 'HIJACKED' : 'BLANK '} ${r} (status=${res.status} len=${text.length})`) }
} catch (e) {
bad.push(r)
console.log(`ERROR ${r} (${e.message})`)
}
}
console.log('---')
console.log(`ROUTES=${ROUTES.length} PASS=${pass} hijacked=${hijacked} blank=${blank}`)
if (bad.length) { console.log('BAD:'); bad.forEach((r) => console.log(' - ' + r)) }
process.exit(pass === ROUTES.length ? 0 : 1)