---
title: "桌面应用"
description: "World Monitor Tauri 桌面应用架构深度详解：涵盖 Rust sidecar 进程管理、系统密钥链安全存储、云端 API 回退策略、离线缓存机制、自动更新流程，以及 Windows、macOS、Linux 的多平台构建、代码签名与公证发布流水线的完整实现细节。"
---

## 概览

### 桌面应用（Tauri）

- **原生桌面应用**，支持 macOS、Windows 和 Linux —— 打包完整的仪表盘，并附带一个本地 Node.js sidecar，在本地运行全部 60+ 个 API 处理器
- **OS 钥匙串集成** —— API 密钥存储在系统凭据管理器中（macOS Keychain、Windows Credential Manager），绝不以明文文件形式存储
- **Token 鉴权的 sidecar** —— 一个唯一的会话 token 防止其他本地进程访问 localhost 上的 sidecar。每次启动时通过随机化哈希生成
- **云端回退** —— 当本地 API 处理器失败或缺失时，请求会透明地回退到云端部署（worldmonitor.app），并剥离 Origin 头
- **设置窗口** —— 专用配置 UI（Cmd+,），包含三个标签页：**LLMs**（Ollama 端点、模型选择、Groq、OpenRouter）、**API Keys**（12+ 个数据源凭据，逐键校验）以及 **Debug & Logs**（流量日志、详细模式、日志文件）。每个标签页运行独立的校验流水线 —— 在 LLMs 标签页保存不会阻塞 API Keys 的校验
- **自动模型发现** —— 当你在 LLMs 标签页设置 Ollama 或 LM Studio 的端点 URL 时，设置面板会立即查询其可用模型（先尝试 Ollama 原生 `/api/tags`，再尝试 OpenAI 兼容的 `/v1/models`），并填充下拉菜单。Embedding 模型会被过滤掉。如果发现失败，会显示一个手动文本输入作为回退
- **跨窗口密钥同步** —— 主仪表盘和设置窗口运行在独立的 webview 中，具有独立的 JS 上下文。在 Settings 中保存密钥时会写入 OS 钥匙串并广播一个 `localStorage` 变更事件。主窗口监听此事件并热重载所有密钥，无需重启应用
- **合并式钥匙串保险库** —— 所有密钥作为一个 JSON blob 存储在单个钥匙串条目（`secrets-vault`）中，而非每个密钥一个条目。这将每次应用启动时的 macOS Keychain 授权提示从 20+ 次减少到正好 1 次。一次性迁移会读取现有任何单独条目，合并它们，并清理旧格式
- **详细调试模式** —— 切换流量日志，状态在重启后保持。查看最近 200 个请求，包含时序、状态码和错误详情
- **DevTools 切换** —— Cmd+Alt+I 打开嵌入式 web 检查器进行调试
- **自动更新检查器** —— 每 6 小时轮询云端 API 检查新版本。显示非侵入式更新徽章，附带直接下载链接，并支持按版本忽略。具备变体感知能力 —— Tech Monitor 桌面应用会链接到正确的 Tech Monitor 发布资产

## 多平台架构

全部六个变体运行在三个相互协作的平台上：

```
┌─────────────────────────────────────┐
│          Vercel (Edge)              │
│  60+ edge functions · static SPA    │
│  Proto gateway (24 typed services)  │
│  CORS allowlist · Redis cache       │
│  AI pipeline · market analytics     │
│  CDN caching (s-maxage) · PWA host  │
└──────────┬─────────────┬────────────┘
           │             │ fallback
           │             ▼
           │  ┌───────────────────────────────────┐
           │  │     Tauri Desktop (Rust + Node)   │
           │  │  OS keychain · Token-auth sidecar │
           │  │  60+ local API handlers · br/gzip    │
           │  │  Cloud fallback · Traffic logging │
           │  └───────────────────────────────────┘
           │
           │ https:// (server-side)
           │ wss://   (client-side)
           ▼
┌──────────────────────────────────────────┐
│        Railway (Relay Server)            │
│  AIS WebSocket · OpenSky OAuth2          │
│  Telegram MTProto (26 OSINT channels)    │
│  OREF rocket alerts (residential proxy)  │
│  Polymarket proxy (queue backpressure)   │
│  ICAO NOTAM · RSS proxy · gzip all resp │
└──────────────────────────────────────────┘
```

**为什么需要两个平台？** 一些上游 API（OpenSky Network、CNN RSS、UN News、CISA、IAEA）会主动屏蔽来自 Vercel IP 段的请求，还有一些需要持久连接或 edge function 不支持的协议。Railway 中继服务器作为备用源站，负责处理：

