# DeepSeekPulse **Repository Path**: aowind/DeepSeekPulse ## Basic Information - **Project Name**: DeepSeekPulse - **Description**: Windows ����������DeepSeek ����������������GLM Coding Plan ������������������������������ - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-24 - **Last Updated**: 2026-10-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # DeepSeekPulse Windows 托盘小工具:常驻显示 DeepSeek 余额 / 今日消耗 + GLM 套餐额度,点一下弹出浮窗看明细。 ![浮窗预览](docs/deepseek.png) ## 能显示什么 | 位置 | 内容 | |---|---| | 托盘图标 | 余额或今日消耗(可切「余额 / 今日 / 轮播」),读不出来时显示 `!`,欠费显示 `欠费` | | 鼠标悬停 | `DS ¥233.4 \| GLM 5h 62% 周45% \| 低谷→14:00` | | 浮窗 · DeepSeek 页 | 账户余额、赠送余额、充值余额、今日消耗、请求次数、Token 用量、缓存命中率、峰谷时段与倒计时、24 小时时段轴 | | 浮窗 · GLM 页 | 套餐等级(Lite/Pro/Max)、5 小时额度、周额度、MCP 工具月额度,各自剩余百分比、进度条、**下次刷新时间**、已用/总量 | 浮窗顶上有两个页签,鼠标点一下就能切;托盘右键菜单也能直接打开指定那一页。 浮窗失焦自动收起,`Esc` 也可以收起。 托盘图标的**文字颜色跟随系统任务栏主题**:浅色任务栏用深色字 + 白描边,深色任务栏用白字 + 黑描边。 切换系统主题会立刻重绘,不用等下一次刷新。嫌不够醒目可以在设置里勾「图标加深色底」。 ![托盘效果](docs/tray.png) ## 清理残留进程 托盘右键 → **清理残留进程…**,收拾两类东西: - **孤儿进程**:父进程已经退出、没人管、还在吃内存的 WorkBuddy / node 僵尸 - **空闲会话的 MCP server**:会话还开着,但早就不用了,它名下一整套 MCP 进程一直占着内存 ![清理窗口](docs/cleanup.png) ### 一类:孤儿进程 判定规则(关键在「别误杀正在用的」): 1. 从进程沿父链一路向上走,**父进程已经不存在**才叫孤儿 2. 再排除四样:**拥有顶层窗口的**、**属于某个运行中程序的子孙**、**属于 WorkBuddy 当前会话的**、 **安全软件/系统防护进程**(360tray、QQPCTray、MsMpEng、火绒等,见下) —— 飞书这类 Electron 应用主进程不带窗口但子进程带,靠这条能正确放行 3. 会话 0(服务会话)里的进程一律不看:系统服务永远不是"用户残留" 4. 只剩真正没人认领的才进列表,默认已勾选 ### 为什么要用 EnumWindows 而不是 MainWindowHandle `Process.MainWindowHandle` 只认**可见的主窗口**。托盘类程序(360tray、输入法、各类后台助手) 的窗口是 `WS_VISIBLE=false` 的消息窗口,MainWindowHandle 恒为 0 —— 于是它们被当成 "父进程已退出的孤儿",勾了又杀不掉(360 有保护驱动,直接拒绝访问 ACCESS_DENIED), 反复刷屏。实测 360tray 拥有 12 个隐藏顶层窗口(`Q360NetmonClass`、`Qh360TraySafeId` 等)。 改成枚举**所有**顶层窗口后,又踩了一个坑:后台 node 进程带一个隐藏的 `ConsoleWindowClass` 窗口,于是真正的孤儿 node 全被"保护"起来,工具等于废了。 所以隐藏窗口要按类名过滤掉纯消息窗口(控制台、tooltip、GDI+ Hook、IME 等), **可见窗口则一律算在用**。 ### 二类:空闲会话的 MCP server WorkBuddy 每开一个会话,就为它起一整套 MCP server(weixinpay / sheetagent / cloudbase…): ``` WorkBuddy.exe(Electron 主进程) └ main └ 每开一个会话多一个「会话宿主」WorkBuddy.exe --type=main └ 每个宿主各 spawn 4 个 MCP:weixinpay-mcp / sheetagent-mcp / cloudbase-npx / cloudbase-mcp ``` MCP 的生命周期 = 会话生命周期,不是"调一次起一次、用完就退",而 `~/.workbuddy/settings.json` 里也没有任何空闲回收配置。于是**会话不关 → 整套 MCP 不退出**,一套约 340MB。 麻烦在于这些进程的**父进程始终是活的**,靠「父进程已退出」永远抓不到, 所以这里读 WMI 的 `Win32_Process.CommandLine` 来认 MCP(`Core/McpProbe.cs`), 再顺着父链找到它属于哪个会话宿主,然后按下面五道闸判断闲置: | 闸 | 规则 | 目的 | |---|---|---| | 1 | 找不到所属会话宿主 | 保守跳过 | | 2 | 会话宿主在本程序祖先链上 | 当前会话,绝不动 | | 3 | 会话宿主是**最新启动**的那个 | 视为眼前正在用的,不动 | | 4 | 会话启动至今 < `McpIdleMinutes`(默认 60) | 刚开的会话,不动 | | 5 | 最近 `McpActivityMinutes`(默认 10)内起过子进程 | 正在跑工具,不动 | 五道都不命中才算闲置,标成橙色、默认勾选。**只回收 MCP,绝不碰会话宿主** —— 被回收的 MCP 在那个会话下次调用工具时会自动重启,对话上下文不丢。 想更彻底就关掉那个会话窗口,整套一起释放。 阈值在 `%APPDATA%\DeepSeekPulse\config.json` 里改: ```json "McpIdleMinutes": 60, // 会话开多久算闲置,最小 15 "McpActivityMinutes": 10 // 多久内起过子进程算"还在跑" ``` ### 安全软件进程不再提供勾选 360 / 腾讯管家 / Defender / 火绒 等有自己的内核驱动做自我保护, `OpenProcess(PROCESS_TERMINATE)` 会被直接拒绝,就算侥幸杀掉也会被守护进程拉起。 所以按名单直接标为受保护、灰色不可勾选。如果某个进程结束失败原因是拒绝访问, 本次运行内会记住它,后续扫描不再提供勾选,避免反复弹同一个失败。 结束进程会连带整棵子进程树(列表里标「含 N 个子进程」), 确认框还会把每个子进程的名字和内存摊开给你看。 灰色行是运行中的程序(含 WorkBuddy 自己的 11 个进程),**不可勾选**; 即使绕过去,执行层还会再拦一道,直接跳过。 无界面模式,适合挂计划任务: ```bash DeepSeekPulse.exe --cleanup-auto --dry-run # 只报告,不动手 DeepSeekPulse.exe --cleanup-auto # 真清理,结果写日志 DeepSeekPulse.exe --scan --all # 只打印判定结果,排查"为什么它算残留" ``` ## GLM 套餐额度 ![GLM 页面](docs/glm.png) 设置里填 GLM Coding Plan 的 API Key,点「测试连接」当场验证,通过后会显示套餐等级和额度项数。 接口是 `GET /api/monitor/usage/quota/limit`,国内 `open.bigmodel.cn`,国际 `api.z.ai`, 路径和返回结构一致。**同一个 Key 两个站都能查**,所以主机留「自动探测」即可。 额度窗口按 `unit` 区分: | unit | 含义 | 界面标签 | |---|---|---| | 3 | 小时 | `5 小时额度`(TOKENS_LIMIT) | | 6 | 周 | `1 周额度`(TOKENS_LIMIT,部分账号不返回) | | 5 | 月 | `MCP 工具额度(月)`(TIME_LIMIT) | **刷新规则**(页面上的「X 小时后重置」直接来自接口的 `nextResetTime`,不是本地推算): - 5 小时额度:**从每次消耗时刻起滚动 5 小时重置**,不整点对齐 - 周额度:按订阅周期固定刷新 - 额度用完等周期恢复即可,不会扣账户余额 > 鉴权方式社区文档有分歧(一说要 `Bearer`,一说加了反而 401),所以代码里两种都试, > 成功的方式缓存下来。日志里搜 `[glm]` 能看到探测过程和最终用了哪种。 ## 出问题时先看日志 日志默认开启,路径:`%APPDATA%\DeepSeekPulse\log.txt`(超过 1MB 自动轮转成 `log.prev.txt`)。 三条打开方式: 1. 托盘右键 → **查看日志** 2. 托盘右键 → **设置…** → 面板里直接显示最近 200 行,旁边有「打开日志」「数据目录」 3. 程序崩溃时弹窗会问你要不要打开日志 日志格式:`时间 [级别] [模块] 内容`,模块有 `app` / `balance` / `usage` / `glm` / `login` / `popup` / `tray` / `settings` / `cleanup` / `auto` / `kill` / `crash`。 启动时会记一次环境快照(版本、OS、时区、屏幕、WebView2 版本、数据目录),异地排障先看这几行。 任何没被 catch 的异常都会落到 `[crash]` 模块;页面里的 JS 报错会以 `[popup] 页面脚本错误` 记下来,不会静默白屏。 接口改版时最有用的几个文件:`sample-amount.json` / `sample-cost.json` / `sample-glm.json`, 会把最近一次原始响应原样存下来。 ## 数据从哪来 | 数据 | 来源 | 可靠性 | |---|---|---| | DeepSeek 余额 | `GET https://api.deepseek.com/user/balance` | 官方公开接口,稳定 | | DeepSeek 今日消耗 / Token / 请求数 | `https://platform.deepseek.com/api/v0/usage/*` | **非官方内部接口**,需要网页登录态 token,随时可能失效 | | GLM 套餐额度 | `GET /api/monitor/usage/quota/limit` | 官方 web 后台在用的接口,需 Coding Plan Key | | 峰谷时段 | 本地规则 | 官方定价页规则,硬编码即可 | 今日消耗采用双轨: 1. 拿到网页登录态 → 走内部 `cost` / `amount` 接口,标签显示「接口精确」 2. 拿不到 → 用余额下降累计估算,标签显示「余额差值估算」 估算模式下**充值不会导致负值**,但也无法反映「赠送余额 + 充值余额」的分别消耗。 峰谷规则(官方定价页):高峰 = UTC 01:00–04:00、06:00–10:00,周一至周五, 折算北京时间 **09:00–12:00 / 14:00–18:00**;其余时段含周末与法定节假日为低谷,价格为高峰的一半。 法定节假日无法自动判断,需要时在 `config.json` 的 `holidayDates` 里手动加 `yyyy-MM-dd`。 ## 上手 1. 运行 `dist\DeepSeekPulse.exe` 2. 首次启动会弹设置,填 DeepSeek API Key(`sk-` 开头) 3. 想拿精确用量 → 设置里点「登录获取…」,在弹出的窗口登录 platform.deepseek.com,抓到 token 后自动关闭 4. 想显示 GLM 套餐 → 设置里填 GLM Coding Plan Key,点「测试连接」确认通了 5. 托盘图标上右键有:显示 DeepSeek 面板 / 显示 GLM 套餐面板 / 立即刷新 / 打开平台 / 登录 / 设置 / 开机自启 / 清理残留进程 / 查看日志 / 数据目录 / 退出 > Windows 默认把新托盘图标收进 `^` 隐藏区,第一次跑完记得点开 `^` 把图标拖到任务栏上常驻。 ## 命令行参数(调试用) | 参数 | 作用 | |---|---| | `--demo` | 用假数据直接弹浮窗,不发网络请求,用来核对样式 | | `--demo --no-hide` | 浮窗不因失焦收起,方便截图 | | `--settings` | 启动后立刻打开设置窗口 | | `--login` | 启动后立刻打开登录窗口(配合日志排查抓 token 问题) | | `--cleanup` | 启动后立刻打开清理窗口 | | `--cleanup-auto [--dry-run]` | 无界面清理残留进程,结果写日志,适合计划任务 | | `--scan [--all]` | 只扫描并打印每个进程的判定依据(orphan / protected / 原因),不动手 | | `--kill-pid=1234` | 精确结束指定 PID,同样会拒绝受保护进程 | ## 登录抓不到 Token 时 登录窗口底部有三个按钮:**重试** / **用系统浏览器打开** / **手动粘贴 Token**。 页面打不开(被 WAF 429、代理拦截、证书问题)时会显示一张错误页,上面带 `WebErrorStatus`、HTTP 状态码和日志路径。 兜底路径:用系统浏览器登录 `platform.deepseek.com` → F12 → Network → 点任意接口 → Request Headers 里复制 `Authorization` 的值 → 回登录窗口点「手动粘贴 Token」粘进去。 日志里搜 `[login]` 能看到完整的导航过程、每次 HTTP 状态码,以及页面 localStorage 里存了哪些 key (`localStorage keys = ...`),接口改版时靠这行定位凭证位置。 ## 文件位置 | 内容 | 路径 | |---|---| | 配置 | `%APPDATA%\DeepSeekPulse\config.json` | | 当日记账 | `%APPDATA%\DeepSeekPulse\state.json` | | WebView2 缓存 | `%LOCALAPPDATA%\DeepSeekPulse\WebView2` | > API Key 和登录 token 以**明文**存在 config.json,这是本机个人工具的取舍。介意的话改成 DPAPI 加密。 ## 构建 需要 .NET 8 SDK 和 Windows(WinForms + WebView2 运行时)。 ```bash dotnet build -c Release ``` 发布单文件: ```bash dotnet publish -c Release -r win-x64 ^ --self-contained false -p:PublishSingleFile=true ^ -p:IncludeNativeLibrariesForSelfExtract=true -o dist ``` `--self-contained false` 依赖系统已装 .NET 8 桌面运行时;要免依赖改成 `true`(体积约 150MB)。 `Web\` 目录必须和 exe 放在一起,改样式不用重新编译。 ## 结构 ``` Core/ 配置、路径、峰谷判定、快照模型、GLM 模型、本地记账、残留进程扫描 Data/ 余额接口、用量内部接口、GLM 套餐接口 UI/ 托盘宿主、浮窗、设置、登录抓取、清理窗口 Web/ 浮窗前端(HTML/CSS/JS,两个页签) ``` 浮窗 UI 走 WebView2 + 本地虚拟主机映射(`https://pulse.local/`), 改样式只动 `Web/` 目录即可,C# 只在推送数据时有耦合。 ## 已知限制 - Windows 11 移除了第三方 DeskBand,无法真嵌进任务栏;这里是「托盘图标 + 贴靠浮窗」方案 - 内部用量接口一旦改版,会自动降级到余额差值估算,不会崩溃 - 托盘图标只有 16×16 左右,金额做了压缩(`233` / `1.3k`) - 残留进程判定依赖父进程关系:如果某个程序的启动器退出后主进程仍在正常服务(很常见), 只要它有可见窗口就会被正确放行;纯后台无窗口但有隐藏消息窗口的(托盘/助手类)也算在用; 只有**完全没有窗口的纯后台服务**,父进程退出后会被视为孤儿,勾选前请确认那个名字你不需要 - 空闲 MCP 的判定只能靠「会话启动时长 + 近期子进程活动 + 最新会话」这三个代理信号, 无法真正知道你在看哪个会话。万一把你正在用的那个会话的 MCP 回收了,代价是那个会话 下一次 MCP 工具调用要重新拉起(秒级),不会丢上下文 - 判断 MCP 需要读进程命令行,走 WMI(`System.Management`)。个别系统上 WMI 被禁用时 会退化成只清孤儿进程,日志里有 `读取进程命令行失败` 的 WARN