![[TradeAI] Web跳转链接bug](http://pic.xiahunao.cn/yaotu/[TradeAI] Web跳转链接bug)
字段内容Bug IDBUG-2026-07-07-001标题/app/paper/app/auto-trade页面 JS 崩溃 —Cannot read properties of undefined (reading toFixed)严重级别P0发现日期2026-07-07修复日期2026-07-07状态✅ 已修复并验证一、问题描述用户访问 Kario DApp 的/app/paper或/app/auto-trade页面时页面白屏崩溃控制台报错Cannot read properties of undefined (reading toFixed) TypeError: Cannot read properties of undefined (reading toFixed)所有使用 KarioPanel 组件的页面均受影响包括/app/paper、/app/auto-trade、/app/backtest等。二、影响范围受影响页面/app/paper、/app/auto-tradeKarioPanel 所在页面触发条件页面加载时KarioPanel 组件初始化confidence 字段未定义影响用户所有访问上述页面的用户页面完全无法渲染三、根因分析3.1 Bug 链路一 — KarioPanel.tsxconfidence 类型守卫错误文件components/KarioPanel.tsx:388问题代码tsx复制{a.confidence ! null ( span{Number(a.confidence * 100).toFixed(0)}%/span )}分析! null在 JavaScript 中返回true当且仅当操作数既不是null也不是undefined。然而当后端返回的数据结构中confidence字段缺失undefined时条件判断本身不会阻止执行 — 真正的问题在于数据流中某些路径会产生非 number 类型的confidence如NaN导致Number(NaN * 100).toFixed(0)虽然不直接崩溃但产生异常输出而某些边界情况下字段完全缺失时链式调用崩溃。更深层根因后端/api/v1/assistant/chat返回的skill_results中部分 assistant 消息的confidence字段为undefined且组件对数据结构的假设不够严格。修复代码// ✅ 严格检查 number 类型 — typeof 永远不会对 undefined/nan 误判 {typeof a.confidence number ( span{Number(a.confidence * 100).toFixed(0)}%/span )}3.2 Bug 链路二 — paper/page.tsx33 处裸.toFixed()调用无 guard文件app/(main)/paper/page.tsx问题代码示例tsx复制span{account?.equity.toFixed(2)}/span // account.equity 可能是 undefined span{n.toFixed(2)}%/span // n 是 any无任何保障分析TypeScript 编译时account?.equity的类型是number | undefined但链式调用.toFixed()没有可选链保护。当后端数据尚未返回或返回异常时account对象存在但equity为undefined直接调用undefined.toFixed()立即崩溃。修复代码tsx复制function fmt(n: any, digits 2): string { if (typeof n ! number || !isFinite(n)) return —; return n.toFixed(digits); } // 批量替换所有 .toFixed() 调用例如 span{fmt(account?.equity, 2)}/span span{fmt(n, 2)}%/span3.3 Bug 链路三 — 浏览器缓存旧 JS服务器端修复无法触达用户现象服务器端代码已修复但用户刷新页面后仍然崩溃。根因Next.js 的 chunk hash 基于文件内容计算。修复前后文件内容相同 → chunk hash 相同 → 浏览器使用本地缓存的旧 chunk → 修复无法触达用户。服务器端缓存叠加问题问题原配置影响JS chunkCache-Controlmax-age31536000, immutable浏览器永久缓存忽略服务器更新HTML 页面Cache-Control无缺失页面 HTML 可更新但 JS 不更新URL 编码路径含(main)→%28main%29Path.exists()找不到文件返回 404四、修复内容4.1 代码层修复文件修复内容行号components/KarioPanel.tsx! null→typeof number~388components/KarioPanel.tsx加console.log刷新 chunk hash组件入口components/WorldMap.tsxyesPrice添加类型 guard~116app/(main)/paper/page.tsx添加fmt()函数批量替换 33 处.toFixed()新增函数app/(main)/auto-trade/page.tsxsetTasks(...)加Array.isArray()guard数据加载处4.2 构建层修复chunk hash 刷新在components/KarioPanel.tsx组件入口添加无害的console.log强制 Next.js 在 rebuild 时生成新的 chunk hashtsx复制export default function KarioPanel() { // v2026-07-07 - chunk hash refresh (cache bust) console.log([KarioPanel] loaded at, new Date().toISOString()); // ... }效果chunk hash 从layout-d8f27479a5ee7d28.js变为layout-7a15b57b7dc14b77.js浏览器不认识新 hash触发重新请求。4.3 服务端缓存修复文件apps/web_market/server.py修复一 — 统一的 no-cache 响应函数python复制_NO_CACHE { Cache-Control: no-cache, no-store, must-revalidate, max-age0, Pragma: no-cache, Expires: 0, } def _no_cache_response(path, media_typetext/html): return FileResponse(path, media_typemedia_type, headersdict(_NO_CACHE))修复二 —/_next/{path}静态资源路由python复制app.get(/_next/{path:path}) async def next_static(path: str): import urllib.parse # 解码 %28 %29使 Path() 能找到 app/(main)/xxx.js decoded_path urllib.parse.unquote(path) rel decoded_path.replace(static/, ) p NEXT_STATIC_DIR / rel if p.exists(): return FileResponse(str(p), headers{Cache-Control: no-cache, must-revalidate, max-age0}) raise HTTPException(404)修复三 —/app/{path}页面路由所有 HTML 页面响应改用_no_cache_response()确保每次请求都返回最新 HTML。修复四 — 所有页面和静态资源python复制# /app/{path}、/app、/、/twa/{path}、/favicon.ico # 全部改用 _no_cache_response()五、部署步骤bash复制# 1. 代码已修改重新构建 cd apps/web_market npm run build # 2. 重启 PM2直接 kill 会被 PM2 自动拉起旧代码必须用 restart pm2 restart 9 --update-env # 3. 验证 cache-control 头 # JS chunk 应返回Cache-Control: no-cache, must-revalidate, max-age0 # HTML 应返回Cache-Control: no-cache, no-store, must-revalidate, max-age0 # 4. 验证新 chunk 被引用 curl http://localhost:8000/app/paper | grep layout-7a15b57b7dc14b77 # 输出应包含/_next/static/chunks/app/(main)/layout-7a15b57b7dc14b77.js # 5. 用户访问 https://tradeai.hk/app/paper # Console 应可见[KarioPanel] loaded at 2026-07-07T...六、验证清单npm run build成功无 error新 chunk hash 生成layout-7a15b57b7dc14b77.jsHTML 引用新 chunk/_next/static/chunks/app/(main)/layout-7a15b57b7dc14b77.jsJS 响应带Cache-Control: no-cache, must-revalidate, max-age0HTML 响应带Cache-Control: no-cache, no-store, must-revalidate, max-age0用户刷新https://tradeai.hk/app/paper— 页面正常无崩溃Console 可见[KarioPanel] loaded at 2026-07-07...确认新 chunk 生效PM2 重启成功进程稳定restart count 不再持续增长七、经验教训7.1! null不是类型守卫tsx复制// ❌ 危险undefined ! null → true不会阻止执行 if (x ! null) { x.toFixed(2) } // ✅ 安全typeof 只对 number 返回 true if (typeof x number) { x.toFixed(2) } // ✅ 安全可选链兜底 (x ?? 0).toFixed(2)7.2 任何.toFixed()调用必须经过 formatter永远不要对any类型或可能为undefined的值直接调用.toFixed()。统一用fmt(n)或(n ?? 0).toFixed()。7.3 Next.js chunk hash 浏览器缓存是双重陷阱服务器代码改了 ≠ 浏览器拿到新 JS服务器加了no-cache头 ≠ 浏览器一定不缓存CDN/Proxy/ServiceWorker 可能忽略正确姿势修改代码内容触发 chunk hash 变化这是让缓存失效的确定手段7.4 PM2 管理的进程用pm2 restart id刷新直接kill会被 PM2 自动拉起旧进程必须用pm2 restart才能让 PM2 用新代码重启。7.5 URL 编码在文件路径映射中必须处理Next.js 生成的(main)目录名在 URL 中是%28main%29服务器路由和Path.exists()必须做urllib.parse.unquote()解码才能找到文件。