全球雷达概览
想象有一个仪表盘,能同时监控全球军事冲突、金融市场、自然灾害、网络威胁——WorldMonitor 就是这样一个系统
你打开 worldmonitor.app,发生了什么?
想象一下:你打开网站,眼前是一个旋转的 3D 地球,上面闪烁着数百个事件标记——一场正在进行的军事演习、一个飙升的股票指数异常、一场森林火灾的热点图……
这些实时数据来自 65+ 个外部数据源,经过聚合、AI 分析、缓存,在你打开页面的 3 秒内全部呈现。这背后是一套精密的工程系统。
500+ 策划新闻源、AI 合成摘要、双地图引擎、12 类信号指数、21 种语言、原生桌面应用——全部来自一个 TypeScript 单页应用。
五个专业变体,一套代码库
WorldMonitor 最聪明的产品设计之一:一套代码,五个不同的专业版本。
地缘政治 + 军事 + 灾难 + 金融,全品类情报
AI 动态、芯片战争、网络安全威胁
92 个股票交易所、大宗商品、加密货币实时行情
能源、农产品、关键矿产追踪
反刷机器:只展示正面新闻,对抗信息疲劳
技术栈一览
WorldMonitor 选择了一套非常现代、但也挺"反潮流"的技术栈:
原生 TypeScript
没有 React/Vue/Angular。纯 TypeScript + Vite 构建,Panel 类继承体系——86 个面板类,没有框架
双地图引擎
deck.gl 的 WebGL 平面地图 + globe.gl 的 3D 地球仪,45 个数据图层可切换
Edge Functions
60+ Vercel 边缘函数,每个端点独立部署在全球节点,无需管理服务器
本地 AI
支持 Ollama 在本地运行 AI 摘要,完全不需要 API 密钥
前端架构
没有 React 的大型复杂前端——WorldMonitor 如何用面向对象设计管理 86 个面板
8 阶段启动:一次打开的背后
当你访问 worldmonitor.app,App.init() 按顺序执行 8 个阶段,每个阶段有特定的职责:
初始化 IndexedDB、检测语言(支持 21 种)、加载翻译文件
在后台线程预加载 ONNX 机器学习模型(情感分析、摘要生成、命名实体识别)
并发请求两个 /api/bootstrap 端点(快速 3s 超时 + 完整 5s 超时),批量从 Redis 获取缓存数据
PanelLayoutManager 渲染地图和初始面板网格
并行加载所有数据,启动基于变体配置的智能刷新循环
Panel 类:86 个面板的父亲
WorldMonitor 最核心的设计模式:所有面板都继承自一个基类 Panel,就像一个模板。每个专业面板只需要实现"我显示什么内容",其他都由基类处理。
// 基类 Panel 处理所有通用逻辑
class Panel {
setContent(html) { /* 防抖 150ms 渲染 */ }
refresh() { /* 子类实现具体刷新逻辑 */ }
resize() { /* 响应大小变化,持久化到 localStorage */ }
}
// 每个专业面板只关心自己的数据逻辑
class ConflictPanel extends Panel {
refresh() {
const data = getHydratedData('acled-events')
this.setContent(renderConflictList(data))
}
}
Panel 基类是所有面板的"模板父亲",处理通用功能
setContent 负责把 HTML 渲染到面板里,防抖避免频繁重绘
refresh 是"刷新数据"的接口,每个子类自己实现
resize 让用户可以拖拽调整面板大小,记忆到浏览器存储
ConflictPanel 是冲突数据面板,继承所有通用功能
它只需要告诉父类:我要用 acled-events 数据,我的 HTML 长这样
原生 TypeScript + 类继承在这个场景下更快、更可控。WorldMonitor 需要精确控制每个面板的渲染时机和 WebGL 地图的生命周期,框架反而会制造障碍。
双地图引擎:平面与球形
WorldMonitor 同时维护两套地图:
DeckGLMap(平面地图)
deck.gl + MapLibre GL,支持散点图层、热力图、路径图、弧线图。PMTiles 自托管底图切片,Supercluster 标记聚合
GlobeMap(3D 地球)
globe.gl + Three.js,地球纹理 + 大气着色器 + 空闲时自动旋转。所有数据点合并为单一渲染数组,用 _kind 字段区分类型
你想给 WorldMonitor 新增一个"地震数据"面板。为了复用现有的渲染、防抖、大小调整逻辑,你应该继承哪个类?
数据管道
65+ 个外部数据源怎么变成屏幕上那个流畅的实时仪表盘——数据的完整旅程
数据从哪里来,怎么到达你眼前
全球数据到达你的浏览器,要经过一条完整的流水线。理解这条流水线,你就能判断"为什么这个数据是 1 分钟前的"或者"为什么加载慢了"。
Bootstrap:一次请求,所有数据
WorldMonitor 最聪明的性能优化之一:Bootstrap 端点把所有缓存数据一次性批量读出,避免几十个串行请求。
// 同时发两个请求,不等对方
const [fast, slow] = await Promise.allSettled([
fetchBootstrap({ timeout: 3000 }), // 3 秒超时
fetchBootstrap({ timeout: 5000 }), // 5 秒超时
])
// Redis 批量读取所有缓存键
const all_data = await redis.mget(allBootstrapKeys)
同时发起两个 Bootstrap 请求,互不等待
第一个:3 秒超时,先快速加载部分数据,让界面先显示出来
第二个:5 秒超时,加载更完整的数据集
服务端用 Redis mget 一次命令读取所有缓存键——一次网络请求 vs 几十次串行请求
健康监控:数据是否新鲜?
每次往 Redis 写数据,WorldMonitor 同时写一条 seed-meta 记录,记录写入时间和条目数量:
OK
数据在预期时间内更新,一切正常
STALE
数据超过最大允许过期时间,上游可能挂了
WARN
数据有点旧了,但还在可接受范围内,观察中
EMPTY
Redis 里根本没有这条数据,Seed 脚本可能从未成功运行过
/api/health 显示某个数据源的状态是 EMPTY。这最可能意味着什么,你应该优先检查哪里?
缓存与 API 层
四层缓存、ETag 验证、速率限制——WorldMonitor 如何在高并发下保持低延迟
四层缓存:从快到慢
缓存是 WorldMonitor 能处理大量用户访问的关键。它用了四层嵌套缓存,每层都有自己的 TTL(生存时间):
Railway 上的 Seed 脚本定时把上游数据写入 Redis,用户请求到来时 Redis 已经有数据
每个 Edge Function 实例在内存里缓存最近的数据,同一实例的重复请求直接命中
cachedFetchJson 合并并发请求:同一时刻多个请求只向上游发一次请求,然后一起共享结果
前三层都未命中时才请求真实数据源,结果写回 Redis 和 seed-meta
缓存分级:数据有多"鲜"很重要
不同类型的数据"新鲜度要求"不同——飞机位置每秒都在变,历史灾难数据几天才更新一次。WorldMonitor 据此分了 6 个缓存级别:
fast · 5min
实时事件流、航班状态——每 5 分钟刷新
medium · 10min
市场报价、股票分析——10 分钟缓存
slow · 30min
ACLED 冲突事件、网络威胁——半小时缓存
daily · 24h
关键矿产、静态参考数据——每天缓存
no-store · 0
船舶快照、飞机实时追踪——不缓存,每次直接拉取
ETag:省流量的聪明设计
WorldMonitor 的 API 网关有个精妙的优化:ETag 协商缓存。
缓存机制知识测试
Redis 缓存刚过期,100 个用户同时请求同一个 API 端点。如果不处理,会向上游发 100 个请求。WorldMonitor 用哪个机制把 100 个请求变成 1 个上游请求?
桌面端与变体系统
Tauri 2 壳 + Node.js 旁载服务器——WorldMonitor 如何变成一个离线可用的原生桌面应用
为什么要做桌面应用?
WorldMonitor 既是网页应用,也是桌面应用。一套前端代码,两种运行环境。Tauri 2 让这成为可能:
原生系统集成
系统托盘图标、开机自启、原生窗口管理——用户不需要打开浏览器
安全密钥管理
API Key 存储在系统 Keyring(macOS 钥匙串 / Windows Credential Manager),而不是明文文件
本地 API 服务器
桌面版内置一个 Node.js 旁载服务器,把所有 /api/* 请求转发给本地处理——不依赖 Vercel
三层架构:Rust 壳 → Node.js 旁载 → 前端
桌面版有三个独立进程,各司其职:
管理窗口生命周期、系统托盘、IPC 命令注册。从 Keyring 读写 API Key,通过 invoke() 把密钥传给前端
local-api-server.mjs——Tauri 启动时同步拉起,监听本地端口。动态加载 Edge Function 处理器,注入从 Keyring 拿到的密钥
和网页版完全相同的前端代码。启动时检测运行环境,安装 fetch 补丁,把 /api/* 请求重定向到本地旁载服务器
fetch 补丁:一行改变请求目的地
桌面版最精妙的设计:前端代码不需要改动,只在启动时打一个 fetch 补丁,把云端 API 请求无缝切换到本地:
const originalFetch = window.fetch
window.fetch = async (url, options) => {
if (url.startsWith('/api/')) {
// 把 /api/markets 重定向到本地旁载
const localUrl = `http://127.0.0.1:${port}${url}`
return originalFetch(localUrl, {
...options,
headers: { 'Authorization': `Bearer ${jwt}` }
})
}
return originalFetch(url, options) // 其他请求不变
}
保存原始 fetch 函数
用自定义函数替换 window.fetch
如果请求 URL 以 /api/ 开头(如 /api/markets)
把目的地从 Vercel 云端改为 127.0.0.1 本地旁载端口
同时附上 JWT token 供旁载服务器验证身份
其他 URL(外部资源等)原封不动,不影响正常请求
启动握手:旁载服务器怎么和前端建立信任
五变体系统:同一套代码,五种专业版
WorldMonitor 通过检测域名决定激活哪个变体——变体系统在代码加载后第一时间运行:
worldmonitor.app
default 变体:地缘政治 + 军事 + 灾难 + 金融全品类,全部 86 个面板可用
tech.*
tech 变体:AI 动态、芯片战争、网络安全,隐藏金融和冲突面板
finance.*
finance 变体:92 个交易所、加密货币、大宗商品实时行情,5 分钟刷新
commodity.*
commodity 变体:能源、农产品、关键矿产追踪,30 分钟刷新(数据更新慢)
happy.*
happy 变体:仅展示正面新闻,过滤所有冲突和灾难数据——反刷机器
每个变体只是一个配置对象:激活哪些面板、默认哪些地图图层、刷新间隔多久、主题色用哪套。所有业务逻辑、缓存层、API 层完全共享。