我攒了个 Mac 桌面数据面板:Token 账单 + 供应商切换 + 工作日志 AI 助手(已开源)

张 张站长 · 2026-09-16 17:50 · 技术-app · 60 次浏览 · 0 条评论

把 AI 编程的 token 账单、API 供应商一键切换、工作日志 AI 助手塞进一个 Electron 桌面面板——纯本地数据、手绘 SVG 图表、零图表库依赖。技术栈 Electron + Python + 一点 Swift,已开源。

最近把手头几个零散的小需求捏成了一个 Mac 桌面应用,自己每天开着用,顺手开源了。这篇记录一下它是什么、怎么搭起来的,以及几个我觉得值得单独拿出来讲的实现细节。

先说它解决我什么问题:

  • 我用 Claude Code、Codex 写代码,一天下来到底烧了多少 token、花了多少钱,心里没数;
  • 经常要在不同的 API 供应商之间切来切去,每次手动改 ~/.claude/settings.json、~/.codex/config.toml,烦且容易改错;
  • 想要一个「今天我到底干了啥、有效时间多少」的复盘,但又不想手动记;
  • 微信、钉钉、企业微信的未读散在三个 App 里,来回点。

于是就做了这个面板,把这几件事塞进一个窗口。

监控面板首页

一、整体架构

技术选型很朴素:Electron 做壳和界面,重活交给 Python 子进程,几个跟系统打交道的小活用 Swift 原生写。

┌─────────────────────────────────────────────┐
│            Electron 主进程 (main.js)          │
│  ├── trackApp()   前台应用追踪 (AppleScript)  │
│  ├── checkBadges() Dock 角标检测 (Swift)      │
│  ├── read*Messages() 各家消息读取 (Python)     │
│  ├── get-usage-data  Token 用量 (Python)      │
│  ├── switch-provider 供应商切换 (Python)       │
│  └── ai-chat         工作日志助手 (Python)     │
├─────────────────────────────────────────────┤
│         Electron 渲染进程 (前端界面)           │
│  index.html + renderer.js + styles.css        │
│  五个页面:监控 / Token / 供应商 / AI,IPC 通信 │
├─────────────────────────────────────────────┤
│              外部脚本 (Python / Swift)         │
│  usage_reader.py / provider_manager.py /       │
│  ai_assistant.py / *_reader.py / notif-helper  │
└─────────────────────────────────────────────┘

为什么不纯 Node 一把梭?因为解析日志、跟本地数据库打交道这些活,Python 写起来顺手、生态也全(比如 AES 那套直接 pycryptodome)。主进程通过 child_process 调用 Python 脚本,脚本只干一件事:算完把结果打成 JSON 从 stdout 吐出来,主进程收下再通过 IPC 发给渲染进程画界面。各脚本之间互不依赖,单独拿出来在命令行跑也能用,调试很方便。

跟系统底层打交道的两个活单独用 Swift 编译成命令行小程序:一个监听 macOS 通知中心(notif-helper),一个读 Dock 图标上的未读角标数字(chat-scanner,走辅助功能 API)。这俩用 AppleScript / Node 都别扭,Swift 几十行搞定。

下面挑三个我自己最常开的二级页面细说。

二、AI 编程 Token 账单

这是我做得最上心的一页——把 Claude Code 和 Codex 的 token 消耗、预估费用算清楚。

Token 用量页

数据源是纯本地的,零外部依赖。 这两个工具都会把每一轮对话落到本地日志里,我直接解析:

  • Claude Code:~/.claude/projects/*/*.jsonl,每行一条 JSON。我只要 message.role == "assistant" 且带 usage 的行,字段是现成的:
import json, glob

for path in glob.glob(os.path.expanduser("~/.claude/projects/*/*.jsonl")):
    for line in open(path, encoding="utf-8"):
        if '"usage"' not in line:          # 先按字符串粗筛,省掉大量 json.loads
            continue
        d = json.loads(line)
        msg = d.get("message") or {}
        if msg.get("role") != "assistant":
            continue
        u = msg.get("usage") or {}
        tin  = u.get("input_tokens", 0)
        tout = u.get("output_tokens", 0)
        tcr  = u.get("cache_read_input_tokens", 0)      # 缓存读取
        tcw  = u.get("cache_creation_input_tokens", 0)  # 缓存写入
        model = msg.get("model")
        proj  = os.path.basename(d.get("cwd") or "")    # 项目名 = 工作目录最后一段
        hour  = d.get("timestamp", "")[:13]             # 落到「小时」桶
  • Codex:~/.codex/sessions、~/.codex/archived_sessions 里的 token_count 事件(last_token_usage 是逐轮增量),再从 session_meta 拿 cwd 和 model。

几个口径上的坑,我是这么定的:

  • 「核心 Token」= 新输入 + 输出 + 缓存写入,不含缓存读取。 为什么排掉缓存读取?因为同一段上下文会被反复读,缓存读取的数字会大到离谱(我这台一天缓存读取能到 2.6B),把它算进「用量」会完全盖住真实消耗。所以缓存读取单独列一栏,不进主口径。
  • 费用按各模型的 API 单价,分 input / output / cache_read / cache_write 四类分别计——缓存读写单价和普通输入输出差很多,混着算就不准了。价格表内置在 usage_reader.py 里(价格表和聚合维度参考了掘金的开源项目 jusage,MIT,但解析和实现是自己写的)。

