☰
[TradeAI] Web跳转链接bug
2026/10/16 9:04:28 网站建设 项目流程
字段内容
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-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-Controlmax-age=31536000, immutable浏览器永久缓存,忽略服务器更新
HTML 页面Cache-Control无(缺失)页面 HTML 可更新,但 JS 不更新
URL 编码路径含(main)→%28main%29Path.exists()找不到文件,返回 404

四、修复内容

4.1 代码层修复

文件修复内容行号
components/KarioPanel.tsx!= null→typeof === 'number'~388
components/KarioPanel.tsx加console.log刷新 chunk hash组件入口
components/WorldMap.tsxyesPrice添加类型 guard~116
app/(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 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()解码才能找到文件。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询