- **AIS 船舶追踪** —— 维护到 AISStream.io 的持久 WebSocket 连接，并将其多路复用到所有已连接的浏览器客户端，避免触发每用户连接限制
- **OpenSky 航空器数据** —— 通过 OAuth2 client credentials 流程进行认证（Vercel IP 在没有 auth token 时会被 OpenSky 返回 403）
- **Telegram 情报** —— 一个 GramJS MTProto 客户端以 60 秒为周期轮询 26 个 OSINT 频道，具备逐频道超时和 FLOOD_WAIT 处理
- **OREF 火箭警报** —— 通过 `curl` 经住宅代理轮询以色列后方司令部警报系统（Akamai WAF 会屏蔽数据中心的 TLS 指纹）
- **Polymarket 代理** —— 从 Gamma API 拉取，带有并发上游限流（最多 3 个同时请求，队列背压阈值为 20）、在途请求去重，以及 10 分钟缓存，以防止来自 11 个并行标签查询的踩踏
- **ICAO NOTAM 代理** —— 通过中继路由 MENA 机场的 NOTAM 关闭查询，绕过 ICAO API 上对 Vercel IP 的限制
- **GDELT 正面事件** —— 一个 15 分钟的 cron 任务拉取三个主题化的 GDELT GEO API 查询（突破/可再生能源、保护/人道、志愿/慈善），按事件名去重，校验坐标，按类别分类，并以 45 分钟 TTL 写入 Redis。这取代了直接调用 Vercel Edge Function 的方式 —— 后者因 GDELT 约 31 秒的顺序响应时间超过 25 秒的 edge 超时，在 99.9% 的调用中失败。已注册 bootstrap 水合，使 Happy 变体在首次渲染时即有数据
- **RSS feeds** —— 代理来自屏蔽 Vercel IP 的域名的 feeds，并带有独立的域名白名单以保证安全。支持条件 GET（ETag/If-Modified-Since）以减少未变更 feeds 的带宽消耗

Vercel edge function 通过 `WS_RELAY_URL`（服务端，HTTPS）连接到 Railway，而浏览器客户端通过 `VITE_WS_RELAY_URL`（客户端，WSS）连接。这种分离使中继 URL 可按部署配置，而不会把服务端配置泄露给浏览器。

当客户端接受且负载超过 1KB 时，所有 Railway 中继响应都会进行 gzip 压缩（zlib `gzipSync`），可减少 JSON 与 XML 响应约 80% 的出口流量。桌面本地 sidecar 现在优先使用 Brotli（`br`），对于大于 1KB 的负载回退到 gzip，自动设置 `Content-Encoding` 与 `Vary: Accept-Encoding`。

## 桌面应用架构

Tauri 桌面应用将仪表盘封装在一个原生窗口中（macOS、Windows、Linux），并附带一个本地 Node.js sidecar，运行全部 API 处理器而无需依赖云端：

```
┌─────────────────────────────────────────────────┐
│              Tauri (Rust)                       │
│  Window management · Consolidated keychain vault│
│  Token generation · Log management · Menu bar   │
│  Polymarket native TLS bridge                   │
└─────────────────────┬───────────────────────────┘
                      │ spawn + env vars
                      ▼
┌─────────────────────────────────────────────────┐
│      Node.js Sidecar (dynamic port)             │
│  60+ API handlers · Local RSS proxy             │
│  Brotli/Gzip compression · Cloud fallback       │
│  Traffic logging · Verbose debug mode           │
└─────────────────────┬───────────────────────────┘
                      │ fetch (on local failure)
                      ▼
┌─────────────────────────────────────────────────┐
│         Cloud (worldmonitor.app)                │
│  Transparent fallback when local handlers fail  │
└─────────────────────────────────────────────────┘
```

## 密钥管理

API 密钥存储在操作系统的凭据管理器中（macOS Keychain、Windows Credential Manager）—— 绝不以明文配置文件形式存储。所有密钥都合并到钥匙串中的单个 JSON 保险库条目里，因此无论配置了多少密钥，应用启动都只需要一次 OS 授权提示。

sidecar 启动时，会读取保险库、解析，并作为环境变量注入。空值或仅含空白字符的值会被跳过。密钥也可以在运行时更新而无需重启 sidecar：在 Settings 窗口保存密钥会触发一次 `POST /api/local-env-update` 调用，热补丁 `process.env`，使处理器立即取到新值。