图表全是手绘 SVG,一个图表库都没引。 趋势线就是把每小时的值归一化到 viewBox 高度,拼一串 M x,y L x,y ... 的 path;热力图是一格一个 <rect> 按数值深浅上色;环形占比是算每段的 stroke-dasharray。没有 ECharts 那些依赖,包体积小、渲染也快。

性能上,日志文件很多、还一直在长,每次全解析太慢。我按每个文件的 mtime + size 做缓存,把每文件的「小时级聚合桶」存下来,下次只重新解析变过的文件——首次全解析 2~3 秒,之后大概 35 毫秒。

三、AI 供应商一键切换

第二页,解决「手动改配置文件切供应商」的麻烦。

供应商切换页

原理不复杂,就是帮你安全地改写那两个配置文件:

  • Claude Code → 改 ~/.claude/settings.json 里 env 的 ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN,只动这几个键,你原来的 hooks、权限之类的配置一个不碰;
  • Codex → 改 ~/.codex/config.toml:保留公共段(mcp_servers / notify / projects 等),只替换供应商片段(model_provider / model / [model_providers.*]),再更新 ~/.codex/auth.json 里的 key。

关键是别把用户配置改坏,所以加了几道保险:

  1. 写之前先备份:把目标文件按时间戳复制到 ~/.cc-dashboard/provider_backups/,每个文件留最近 20 份,页面上每个工具都有个「↩ 还原上次」;
  2. 原子写 + 写后校验:先写临时文件再 rename,写完再读回来 parse 一遍确认没写坏;
  3. 密钥不走命令行参数:新增/编辑供应商时,key 是通过子进程的 stdin 传进去的,不会出现在进程参数列表里(ps 看不到)。供应商库存在本机 ~/.cc-dashboard/providers.json,权限 600。

小提醒:它改的是和 cc-switch 同一批配置文件,两个一起用会互相覆盖,建议二选一。这页的切换能力就是仿写 cc-switch 的核心逻辑,首次会从它的库里把供应商导进来,之后就在面板里自己管了。

四、AI 工作日志助手

第三页,我用来做每日复盘。

AI 工作日志助手

它读的是面板自己攒的两份本地日志:

  • ~/.cc-dashboard/activity/YYYY-MM-DD.jsonl——应用使用时间线,每次前台应用切换记一段 {app, start, end, durationSec};
  • ~/.cc-dashboard/notifications/YYYY-MM-DD.jsonl——收到的消息/通知,每条一行 {time, app, title, body}。

后端 ai_assistant.py 把当天这两份日志聚合成上下文,再丢给 DeepSeek 回答。聚合时做了两件事:

  • 应用用时按「效率」归类:写代码/终端/IDE 算高效,浏览器/文档/沟通算中性,社交/视频/音乐算低效。分类表就摊在脚本顶上,随手能改。有个小原则——未知应用默认算「中性」,不武断judged成低效;
  • 消息按会话归类,包括「我自己发出去的消息」也单独拎出来,这样能问它「我今天回复某人的语气怎么样」。

于是就能问它:今天工作了多久、高效低效各多少、几点在干嘛、都和谁聊了。上面截图里那两个回答就是实时跑出来的。

这里必须强调隐私:DeepSeek 的 key / base 从 .env 读,用的是你自己的 key、发的是你自己的数据;除了发给你自己配的那个大模型,数据不出本机。页面右上角还有个「清理本工具缓存」,按天清活动/通知日志、清临时解密缓存——只清面板自己攒的东西,绝不碰源数据。

五、关于消息聚合 & 隐私

首页那块「消息通知」聚合了微信、钉钉、企业微信的未读和新消息(截图里聊天内容我都打码了)。至于怎么从这三家客户端把本地消息读出来——涉及各家客户端的加密库逆向,这篇不展开,点到为止。 这部分我在仓库里也没放取密钥的方法,.env 里的密钥更是每个人自己在自己机器上获取、绝不入库。

整个工具的定位是:只读你自己的、在你自己机器上的数据,只发给你自己配的 AI。 没有任何数据上传到我这或第三方。.env 含明文密钥,已经在 .gitignore 里,别手滑提交上去。

结尾

代码已经开源,技术栈就是 Electron + Python + 一点 Swift,没有花哨依赖。Token 账单、供应商切换、手绘 SVG 图表这几块是完全通用的,抠出来单独用也行。有需要的自取,按 README 配好 .env 就能 npm start 跑起来。

仓库地址:https://github.com/zhangmengzhuoa/desktop-data-panel

评论 0

还没有评论
← 全部文章

找站长

站长微信二维码 扫码加微信
售后群二维码 扫码进售后群
站长收款码 建站不易,感谢打赏
微信 18581469390
本站不支持自助充值;需要充值或开通额度,请直接联系站长。
加微信可以找我重置密码、充值、问工具怎么用