ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

全栈自造桌面仪表盘:用Tauri和FastAPI构建开发者状态聚合工具

全栈自造桌面仪表盘:用Tauri和FastAPI构建开发者状态聚合工具 1. 为什么要自造一个桌面仪表盘现成工具解不了的痒先说个场景。你有没有过这种时刻正写代码写得起劲临时要确认三件事——刚才本地起的后端服务到底挂了没有、这台机器最近的内存是不是又被某个进程吃满了、线上那个接口今天的错误率是不是有波动。于是你切到浏览器先开终端看服务日志再跑一个htop确认资源占用然后打开 Grafana 或者云厂商的监控后台登录、选项目、选时间范围、等图表加载。一整套下来少说两三分钟。关键是你明明是个开发者你的日常信息来源却分散在十来个地方每次只是想知道一个“现在还行不行”的答案。这就是我决定做 Status Deck 的起点。一个给开发者用的桌面仪表盘不是要替代 Grafana、Prometheus 这种重量级监控体系也不是要做一个花哨的“桌面美化工具”而是要解决一个真实的日常问题把开发者每天高频查看的那十几个状态信息统一放到一个随时能瞄一眼的桌面上。市面上的方案其实不少但我逐一试过之后发现各有各的妥协。先说说我为什么没有直接拿现成工具修修补补而是选择从零开始“全栈自造”。1.1 开发者桌面真正缺的是什么我对这个工具的定义很明确它不是“运维监控台”而是“个人工作台”。运维监控的核心是报警、指标趋势、历史查询它的使用者是值班工程师。而开发者桌面上需要的是什么是此刻、现在、一眼就能看明白的“快照”——比如本机 CPU、内存、磁盘的实时占用率本地开发环境里几个常用服务是否健康手头项目 Git 仓库的分支状态、未提交改动数量线上几个核心接口的可用性甚至可以是今天的待办清单、关注的 GitHub Issue 数量这些信息的特点是单个信息都很轻、不涉及复杂时序分析、但来源特别分散。而且它们的状态是“一眼型”的——你只需要知道绿灯还是红灯不需要看趋势曲线。这种场景下大而全的监控系统是杀鸡用牛刀而且信息密度反而低。真正缺的是一个轻量、可自由拼装、数据源能自己接的“状态聚合层”再配上桌面级的信息呈现方式。另一个痛点是数据采集的自由度。用现成工具你能采的数据是它预设好的想要加一个自己内部的接口状态检查要么写插件要么被平台绑定。对全栈开发者来说这种束缚比功能缺失更难受——明明只是一个几十行的 HTTP 探测逻辑却要为一个插件机制去读一堆 SDK 文档。1.2 市面方案的三种妥协我把目前可选的路都走了一遍总结下来是三种妥协第一种是传统桌面小组件工具比如 Rainmeter、Conky 这类。定制性强能做很漂亮的面板但本质上主要是系统资源类信息的可视化数据源基本限定在本地。你想让它展示“生产环境订单接口的可用性”就得写一堆脚本去对接而且跨平台能力参差不齐。Conky 在 Linux 下是好手但 macOS 上体验一般Rainmeter 则是 Windows 专属。对需要在不同系统上切换开发的场景不够通用。我需要的不是美化桌面而是一个信息聚合工具这类产品方向不对。第二种是家庭服务器或 NAS 自带的监控页方案比如 Grafana Prometheus、Netdata、Glances 的 Web 模式。这套胜在数据采集能力极强历史趋势、告警规则都能做。但问题是它们默认是“服务端思维”你得先部署配套组件、维护数据库时间线、设计 dashboard 面板。为了看五个状态要维护三个组件成本偏高。而且它们的数据呈现逻辑是面向“监控大屏”的不是面向个人桌面的——信息密度太高反而没法在 3 秒内完成“确认一切正常”这个动作。第三种是浏览器扩展 固定标签页方案也就是把这些页面全部塞到浏览器里。听起来省事但痛点最明显浏览器是一个“主动使用”的应用你得先唤醒窗口再在一堆标签里找到对应页签再刷新。而且浏览器不能常驻显示局部信息标签栏那点空间只够放个图标。我需要的是一块“副屏思维”的常驻区域——不看它的时候它不打扰你看一眼的时候信息就在那儿。1.3 自造的前提全栈可控既然没有现成工具完全贴合那剩下两条路一是买商用仪表盘软件二是自己造。商用软件价格不便宜而且核心逻辑还是“订阅数据源插件”灵活性仍然是瓶颈。我自己本来就是做全栈开发的前端的交互层、后端的服务层、数据采集的脚本层都算是日常手艺那就干脆自己写一套。“全栈自造”这几个字在这个项目里不是炫技而是实实在在的需求前端需要又快又好看地渲染状态卡片后端需要稳定采集、聚合、缓存数据数据源需要能自由定义最好加一个新检查只要写十几行配置整个项目要能打包成桌面应用而不是一个要手动启动一堆服务的方案定下方向之后最重要的反而不是代码怎么写而是先把“边界”划清楚。有哪些东西打死不做、哪些东西一定要做好。我的边界是不做历史趋势存储那是时间序列数据库的活不做复杂告警那是值班系统的事不做多用户权限这是个人工具。在边界之内要做得极致的只有两件事信息采集的稳定性和信息呈现的即时性。这个项目和传统监控系统的核心区别它不需要告诉你“过去一小时发生了什么”它只告诉你“现在是否一切正常”。这个定位决定了整个技术架构的走向。2. 技术选型定夺这套全栈组合是怎么敲定的方向定下来之后技术选型反而没花太多时间纠结。原因是我对每个候选方案都有过实际使用体验清楚它们的脾气。整个项目的技术载体是桌面应用这决定了它的技术栈不会是和普通 Web 项目完全一样。我最终选定的组合是Tauri 2 做桌面壳 React 做前端 FastAPI 做本地数据聚合后端 SQLite 做本地缓存。这个组合可能有人觉得奇怪为什么不是 Electron为什么前端和后端要分开为什么不干脆全部用 Node.js下面逐个说理由。2.1 桌面壳选 Tauri 而不是 Electron 的理由两者都能把 Web 技术打包成桌面应用但特性上的差异在这个项目里是关键性的。Electron 的优势是生态成熟、遇到问题基本都能搜到答案。但它有两个我接受不了的特性一是打包体积大一个空的 Electron 应用动辄一两百 MB因为内置了一整个 Chromium二是内存占用高常驻后台时动不动就吃掉五六百 MB 内存。对于我这个应用来说它本身就是一个“监控工具”一个监控工具自己占的资源比被监控的服务还多这说不过去。Tauri 2 的方案是用系统自带的 WebView 渲染前端用 Rust 写后端逻辑两者通过 IPC 通信。打包体积小很多通常几 MB 到十几 MB内存占用也低一个量级。更重要的一个考量是我想把 Status Deck 设计成“前端是壳、后端是真正的大脑”的架构。Tauri 对 HTTP 服务的支持比较自由它有 sidecar 机制可以在应用启动时顺带拉起一个独立的后端进程。这就让我可以把“数据采集”这部分完全从桌面壳中解耦出来单独成为一个服务。它也有代价。Tauri 的生态确实比 Electron 新调试链路更复杂WebView 在不同操作系统上的渲染一致性需要额外适配Rust 那一侧如果功能写得多编译时间会让人焦虑。但在这个项目里Rust 侧我可以尽量精简——只做窗口管理、系统托盘、进程管理这类活业务逻辑全部交给后端服务。2.2 后端服务选 FastAPI SQLite 的考虑很多桌面仪表盘项目会直接把采集逻辑写进前端或者用 Node.js 做一个小服务。但我的数据源包含系统信息、HTTP 探测、Git 仓库状态、也许后续还会有数据库查询这一层非常需要一个“真正的后端”来承载而不是用 Electron/Tauri 的 renderer 进程去干这些脏活累活。选 FastAPI 的原因很直白第一Python 在系统信息采集和脚本类逻辑上有天然优势psutil这个库就能覆盖绝大多数硬件信息读取需求第二FastAPI 的异步特性适合做并发探测——多个 HTTP 接口的健康检查完全可以并行发请求而异步 IO 写出来的代码干净且高效第三FastAPI 自带 OpenAPI 文档开发过程中前端对接 API 基本不用另外维护文档。存储层用 SQLite 一开始看着有点“小看”这个项目但仔细想就清楚了。这个应用的数据特征是高频读、低频写、单机使用。配置信息、历史状态的缓存量都非常小完全不需要 MySQL 这类独立数据库。SQLite 以文件形式存在备份就是拷一个文件对桌面应用来说是最合适的方案。后续如果要做更长的历史趋势分析再去接 ClickHouse 之类的重型存储也不迟。2.3 前端用 React 加自研 widget 体系前端部分我选择了 React没有引入大型组件库而是按照这个项目的需求自研了一套轻量的 widget 体系。为什么不用现成的 dashboard 组件库因为现有库的设计理念都是“大屏监控”数据格式、交互方式都是面向展示型页面的。我要的是一个“个人信息聚合器”卡片大小不一、信息密度不同、优先级经常变化这类灵活布局用自研体系反而更可控。Status Deck 的页面核心是一块网格画布每个 widget 占据一个或多个网格单元。用 CSS Grid 实现这种布局非常顺手从小尺寸的“CPU 占用率”卡片到跨多列展示的“服务列表”卡片都能自由排布。widget 之间的数据相互独立通过统一的协议和后端通信。这套方法的好处是可以像摆积木一样自由组合桌面而且后续新增一个 widget 只需要在前端注册组件、在后端加一个数据源不需要动其他任何代码。技术选型还有一个隐藏原则每一层都选择我最有把握撑住的技术。全栈并不意味着句句都是时髦的新东西而是每个环节选择了需求和技术成熟度的最优交。整个 MVP 的技术栈敲定之后桌面壳就是一个轻量容器数据大脑跑在本地服务里前端只是纯展示皮肤。三者各司其职后续迭代的空间也拉出来了。3. 先画地图再动工数据模型与 API 边界动手写代码之前我花了一整天绘制整个项目的“数据地图”。这个环节的重要性怎么强调都不为过——桌面仪表盘类项目最大的风险不是编码难度而是做了一半发现信息边界不清、数据“从哪来到哪去”一团乱麻。数据地图解决的就是这个问题把每个信息源、每个存储表、每个 API 接口的位置和关系提前定死。3.1 信息分类和数据源抽象我先把桌面上要展示的信息分成了四大类这个分类直接决定了后端模块的边界类别信息示例数据来源采集频率系统资源CPU、内存、磁盘占用率本地 psutil 采集3-5 秒服务健康开发环境接口、数据库连通性HTTP/TCP 探测10-30 秒开发环境端口占用、Git 仓库状态、今日任务本地命令与文件解析10 秒外部状态线上接口可用性、关注 Issue 数量外部 API / 爬虫30-60 秒注意这个表格里的“采集频率”一列它不是拍脑袋定的。系统资源信息变化快、影响即时必须高频开发环境状态相对稳定中频足够外部 API 的请求有成本且可能被限流低频更安全。不同频率的采集任务在后端要用不同的调度策略不能一锅炖。数据源抽象是整个后端设计的核心。我定义了一个统一的数据采集接口每个数据源只要实现这个接口就是一个标准化的采集器# core/base.py from abc import ABC, abstractmethod from typing import Any, Dict class DataSource(ABC): 所有数据源的统一基类 name: str base_source interval: int 30 # 默认采集间隔秒 abstractmethod async def collect(self) - Dict[str, Any]: 采集一次数据返回标准化的 JSON 结构 pass async def health_check(self) - bool: 检查数据源本身是否可用 return True每个具体的数据源SystemSource、HttpProbeSource、GitStatusSource 等都继承这个基类并实现collect方法。调度器统一跑这些数据源而不是每个模块各自开线程。这个抽象带来的好处在项目后期特别明显新增一个“查看今天北京天气”的数据源只需要写一个类不用改任何调度逻辑和前端逻辑。3.2 数据表设计与采集策略SQLite 在这套系统里主要存三类数据配置项、最近一次采集的缓存值、历史短记录。下面是实际建表时的核心表结构我把简化版列出来-- 配置表存设置项key-value 结构 CREATE TABLE settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL ); -- 数据缓存表每个数据源的最新一次采集结果 CREATE TABLE source_cache ( source_name TEXT PRIMARY KEY, data TEXT NOT NULL, -- JSON 序列化后的采集结果 collected_at INTEGER NOT NULL, status TEXT NOT NULL -- ok | degraded | error ); -- 短期历史表用于绘制迷你趋势图只保留最近N条 CREATE TABLE metrics_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_name TEXT NOT NULL, value REAL NOT NULL, created_at INTEGER NOT NULL ); CREATE INDEX idx_history_source_time ON metrics_history(source_name, created_at);有几点设计考量值得展开。source_cache表是“缓存优先”策略的关键前端请求数据时后端默认直接读这张表而不是触发一次实时采集。这样做的原因很现实——前端 UI 随时可能因为窗口重绘、widget 切换而发起请求如果不加缓存会导致采集任务被并发触发轻则资源浪费重则拖垮系统。真正的采集逻辑只由调度器按预设间隔执行采集完写入缓存前端拿到的永远是最近一次完整采样的结果。metrics_history表只用于迷你趋势图。做 CPU 占用率 widget 的时候光有当前数字太单薄配一个最近两小时每 30 秒一个采样点的小折线信息量立刻丰富。这张表的时间范围和采样密度控制住就不会膨胀两小时 240 个点一张表几个数据源加起来也才几千行SQLite 处理这种量毫无压力。3.3 API 接口按“只读优先”设计后端对外暴露的 API 我坚持了“只读优先”原则。整个 MVP 阶段前端对后端只有读操作没有写操作。所有写操作比如新增数据源、调整 widget 配置都通过直接编辑配置文件完成。这不是偷懒而是刻意为之读接口的安全性验证成本低可以把这个项目的复杂度集中在数据采集本身。最终 API 设计很干净一共就四个核心端点GET /api/sources获取所有已注册的数据源列表及状态GET /api/source/{name}获取某个数据源的最新缓存数据GET /api/metrics/history/{name}?hours2获取某个数据源的短期趋势数据GET /api/health整个后端服务的健康检查这四条接口覆盖了 MVP 所有展示需求。为什么把“数据源列表”和“某个数据源的数据”分开而不是一次返回全部因为前端首次加载只需要知道有哪些 widget 可用不用拉取所有数据卡片渲染时再按需请求具体数据这样首屏加载速度更快也天然形成了数据加载的优先级。接口设计里的这个细节对桌面应用的启动体验影响不小——仪表盘最忌讳的就是启动时卡顿。4. 后端采集模块落地从硬件信息到服务状态架构定清楚之后真正动手写代码的阶段就顺风顺水了。后端采集模块是整套系统的“感觉器官”它的质量直接决定仪表盘上的数据是真话还是废话。这一节挑三个核心模块展开讲系统信息采集、服务健康探测、开发环境状态它们各自的实现思路和遇到的坑都值得记录。4.1 系统信息采集的实现细节系统资源信息这块Python 生态里 psutil 是绝对的首选库。它把不同操作系统的底层调用封装成了统一的 API写一套代码到处跑。但实际使用中我发现直接拿 psutil 的原始数据塞到前端是行不通的必须做一层“语义化”转换。核心采集逻辑# sources/system_source.py import psutil from core.base import DataSource class SystemSource(DataSource): name system interval 3 async def collect(self): # 计算 CPU 总占用率interval 传 None 表示采样自上次调用 cpu_percent psutil.cpu_percent(intervalNone) # 内存信息虚拟内存总量、使用量、可用量 mem psutil.virtual_memory() # 磁盘信息只统计物理分区忽略挂载的系统虚拟文件系统 disk_usage {} for partition in psutil.disk_partitions(): if partition.fstype in (tmpfs, devtmpfs, overlay): continue try: usage psutil.disk_usage(partition.mountpoint) disk_usage[partition.mountpoint] { total_gb: round(usage.total / (1024**3), 1), used_gb: round(usage.used / (1024**3), 1), percent: usage.percent } except PermissionError: # 某些系统目录无权限访问跳过不报错 continue return { cpu: { percent: cpu_percent, cores: psutil.cpu_count(logicalTrue) }, memory: { total_gb: round(mem.total / (1024**3), 1), used_gb: round(mem.used / (1024**3), 1), percent: mem.percent }, disk: disk_usage }这里有个容易踩的坑psutil.cpu_percent(intervalNone)第一次调用时返回的是 0.0因为它需要两次采样做差值。我在 UI 层发现刚启动时 CPU 显示 0% 就是这个问题。解决办法是启动时先主动调用一次“预热”让内部计数器跑起来后续的采样值才准确。这个细节如果不注意用户一看“CPU 0%”还以为电脑在摸鱼实际上只是采样机制造成的假象。磁盘分区过滤也值得注意。Linux 系统上/proc、/sys、/dev这些挂载点不能被当成普通磁盘展示否则用户会在界面上看到一堆 1% 占用的小分区而且操作系统在遍历这些虚拟文件系统时还可能因为权限问题抛异常。最好的处理就是上面代码里的做法用fstype黑名单过滤再针对PermissionError做兜底。4.2 服务健康检查HTTP、TCP、进程三种探测方式服务健康检查是开发环境里最常用的状态信息。我在设计时定义了三种探测器每种应对不同的场景HTTP 探测是最常见的适合接口、Web 服务、API 网关。实现不复杂但要考虑超时和状态码语义。超时设置 5 秒是折中值——太短容易把偶发慢服务误判为故障太长会卡住整个轮询批次。状态码的判健康标准也要想清楚2xx和3xx都算正常4xx和5xx按具体场景区分比如一个接口返回 404 可能说明路由配置错了但返回 401 可能只是没带 token服务本身是活的。TCP 探测适合数据库、Redis、消息队列这类没有简单 HTTP 语义的服务。逻辑就是尝试建立一次 TCP 连接能连上就是活的。这里有个经验TCP 探测最好也设置连接超时不能依赖操作系统的默认超时——默认值往往是一两分钟会让整个检查批次卡到天荒地老。3 秒超时对它来说绰绰有余。进程探测最简单也最实用检查目标进程是否在运行。比如开发时依赖 Nginx、MySQL直接用进程名判断是否存活就够了不需要发真实请求。但这个方案有个局限进程活着不代表服务正常。进程卡死、端口没监听、连接池耗尽都会导致“进程在但服务不可用”的情况。所以我在实际使用中的建议是进程探测只作为“最低限度”检查关键服务尽量配合 TCP 或 HTTP 探测一起使用。这三种探测器的组合使用效果很好。比如一个典型的“后端服务健康检查”配置会先用进程探测确认主进程没挂再用 HTTP 探测确认/health端点返回 200两层都过了才会在仪表盘上亮绿灯。如果进程活着但 HTTP 挂了那显示的就是“降级”状态而不是直接红灯——这个三态设计比简单的“通/不通”更符合真实情况也减少误报焦虑。4.3 开发环境小工具端口占用与 Git 仓库状态除了系统资源和服务健康开发者的桌面还常需要看到“开发环境专属信息”。我实现了两个既简单又特别实用的小工具端口占用检查和 Git 仓库状态汇总。端口占用检查解决的问题很具体当你启动一个项目发现端口被占用仪表盘上能直接显示“TCP 3000 被 PID 12345 占用进程名 node”。这个功能的实现依赖解析系统命令的输出比如 Linux/macOS 上用lsof -i :3000Windows 上用netstat -ano然后交叉匹配进程信息。这写起来不复杂但跨平台要处理不同命令和输出格式的差异代码里塞了不少条件分支。这部分我用的是一种“尽量封装、不追求完美”的策略——每种操作系统都有主流命令把这几种命令的输出格式解析对就已经能满足 90% 的使用场景了。Git 仓库状态汇总则是“一站式解决日常 Git 焦虑”的功能。它可以扫描你配置的几个常用工作目录比如~/Projects下的每个子目录逐个检查并返回当前所在分支未提交的改动数量modified/untracked 文件数与远程仓库的分歧情况领先几个 commit / 落后几个 commit最近一次提交时间这个功能的实现方法是用 Python 的subprocess调用git命令。之所以不直接解析.git目录里的内部文件是因为git命令的--porcelain输出格式比文件解析稳定得多而且不用关心版本差异。核心命令是git status --porcelainv1 -b加git rev-list --left-right --count origin/main...HEAD。前者给改动状态后者给分支差异。整个逻辑也就一百来行代码但桌面上多了一块“Git 全局总览”之后效率提升非常明显——再也不用挨个目录跑git status了。5. 前端仪表盘骨架搭建卡片、轮询与暗色主题后端数据通路建好之后前端就是直接“消费”这些数据的地方。作为桌面仪表盘前端的第一优先级不是炫酷动效而是启动快、状态一目了然、长时间运行不卡。这一节讲三个关键设计widget 数据协议、轮询机制、暗色主题下的语义色方案。5.1 Widget 数据协议设计前端每个 widget 和后端交互时用的是统一的协议结构。这个协议的设计原则是“字段精简但语义完备”——不多传无关数据但该有的描述信息一应俱全。// types.ts interface WidgetData { source: string; // 对应后端的数据源名称 status: ok | degraded | error; updatedAt: number; // 数据生成时间戳毫秒 payload: Recordstring, any; // 数据源的具体内容 meta?: { lastError?: string; // 最近一次采集错误信息 durationMs?: number; // 本次采集耗时 }; }这个协议最有价值的地方是那个status字段。widget 的渲染逻辑完全围绕“三态”展开ok是正常的绿色状态degraded是部分正常或数据陈旧比如某个服务挂了但整体可用error是数据源采集中断或后端不可达。三态设计让 UI 层不需要关心具体数据内容就能做出正确的视觉反馈——只要后端标了error前端就显示红色卡片的通用错误态不用每个 widget 单独写异常逻辑。updatedAt字段也很关键。前端展示数据时永远显示“数据刷新时间”如果某个数据源已经 30 秒没更新了widget 上就会看到时间戳停在那里用户立刻能分辨“刚采集的”和“已经陈旧”的数据——不会把 5 分钟前的数据当成现在的状态做判断。5.2 轮询机制与并发控制前端的数据刷新我做了一个统一的轮询调度器而不是让每个 widget 自己定时发请求。这样做的好处是可以在调度器层面统一控制并发数量和频率间隔避免出现“启动时十几个 widget 同时请求后端”的瞬时风暴。调度器的核心逻辑全局只有一个 2 秒的定时器每轮检查所有已挂载 widget 的注册表。如果某个 widget 的数据已经超过它的数据源采集间隔比如系统数据源是 3 秒服务健康是 30 秒并且没有在等待上一次请求返回就触发一次拉取。这个机制避免了两个典型问题数据请求频率永远小于等于后端采集频率不会出现“前端把后端问爆”的情况同一时间最多只有一个请求在飞接口返回后立即更新所有相关的 widget// scheduler.ts const pending new Mapstring, Promisevoid(); export function scheduleFetch(sourceName: string, intervalMs: number) { if (pending.has(sourceName)) return; // 已有请求在飞跳过 const request fetch(/api/source/${sourceName}) .then(res res.json()) .then(data updateWidget(sourceName, data)) .finally(() pending.delete(sourceName)); pending.set(sourceName, request); }UI 层面还有个细节数据刷新时不能整卡闪烁或空白闪烁。我采用的是“保留旧值直到新值到来”的策略widget 内部的数字先平滑过渡到新值而不是先变成 loading 状态再变成新值。桌面仪表盘的使用场景是“瞥一眼”如果每次刷新都闪一下 loading那这个工具就会变成一个闪烁的干扰器。5.3 暗色主题与状态色语义开发者工具用暗色主题几乎是标配Status Deck 也不例外。但在暗色背景上做状态色设计有几个坑值得单独一说。第一是“纯红”与“纯绿”在暗色背景上的对比度问题。纯度 100% 的红色#FF0000在深色底上非常刺眼纯度 100% 的绿色#00FF00则偏亮且容易显得廉价。我的调色方案是统一降低饱和度、提高明度选用了类似“绿色#4ADE80、黄色#FBBF24、红色#F87171”这种带一点灰度的颜色。它们既保持了语义清晰又和暗色背景的融合度更好长时间盯着看也不疲劳。第二是“正常状态不应过度强调”的原则。整个仪表盘绝大多数时间应该是一篇安心的深色加少量绿色只有在异常时红色才大面积出现。这个设计逻辑对应到代码里就是背景色深灰#111827卡片底色稍浅#1F2937正常状态的文字用浅灰色#E5E7EB只有状态指示点用语义色。这样做的好处是“扫一眼看哪里颜色突兀”就能定位异常而不是整屏花团锦簇。第三是状态色不能单独承担所有信息传递。我特意在颜色之外加了一层形状辅助正常是圆点、降级是空心圆、异常是闪烁的圆点。考虑到个别用户可能有色觉差异只靠颜色分辨状态对这部分人不友好。形状辅助是一个很小的改动但对可访问性提升明显。6. 首版跑通之后踩到的坑与取舍任何项目写出来和跑起来之间都有距离。Status Deck 的首版从“能跑”到“好用”之间踩了好几个坑。这一节记录几个印象最深的每一个都是从现象到排查再到解决完整链路复盘。6.1 轮询风暴多 widget 同时请求导致的拥塞首版联调的时候遇到一个很典型的问题应用启动后前端一下子渲染出十几个 widget每个 widget 都按照自己的逻辑发请求后端服务在启动后 2 秒内收到了二十多个并发请求。结果就是 SQLite 的锁竞争、部分 HTTP 探测请求超时、FastAPI 的并发任务堆积整个仪表盘反而比预期更慢地完成首次刷新。排查过程是先看后端日志发现/api/source/的请求集中在同一秒涌入然后看资源监控发现 Python 进程 CPU 飙升。根因很清楚前端缺少全局的请求调度每个 widget 都在“页面加载”这个事件上争先恐后地拉数据。解决方法是前面提到的统一轮询调度器。核心不是限制“同时只能有一个请求”而是让请求频率和 widget 可见性解耦——数据请求只在“系统级心跳”里触发widget 自身的挂载和卸载不直接影响请求节奏。还有一个额外的优化是“错峰启动”首次启动时各个数据源按配置的间隔错开 300-500 毫秒依次采集第一笔数据这样后端压力均匀用户看到的卡片也是一个一个从“骨架屏”变成完整数据的顺滑过程而不是一次性全卡住。6.2 系统信息采集的跨平台兼容问题这个项目一开始就是打算在 macOS 和 Linux 上都能跑的但实际测试下来系统信息这块的兼容性问题比预期多。首先要说明的是psutil 库本身已经做了跨平台的 API 封装CPU、内存、磁盘这类基础信息在两大系统上都能拿到。但到了“磁盘分区过滤”和“文件系统类型”这一层差异就冒出来了。Linux 上有/dev/sda1、/dev/nvme0n1p2这类设备名macOS 上是/dev/disk3s1两者对“系统保留分区”的标识方式不一样。Linux 需要过滤tmpfs、overlay这类内存型文件系统macOS 需要过滤apfs的System只读快照部分。如果不过滤仪表盘上就会显示一大堆 1% 占用的迷惑性分区。最终我定义了一个维护成本很低的方案每个平台维护一个“挂载点过滤前缀 文件系统类型黑名单”的配置默认隐藏掉系统保留分区只展示用户关心的数据盘、项目盘。另一个意外是 macOS 的psutil.disk_usage()在某些情况下会抛PermissionError——特别是访问 Time Machine 备份盘或者一些加密卷的元数据时。一开始这个异常会导致整个采集任务中断后来在采集代码里对单个分区的读取加了 try/except 并做了“该分区跳过”的标记绝不让一个隔离盘故障拖垮整个 SystemSource。6.3 首版之后我做的三个取舍按我一开始的规划首版 MVP 没有做很多“加上去会很酷”的功能有意识砍掉的是这三样第一砍掉“实时终端输出”功能。让仪表盘直接显示某个服务的启动日志流实现上能通过 WebSocket 做到但它会让架构复杂度翻倍数据源要变成流式协议、前端要处理增量渲染、后端要管理长连接而且它本质上改变了仪表盘的定位——从“状态快照”变成了“日志工具”。这个功能不如留给独立的日志工具去做。第二砍掉全应用级设置界面。MVP 阶段的配置数据源列表、轮询频率直接写在config.yaml文件里用户改文件、点“重载配置”按钮生效。做一个完整的设置界面好看但不实用——那是应用成熟到一定阶段才值得投入的地方。先把信息采集做扎实再考虑配置管理也不迟。第三砍掉“多主题皮肤”。暗色主题之外我没有做亮色主题或自定义主题色。不是做不了而是会分散设计精力亮色主题对状态色的语义调整、组件低对比度的测试都是不小的工作量。这类视觉层面的扩展等核心稳定后顺手做掉才是正解。做取舍的原则很简单把资源投向那件“用户每天打开十次”的事上而不是投向“用户偶尔试一次”的事上。Status Deck 走到这里已经完成了从“我要一个能用的工具”到“工具确实能用了”的闭环。目前这套系统每天就挂在副屏旁边占用了不到 300 MB 内存系统资源的三个 widget 每 3 秒刷新服务健康检查每 30 秒跑一轮桌面上一眼扫过去开发环境的状态尽收眼底。这款项目的下一步我计划给 widget 加一个拖拽布局编辑器再把历史上的短期趋势数据升级为跨天的“用时趋势”不过这已经是下一篇文章的内容了。首版踩坑给我的最大体会是这种个人工具型项目代码不如克制重要——判断“做什么”比“怎么做”难得多而这一课的学费是那几天被轮询风暴打挂的后端服务替我交的。
返回列表