> ## Documentation Index
> Fetch the complete documentation index at: https://www.worldmonitor.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 情报分析工艺与前端设计

> World Monitor 仪表盘背后的情报分析工艺、算法设计决策与原生 TypeScript 架构，包括可辨识联合标记系统与面板事件委托模式，说明为何选择这些工程取舍以及它们如何降低复杂度，面向评估平台的工程师与希望参与贡献的开发者阅读。

本页属于[设计哲学](https://www.worldmonitor.app/docs/zh/architecture.md)指南，该页索引全部主题。部署拓扑、目录结构与 CI/CD 请参阅 [`ARCHITECTURE.md`](https://github.com/koala73/worldmonitor/blob/main/ARCHITECTURE.md)。

***

## 情报分析工艺

仪表盘的设计借鉴了成熟的情报分析方法论，并针对自动化开源情报进行了调整：

**结构化分析技术（SATs）** — 系统不直接呈现原始数据，而是应用结构化框架以减少认知偏差。国家不稳定性指数（CII）将"不稳定性"分解为四个加权分量（动荡、冲突、安全、信息速率）— 迫使分析师独立考虑每个维度，而不是锚定在最显眼的头条新闻上。战略风险评分同样将地缘政治风险分解为收敛性、CII、基础设施、战区和突发新闻分量。

**竞争假设分析（ACH）** — 复合情报产品通过佐证独立输入来降低单一来源偏差：国家不稳定性指数融合动荡、冲突、安全和信息速率信号；抗议数据合并 ACLED 与 GDELT；灾害数据融合 USGS、GDACS 和 NASA EONET。突发新闻横幅有意采用不同语义：当前代码只接入两个独立生产者 `rss_alert` 和 `oref_siren`，不要求佐证计数或跨来源收敛。符合条件的 RSS 条目通过新近度、来源层级、故事阶段、重要性、去重、冷却、启动宽限和用户敏感度门控后，可以触发高或关键警报。非空 OREF 警报批次在警报已启用且事件未重复时直接升级为关键，并绕过仅适用于 RSS 的门控和全局冷却。

**情报缺口意识** — 专业情报评估总是注明它们*不知道*的内容。数据新鲜度追踪器显式报告注册来源及其状态分类（新鲜、过期、非常过期、无数据、错误、已禁用）。两个浏览器追踪来源（GDELT、RSS）被标记为 `requiredForRisk`，意味着它们的缺失会迫使战略风险面板进入硬性数据不足状态。该标志比完整的 CII 评分依赖图更窄：UCDP 事件、ACLED 冲突、新闻摘要、网络威胁和其他评分输入通过来源专属健康度加上 `riskScores` 健康/缓存信号密度覆盖来监控。当关键数据源宕机时，系统显著展示该缺口，而不是静默省略，防止因数据不全而产生虚假信心。

**来源可信度加权** — 四级来源层级（通讯社 → 主要媒体 → 专业媒体 → 聚合器）对应情报界的来源评估（A–F 可靠性，1–6 置信度）。国家附属来源出于完整性被纳入，但带有宣传风险指标标记，使分析师能够将编辑偏差纳入考量。较高层级的来源在焦点事件检测和告警生成中具有更高权重。

**时间上下文** — Welford 在线基线计算提供了原始计数所缺乏的时间上下文。"50 架军事航班"本身没有意义，除非知道该星期几和月份的平均值是 15 — 这使观测值高于正常水平 3.3σ。系统自动为每种信号类型提供此上下文。

**警报通道分离** — 关键词飙升、热点升级和军事激增会进入关联与异常产品，但当前并未接入突发新闻横幅。横幅把符合条件的 RSS 条目与 OREF 警报作为彼此独立的警报展示，不暗示这些通道已经融合成经过佐证的评估。

***

## 算法设计决策

几个不那么显而易见的算法选择值得说明：

**对数 vs. 线性抗议评分** — 民主体制中经常发生不表明不稳定的例行抗议（法国黄背心运动、美国校园抗议）。威权国家很少看到公开抗议，因此每个事件都很重要。CII 对民主体制使用 `log(protestCount)`，对威权国家使用线性缩放，防止民主噪声淹没真正的威权动荡信号。

**Welford 在线算法用于基线** — 传统的均值/方差计算需要存储所有历史数据点。Welford 方法维护一个滚动均值和 M2（平方偏差之和），可以用 O(1) 时间和 O(1) 空间随每个新观测更新。这使得在 Redis 中追踪数百个事件类型 × 地区 × 星期 × 月份组合的基线成为可能，而无需存储原始观测。

**H3 六边形网格用于 GPS 干扰** — 使用六边形网格（H3 分辨率 4，边长约 22km）而非矩形经纬度网格，因为六边形具有均匀邻接（6 个邻居 vs. 正方形的 4/8 个）、任意纬度等面积，且无子午线收敛畸变。这对于干扰区检测很重要，因为空间均匀性会影响聚类准确性。

**余纬度修正距离** — 电缆健康度匹配和多个邻近度计算使用等距矩形近似配合 `cos(lat)` 经度修正，而非完整 Haversine。在涉及的距离（50–600km）下，误差 \<0.5%，但速度约快 10 倍 — 在对每个事件计算大型基础设施目录的距离时很重要。

**负缓存** — 当上游 API 返回错误时，系统在定义的时间段内缓存失败状态（UCDP 为 5 分钟，Polymarket 队列拒绝为 30 秒），而不是立即重试。这防止了数百个并发用户同时轰炸宕机 API 的惊群效应，并向情报缺口追踪器提供清晰信号表明某来源不可用。

**O(1) 屈折后缀匹配** — 关键词匹配管道对每个摄入标题中的每个单词检查一组英语屈折后缀（`-ing`、`-ed`、`-tion`、`-ment` 等）以进行词形归一化。后缀列表从 `Array`（每词 O(n) `.some()` 扫描）改为 `Set`（O(1) `.has()` 查找），消除了对每条标题每个单词执行的线性扫描 — 鉴于系统每个刷新周期处理数千条标题，这是一个有意义的热路径优化。

**栈安全数组操作** — `Math.min(...array)` 和 `Math.max(...array)` 展开模式受 V8 参数栈限制（约 65,535 个条目）。对于大型新闻聚类（突发事件中常见），展开会静默溢出并返回 `Infinity` / `-Infinity`，破坏 `firstSeen` 和 `lastUpdated` 时间戳。这些被替换为 `Array.prototype.reduce` 循环，无论数组大小均在 O(1) 栈空间内操作。

***

## TypeScript 架构

### 原生 TypeScript 架构

World Monitor 使用原生 TypeScript 编写 — 不使用任何前端框架（React、Vue、Svelte、Angular）。这是一个刻意的架构决策，而非疏忽。

**为什么不用框架：**

* **包体积** — 仪表盘加载数十个数据图层、地图渲染器、ML 模型和实时视频流。框架开销的每一 KB 都在与实际情报数据竞争空间。整个应用外壳（面板系统、路由、状态管理）编译后的 JavaScript 体积小于 React 的运行时
* **DOM 控制** — 面板系统通过防抖内容替换（`setContent()`）和稳定容器元素上的事件委托直接操作 `innerHTML`。框架的虚拟 DOM diff 会与此模式冲突，增加开销却无收益 — 仪表盘没有框架所优化的细粒度响应式状态更新
* **WebView 兼容性** — Tauri 桌面应用在 WKWebView（macOS）和 WebKitGTK（Linux）中运行，它们在拖放、剪贴板、自动播放和内存管理方面有特殊行为。直接 DOM 操作使得绕过这些平台怪癖成为可能，而无需与框架抽象抗争
* **长期简洁性** — 没有框架版本升级，没有破坏性 API 迁移，没有适配器库。代码库依赖于浏览器标准（DOM、Web Workers、IndexedDB、Intersection Observer、ResizeObserver），这些标准在各引擎更新中保持稳定

**填补框架空白的方案：**

| 关注点 | 解决方案 |
| - | - |
| 组件模型 | `Panel` 基类，带生命周期方法（`render`、`destroy`）、防抖内容更新和事件委托 |
| 状态管理 | `localStorage` 存储用户偏好，`CustomEvent` 派发用于面板间通信（`wm:breaking-news`、`wm:deduct-context`、`theme-changed`、`ai-flow-changed`），以及集中式信号聚合器管理情报状态 |
| 路由 | URL 查询参数（`?view=`、`?c=`、`?layers=`）在启动时解析；`history.pushState` 用于可分享的深链接 |
| 响应式 | `SmartPollLoop` 和 `RefreshScheduler` 类，带命名刷新运行器、可见性感知调度和进行中去重 |
| 虚拟滚动 | 自定义 `VirtualList`，带 DOM 元素池、上下占位 div 和 `requestAnimationFrame` 批处理的滚动处理 |

### 可辨识联合标记系统

所有地图标记 — 在 globe.gl 和 deck.gl 两种引擎中 — 都携带一个 `_kind` 判别字段，用于在运行时识别其类型。每个标记是一个带有字面量 `_kind` 字符串的普通 TypeScript 对象，而非使用类继承（后者需要 `instanceof` 检查且阻止标记数据成为普通可序列化对象）：

```typescript theme={null}
type MapMarker =
  | { _kind: 'conflict'; lat: number; lon: number; severity: string; ... }
  | { _kind: 'flight'; lat: number; lon: number; callsign: string; ... }
  | { _kind: 'vessel'; lat: number; lon: number; mmsi: number; ... }
  | { _kind: 'protest'; lat: number; lon: number; crowd_size: number; ... }
  // ... 15+ additional marker kinds
```

这使得渲染管道中的穷尽式 `switch` 匹配成为可能 — TypeScript 编译器验证每种标记类型都被处理，添加新类型会在每个未处理位置产生编译错误。标记数据可以序列化到/自 JSON（用于 IndexedDB 持久化和 Web Worker 传输）而无需自定义序列化逻辑。相同的标记对象流经聚类、工具提示生成和图层过滤，无需类型转换。

### 面板事件委托模式

`Panel` 基类使用防抖的 `setContent(html)` 方法（150ms 延迟）来批量处理快速 DOM 更新。这带来了一个微妙但关键的问题：附加到面板 `innerHTML` 内元素上的任何事件监听器在防抖触发并替换内容时都会被销毁。

解决方案是**事件委托** — 所有 click、change 和 input 处理器绑定到稳定的外部 `this.content` 容器元素（它从不被替换，只有其 `innerHTML` 变化），使用 `event.target.closest('.selector')` 匹配目标元素：

```typescript theme={null}
// WRONG — listener destroyed on next setContent()
this.content.querySelector('.btn')?.addEventListener('click', handler);

// CORRECT — survives innerHTML replacement
this.content.addEventListener('click', (e) => {
  if (e.target.closest('.btn')) handler(e);
});
```

此模式在所有面板子类中全项目强制执行。在 E2E 测试中，元素引用在防抖渲染后也会失效 — 测试代码必须在每次渲染周期后重新查询 DOM，而不是持有缓存的元素引用。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.