| 字段 | 内容 |
|---|---|
| Bug ID | BUG-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-trade(KarioPanel 所在页面) - 触发条件:页面加载时,KarioPanel 组件初始化,confidence 字段未定义
- 影响用户:所有访问上述页面的用户(页面完全无法渲染)
三、根因分析
3.1 Bug 链路一 — KarioPanel.tsx:confidence 类型守卫错误
文件: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.tsx:33 处裸.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)}%</span>3.3 Bug 链路三 — 浏览器缓存旧 JS,服务器端修复无法触达用户
现象:服务器端代码已修复,但用户刷新页面后仍然崩溃。
根因:Next.js 的 chunk hash 基于文件内容计算。修复前后文件内容相同 → chunk hash 相同 → 浏览器使用本地缓存的旧 chunk → 修复无法触达用户。
服务器端缓存叠加问题:
| 问题 | 原配置 | 影响 |
|---|---|---|
JS chunkCache-Control | max-age=31536000, immutable | 浏览器永久缓存,忽略服务器更新 |
HTML 页面Cache-Control | 无(缺失) | 页面 HTML 可更新,但 JS 不更新 |
| URL 编码 | 路径含(main)→%28main%29 | Path.exists()找不到文件,返回 404 |
四、修复内容
4.1 代码层修复
| 文件 | 修复内容 | 行号 |
|---|---|---|
components/KarioPanel.tsx | != null→typeof === 'number' | ~388 |
components/KarioPanel.tsx | 加console.log刷新 chunk hash | 组件入口 |
components/WorldMap.tsx | yesPrice添加类型 guard | ~116 |
app/(main)/paper/page.tsx | 添加fmt()函数,批量替换 33 处.toFixed() | 新增函数 |
app/(main)/auto-trade/page.tsx | setTasks(...)加Array.isArray()guard | 数据加载处 |
4.2 构建层修复(chunk hash 刷新)
在components/KarioPanel.tsx组件入口添加无害的console.log,强制 Next.js 在 rebuild 时生成新的 chunk hash:
tsx
复制
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-age=0", "Pragma": "no-cache", "Expires": "0", } def _no_cache_response(path, media_type="text/html"): return FileResponse(path, media_type=media_type, headers=dict(_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-age=0"}) 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-age=0 # HTML 应返回:Cache-Control: no-cache, no-store, must-revalidate, max-age=0 # 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.js - HTML 引用新 chunk:
/_next/static/chunks/app/(main)/layout-7a15b57b7dc14b77.js - JS 响应带
Cache-Control: no-cache, must-revalidate, max-age=0 - HTML 响应带
Cache-Control: no-cache, no-store, must-revalidate, max-age=0 - 用户刷新
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()解码才能找到文件。