**校验流水线** —— 当你在 Settings 中输入凭据时，应用会针对实际的提供商 API 进行校验（Groq → `/openai/v1/models`，Ollama → `/api/tags`，FRED → GDP 测试查询，NASA FIRMS → 火灾数据拉取等）。网络错误（超时、DNS 失败、主机不可达）被视为软通过 —— 密钥会被保存并附带"无法验证"提示，而不会阻塞。只有来自提供商的明确 401/403 响应才会将密钥标记为无效。这防止了临时网络问题把用户锁在自己的凭据之外。

**智能重新校验** —— 保存设置时，校验流水线会跳过自上次成功校验以来未修改的密钥。这避免了当用户只改动一个密钥但已有 15 个已配置且校验通过的密钥时，对提供商 API 产生不必要的往返请求。只有新输入或修改过的密钥才会触发校验请求。

**桌面端特有要求** —— 一些功能在桌面端需要的凭据比 Web 端更少。例如，Web 端的 AIS 船舶追踪同时需要中继 URL 和 API 密钥，但桌面 sidecar 在内部处理中继连接，因此只需要 API 密钥。设置面板会根据检测到的平台调整其必填字段显示。

### 桌面运行时配置 Schema

World Monitor 桌面端使用一个运行时配置 schema，具备按功能切换的开关与密钥支撑的凭据。

### 密钥

桌面保险库 schema（Rust `SUPPORTED_SECRET_KEYS`）支持以下 25 个密钥：

- `GROQ_API_KEY`
- `OPENROUTER_API_KEY`
- `FRED_API_KEY`
- `EIA_API_KEY`
- `FINNHUB_API_KEY`
- `CLOUDFLARE_API_TOKEN`
- `ACLED_ACCESS_TOKEN`
- `URLHAUS_AUTH_KEY`
- `OTX_API_KEY`
- `ABUSEIPDB_API_KEY`
- `NASA_FIRMS_API_KEY`
- `WINGBITS_API_KEY`
- `WS_RELAY_URL`
- `VITE_WS_RELAY_URL`
- `VITE_OPENSKY_RELAY_URL`
- `OPENSKY_CLIENT_ID`
- `OPENSKY_CLIENT_SECRET`
- `AISSTREAM_API_KEY`
- `OLLAMA_API_URL`
- `OLLAMA_MODEL`
- `WORLDMONITOR_API_KEY` —— 控制云端回退访问（最少 16 字符）
- `WTO_API_KEY`
- `AVIATIONSTACK_API`
- `ICAO_API_KEY`
- `UCDP_ACCESS_TOKEN`

### 功能 schema

每个功能包含：

- `id`：稳定的功能标识符。
- `requiredSecrets`：必须存在且有效的密钥列表。
- `enabled`：来自运行时设置面板的用户切换状态。
- `available`：计算值（`enabled && requiredSecrets valid`）。
- `fallback`：面向用户的降级行为描述。

### 桌面密钥存储

桌面构建通过 Tauri 命令绑定将密钥持久化到 OS 凭据存储中，底层为 Rust `keyring` 条目（`world-monitor` 服务命名空间）。

前端**不会以明文文件形式存储**密钥。

### 降级行为

若所需密钥缺失/被禁用：

- 摘要：Groq/OpenRouter 被禁用，回退到浏览器模型。
- FRED / EIA / Finnhub：经济、石油分析和股票数据返回空状态。
- Cloudflare / ACLED：中断/冲突返回空状态。
- 网络威胁 feeds（URLhaus、OTX、AbuseIPDB）：网络威胁图层返回空状态。
- NASA FIRMS：卫星火灾检测返回空状态。
- Wingbits：航班增强被禁用，仅保留基于启发式的航班分类。
- AIS / OpenSky 中继：实时追踪功能被干净地禁用。
- World Monitor API 密钥：云端回退被阻塞；桌面端仅以本地模式运行。

## Sidecar

### Sidecar 鉴权

每次应用启动时，通过随机化哈希状态（Rust 标准库的 `RandomState`）生成一个唯一的 32 字符十六进制 token。该 token 会：

1. 作为 `LOCAL_API_TOKEN` 注入 sidecar
2. 前端通过 `get_local_api_token` Tauri 命令获取（在首次 API 请求时懒加载）
3. 作为 `Authorization: Bearer <token>` 附加到每个本地请求

`/api/service-status` 健康检查端点免除 token 校验，以支持监控工具。

### 动态端口分配

sidecar 默认使用端口 46123，但会优雅地处理 `EADDRINUSE` —— 如果端口被占用（另一个 World Monitor 实例，或任何其他进程），sidecar 会绑定到端口 0 并让 OS 分配一个可用的临时端口。实际绑定的端口会写入一个端口文件（日志目录中的 `sidecar.port`），Rust 宿主在启动时轮询该文件（100 毫秒间隔，5 秒超时）。前端在运行时通过 `get_local_api_port` IPC 命令发现端口，`runtime.ts` 中的 `getApiBaseUrl()` 是规范的访问器 —— 禁止在前端代码中硬编码端口 46123。CSP 的 `connect-src` 指令使用 `http://127.0.0.1:*` 以适配任意端口。

### 本地 RSS 代理

sidecar 内置了一个 RSS 代理处理器，直接从源域名拉取新闻 feeds，完全绕过云端 RSS 代理。这意味着桌面应用可以加载全部 500+ 个 RSS feeds 而无需任何云端依赖 —— Vercel edge 代理使用的相同域名白名单会在本地强制执行。结合本地 API 处理器，这使桌面应用能够作为一个完全自包含的情报聚合平台运行。

### Sidecar 弹性

sidecar 采用多种弹性模式，以在上游 API 降级时维持数据可用性：

- **出错时返回旧数据** —— 当上游 API 返回 5xx 错误或超时时，sidecar 从其内存缓存中提供最后一次成功响应，而非传播失败。面板会显示旧数据并附带可视的"重试中"指示器，而不是变空白
- **负缓存** —— 上游失败后，sidecar 会记录一个 5 分钟的负缓存条目，以防止立即重新命中同一故障端点。冷却期间的后续请求会立即收到旧响应
- **错峰请求** —— 具有严格速率限制的 API（Yahoo Finance）使用顺序请求批处理，请求间延迟 150 毫秒，而非 `Promise.all`。这会把 10 个并发请求（会触发 HTTP 429）转换为一个保持在速率限制之下的错峰序列
- **在途请求去重** —— 对同一资源的并发请求（例如多个面板轮询同一端点）被合并为单次上游拉取。第一个请求创建一个 Promise 存储在在途映射中；所有并发请求都等待该单个 Promise
- **面板重试指示器** —— 当面板的数据拉取失败并重试时，Panel 基类会显示一个非侵入式的"Retrying..."指示器，让用户理解仪表盘正在自我修复而非已损坏

## 云端回退

当本地 API 处理器缺失、抛出错误或返回 5xx 状态时，sidecar 会透明地将请求代理到云端部署。失败的端点会被标记为 `cloudPreferred` —— 后续请求会跳过本地处理器并直接访问云端，直到 sidecar 重启。代理前会剥离 Origin 和 Referer 头，以维持服务端到服务端的对等。

## 可观测性

- **流量日志** —— 最近 200 个请求的环形缓冲区，包含方法、路径、状态和耗时（毫秒），可通过 `GET /api/local-traffic-log` 访问
- **详细模式** —— 通过 `POST /api/local-debug-toggle` 切换，在 `verbose-mode.json` 中跨 sidecar 重启持久化
- **双日志文件** —— `desktop.log` 捕获 Rust 端事件（启动、密钥注入计数、菜单操作），而 `local-api.log` 捕获 Node.js 的 stdout/stderr
- **强制 IPv4 拉取** —— sidecar 补丁 `globalThis.fetch` 以对所有出站请求强制使用 IPv4。政府 API（NASA FIRMS、EIA、FRED）发布 AAAA DNS 记录，但其 IPv6 端点经常超时。该补丁使用 `node:https` 并设置 `family: 4` 以绕过 Happy Eyeballs，避免级联的 ETIMEDOUT 失败
- **DevTools** —— `Cmd+Alt+I` 切换嵌入式 web 检查器

## 自动更新

桌面应用通过轮询 `worldmonitor.app/api/version` 检查新版本 —— 启动时一次（延迟 5 秒），之后每 6 小时一次。当检测到较新版本时（semver 比较），会出现一个非侵入式的更新徽章，附带指向 GitHub Release 页面的直接链接。

更新提示可按版本忽略 —— 忽略 v2.5.0 不会抑制 v2.6.0 的通知。更新器具备变体感知能力：Tech Monitor 桌面构建会链接到 Tech Monitor 的发布资产，而非 full 变体。

`/api/version` 端点会读取最新的 GitHub Release 标签并将结果缓存 1 小时，因此版本检查不会在每次请求时都命中 GitHub API。
