前阵子一直在调 DeepSeek 的 API,模型能力没得说,但有个问题特别烦人:每跑一轮脚本,都要打开浏览器、登录开放平台、点进控制台,才能看到 token 消耗了多少、余额还剩多少。次数一多我就受不了了。与其反复手动查,不如直接做个桌面小面板,一键刷新看用量。
正好我手上一直是 Windows 环境,PowerShell 和 WinForms 又是系统自带的东西,不需要额外装 Python、Node 那一套,所以我用 PowerShell + WinForms 搓了一个 DeepSeek Token 用量桌面面板。做完之后,每天打开电脑顺手点一下就能看到账户余额、今天花了多少 token、哪个模型烧钱最多,爽很多。
这篇文章不吹不黑,就把整个实现过程拆开讲清楚:从 API 数据从哪来、WinForms 界面怎么搭、PowerShell 请求接口怎么解析,到开机自启、日志统计、乱码和鉴权踩坑,全部按我实际操作的顺序记录下来。适合两类人参考:一是想用 PowerShell 快速写一个正经 GUI 工具的朋友,二是想高强度用 DeepSeek API 并做好预算管理的开发者。
1. 为什么选 PowerShell + WinForms 做桌面面板
1.1 需求很具体:我想一眼看到 Token 消耗
先说清楚这个面板要解决什么问题。DeepSeek API 的计费核心是 token,每次调用模型都会在响应的 usage 字段里返回 prompt_tokens、completion_tokens、total_tokens。但这些消耗是分散在一次次请求里的,如果只是偶尔调几次,手动看后台也没啥;可一旦开始批量测试 prompt、跑数据处理任务,或者把 API 接进自动化流程,token 消耗就成了需要持续盯住的指标。
我最初的诉求其实很简单:
- 打开一个窗口,就能看到当前账户还有多少余额。
- 能看到累计消耗的 token 总数,最好能按天分组。
- 能自动刷新,不用每次手动点刷新按钮。
要做到这三点,最安分的方式是写代码调 DeepSeek 官方 API。但我不想为了一个十几行的统计工具去装 Python 环境,也不想把 Electron 那套动辄几百 MB 的依赖搬过来。于是就把目光放到了 Windows 自带能力上。
1.2 为什么没选 Python、Electron 而是 PowerShell
坦率地说,我以前也纠结过这类工具该用什么技术栈。Python + Tkinter 写起来很简单,可前提是目标机器上有 Python 3.10+,还要处理 pip 依赖;Electron 界面漂亮,但为了显示几个数字就拉起 Chromium,实在是杀鸡用牛刀。
PowerShell + WinForms 的好处非常实际:
- 系统自带,只要是 Windows 10/11 都有 PowerShell 5.1,WinForms 程序集也随 .NET Framework 预装。
- 脚本即源码,复制一个 .ps1 文件就能运行,改一行配置立刻生效。
- 对 REST API 的支持很完善,
Invoke-RestMethod直接读 JSON,解析成本低。 - 后续接计划任务、开机自启、邮件通知都很顺。
当然缺点也明显:WinForms 的界面风格比较旧,功能复杂时不好维护,跨平台更是无从谈起。但做内部工具、个人小面板,这些缺点完全能接受。我的判断标准很简单:能在 10 分钟内跑通、能稳定运行、后续好改,就足够了。
1.3 面板功能规划
动工前我列了一个最小功能清单,尽量克制,不做多余的东西:
| 区域 | 功能 | 说明 |
|---|---|---|
| 顶部 | 余额显示 | 显示 API Key 对应的总余额、赠送余额、充值余额 |
| 中部 | Token 用量汇总 | 显示累计 prompt tokens、completion tokens、总数 |
| 中部表格 | 模型维度消耗 | 按模型分组统计最近一段时间的消耗 |
| 底部 | 状态栏 | 最近刷新时间、请求状态、错误信息 |
| 全局 | 刷新按钮 | 点击立即重新请求接口并刷新界面 |
| 全局 | 自动刷新 | 用 Timer 每 60 秒自动查询一次 |
这个清单看起来简单,但实际落地时有不少细节,比如数据从哪来、日志怎么记录、界面怎么布局,下面一章开始逐步拆解。
2. 准备工作:API Key、接口和数据结构
2.1 先拿到 DeepSeek 的 API Key
在动手写代码之前,必须先把 API Key 准备好。去 DeepSeek 开放平台注册账号、完成实名认证,然后在 API Keys 页面创建密钥。密钥通常以sk-开头,创建时机要留意:很多平台只在创建时完整显示一次,之后就没法再看到明文了。
拿到 Key 之后,务必养成一个习惯:不要直接写死在脚本里,更不要截图发到群里。我会在后面的“配置抽离”小节讲具体做法,这一阶段先记住一个原则——API Key 就是钱,泄露了等于别人能用你的配额,花钱你买单。
顺带一提,平台通常会送一点体验额度或者有免费 token 活动,新账号可以先拿这点免费额度做测试,确认 API 调用链路没问题,再考虑充值。我就是先用赠送额度把接口调通了,避免一开始就产生费用。
2.2 用量数据不是“一个接口全给”,得自己攒
很多朋友刚上手时容易有一个误区:以为 DeepSeek 会提供一个“历史 token 用量明细”接口,直接调用就能把所有消耗拉出来。根据我的实测,官方目前主要提供的是余额查询接口,以及每次模型响应中的 usage 字段。
也就是说,我能直接拿到两类数据:
第一类是账户余额。调用鉴权后的余额接口,返回的信息包含total_balance、granted_balance、topped_up_balance等字段。简单说就是总余额、赠送余额、充值余额。
第二类是单次请求的 token 消耗。每次调用对话补全接口,返回的 JSON 里都会带一段usage,结构大致如下:
{ "prompt_tokens": 25, "completion_tokens": 118, "total_tokens": 143, "prompt_tokens_details": { "cached_tokens": 0 } }如果做的是流式请求,usage 通常会出现在最后一个 chunk 里。如果中间做了重试,重试的每一次都可能产生 token。这些细节决定了统计是否准确。
因为没有官方历史用量接口,我采取了最直接的方案:自己写日志。我在每次成功调用模型后,把响应里的 usage 追加到一个本地 JSON 文件里。桌面面板启动时读取这个文件,做汇总和展示。这样既不需要后台数据库,也能保证数据统计的连续性。
2.3 我设计的日志结构
我把日志文件命名为token_usage_log.json,放在脚本同目录下,结构设计成:
{ "records": [ { "timestamp": "2025-04-07T10:23:45", "model": "deepseek-chat", "prompt_tokens": 320, "completion_tokens": 480, "total_tokens": 800 } ] }每个字段的用途:
timestamp:记录请求发生的时间,用于按天、按小时做维度统计。model:记录模型名,用于分析不同模型的消耗量。prompt_tokens:输入 token 数,对应计费中的输入部分。completion_tokens:输出 token 数,对应计费中的输出部分。total_tokens:两者之和,方便一眼看出单次消耗量。
我特意没有在日志里记录请求体和完整响应,只记录 usage 相关字段,一是控制日志文件体积,二是降低敏感信息泄露风险。如果你用了更长上下文的模型,单次请求可能上万 token,日志快速增长,这时可以考虑按天滚动归档,后面会讲到。
3. 界面先行:WinForms 控件的选择与布局
3.1 加载 WinForms 的前提
PowerShell 里用 WinForms,第一步是加载程序集。在 Windows PowerShell 5.1 和 PowerShell 7 中都可以这样写:
Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing其中System.Windows.Forms是窗体控件相关,System.Drawing负责颜色、字体、位置等图形相关操作。
然后创建一个主窗体对象,设置标题、尺寸、位置:
$form = New-Object System.Windows.Forms.Form $form.Text = "DeepSeek Token 用量面板" $form.Size = New-Object System.Drawing.Size(680, 520) $form.StartPosition = "CenterScreen" $form.MaximizeBox = $false这些属性和 C# WinForms 项目几乎一一对应,只要稍微懂一点 WinForms 的基础概念,就能在 PowerShell 里照葫芦画瓢。
3.2 画窗体:哪些控件够用
面板主体我用了这几类控件:
System.Windows.Forms.Label:显示余额文字、token 汇总数据。System.Windows.Forms.ListView:按模型维度显示消耗明细,性能比 DataGridView 在少量数据时更轻。System.Windows.Forms.Button:手动刷新按钮。System.Windows.Forms.Timer:定时自动刷新。System.Windows.Forms.StatusStrip或一个额外的 Label:显示状态信息。
创建 Label 的基本套路如下:
$lblBalance = New-Object System.Windows.Forms.Label $lblBalance.Location = New-Object System.Drawing.Point(20, 20) $lblBalance.Size = New-Object System.Drawing.Size(200, 30) $lblBalance.Text = "余额:--" $form.Controls.Add($lblBalance)布局时我会算好坐标:顶部一排放余额标签,下方放大字号的总消耗标签,再往下放 ListView,底部放状态栏和按钮。坐标制在代码里看起来有点原始,但胜在直观、可控。
3.3 事件绑定与 UI 防卡死
WinForms 是事件驱动的,按钮点击、窗体加载都要绑定事件。在 PowerShell 里绑定事件比 C# 稍微绕一点,核心是用Add_Click这类事件方法:
$btnRefresh.Add_Click({ Write-Host "clicked" })踩坑提醒:如果事件处理代码里包含耗时操作(比如网络请求),WinForms 界面会“卡死”,窗口拖不动、按钮点不了。这是因为操作都在 UI 线程上执行。解决办法有两个方向:
第一,简单场景用System.Windows.Forms.Timer定时触发,并在触发时先设置状态栏文字“刷新中…”,请求结束后恢复。
第二,如果必须是用户点击后立刻执行的耗时请求,至少要把网络请求放在后台线程或者Runspace中运行,完成后把数据传回主线程更新控件。
面板项目规模小,我用了 Timer 方式,同时把请求逻辑封装成一个函数,避免在事件代码里写太多业务逻辑。
4. 用 PowerShell 调 DeepSeek API:核心代码拆解
4.1 发请求前的 TLS 与请求头
Windows PowerShell 5.1 默认使用的 TLS 版本可能比较低,而很多云服务商要求至少 TLS 1.2,否则连接会被拒绝。遇到“请求被服务器关闭”这类问题时,多数情况是因为 PowerShell 还在用老旧的 TLS 协议。
保险起见,我在脚本开头统一设置:
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12然后是请求头。调用 DeepSeek API 需要在 HTTP 头里带上鉴权信息:
$headers = @{ "Authorization" = "Bearer $apiKey" "Content-Type" = "application/json" }这里$apiKey是从配置文件中读出来的字符串,格式是sk-...。保持Bearer前缀和 key 之间的空格,拼错是最常见的 401 原因。
4.2 余额查询接口的调用与解析
余额查询用 GET 请求即可,我先封装一个函数:
function Get-DeepSeekBalance { param($ApiKey) $headers = @{ "Authorization" = "Bearer $ApiKey" } $response = Invoke-RestMethod -Uri "https://api.deepseek.com/user/balance" -Method Get -Headers $headers return $response }实际返回的 JSON 结构类似:
{ "is_available": true, "balance_infos": [ { "currency": "CNY", "total_balance": "110.00", "granted_balance": "10.00", "topped_up_balance": "100.00" } ] }is_available标识账户当前是否可用,balance_infos是一个数组,里面可能有多条币种记录。我取balance_infos[0]展示即可:
$balanceInfo = $response.balance_infos[0] $total = $balanceInfo.total_balance $granted = $balanceInfo.granted_balance $toppedUp = $balanceInfo.topped_up_balance注意:接口返回的余额字段是字符串,不是数字,如果你要拿来做阈值判断或告警,要用[double]::Parse()做类型转换,否则字符串比较会出现“100 < 20”这种错误。
4.3 把 JSON 变成界面上的数据
从日志文件读 token 用量数据后,我要做两件事:汇总总数、按模型分组。
汇总总数的方法很简单:
$totalPrompt = ($log.records | Measure-Object -Property prompt_tokens -Sum).Sum $totalCompletion = ($log.records | Measure-Object -Property completion_tokens -Sum).Sum $totalTokens = ($log.records | Measure-Object -Property total_tokens -Sum).Sum按模型分组则需要用管道配合 Group-Object:
$grouped = $log.records | Group-Object model | ForEach-Object { [PSCustomObject]@{ Model = $_.Name RequestCount = $_.Count PromptTokens = ($_.Group | Measure-Object -Property prompt_tokens -Sum).Sum CompletionTokens = ($_.Group | Measure-Object -Property completion_tokens -Sum).Sum TotalTokens = ($_.Group | Measure-Object -Property total_tokens -Sum).Sum } }拿到这些数据后,填充 UI 控件就非常直接了。Label 的 Text 属性一改,ListView 用 Items.AddRange 逐行添加就好。
4.4 串起整个刷新流程
一个完整的刷新流程大概是这样的:
function Update-Panel { $statusLabel.Text = "正在刷新..." $form.Refresh() try { $balance = Get-DeepSeekBalance -ApiKey $config.apiKey $lblBalance.Text = "总余额:$($balance.balance_infos[0].total_balance) 元" Update-TokenSummary $statusLabel.Text = "刷新成功:" + (Get-Date -Format "HH:mm:ss") } catch { $statusLabel.Text = "刷新失败:" + $_.Exception.Message } }这里我特意在刷新前后调用$form.Refresh(),否则界面上的文字不会及时变化,看起来像没点中按钮。这个小细节第一次写很容易忽略。
5. 让面板真正“可用”:统计、自启、配置化
5.1 日志汇总:Token 用量的日维度统计
面板只有“总消耗”还不够,我希望它能告诉我今天消耗了多少 token。拆开来看,就是按照timestamp的日期部分进行过滤。
如果日志结构是数组records,可以用Where-Object过滤当天记录:
$today = (Get-Date).ToString("yyyy-MM-dd") $todayRecords = $log.records | Where-Object { $_.timestamp.StartsWith($today) }然后对$todayRecords做求和,逻辑和前面类似。因为我是把日志文件放在本地,数据量不大,这种遍历在启动时完全无压力。
建议给记录按天归档。你可以写一个简单逻辑:读取日志时,如果发现记录数超过 5000 条,就按月份拆分到history/目录,当前目录只保留最近一个月的数据。避免日志文件无限增大,导致后续读取变慢。
5.2 自动刷新与定时任务
我用的是 WinForms 自带的System.Windows.Forms.Timer,注意它和System.Timers.Timer不一样,前者需要消息循环才能触发,放在 WinForms 里正合适。
创建定时器:
$timer = New-Object System.Windows.Forms.Timer $timer.Interval = 60000 $timer.Add_Tick({ Update-Panel }) $timer.Start()这样每隔 60 秒自动调用一次刷新流程。但要注意,如果每次刷新都请求余额接口,频率太高可能触发平台限流策略,因此一般建议把 Interval 设置成 300000 毫秒(5 分钟),或者刷新后记录时间,距离上次不足 30 秒就跳过。
5.3 开机自启的两种稳妥姿势
做完面板后,我希望它开机自动运行,遂把脚本做成了带窗体的自启小工具。Windows 下的常见方案有两种:
第一种是启动文件夹方式。把脚本的快捷方式放到shell:startup文件夹里,即C:\Users\你的用户名\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup。优点是简单直观,缺点是如果脚本路径带中文和空格,快捷方式目标必须用引号包好,否则会找不到文件。
第二种是注册表 Run 键。在HKCU:\Software\Microsoft\Windows\CurrentVersion\Run下创建一个字符串值,数据填powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass -File "D:\script\panel.ps1"。注意两点:一是-WindowStyle Hidden可以避免启动时弹出黑框,二是-ExecutionPolicy Bypass或者先通过Set-ExecutionPolicy -Scope CurrentUser RemoteSigned调整策略,否则可能被默认执行策略拦下。
注册表方式我实际用了很久,稳定可靠。但如果你用的是 PowerShell 7,注意把启动路径从powershell.exe改成pwsh.exe,否则会用 Windows PowerShell 5.1 去跑,可能缺模块。
5.4 配置抽离:API Key 不写死在代码里
写脚本最忌讳的就是把密钥硬编码。我单独建了一个config.json:
{ "apiKey": "sk-你申请到的Key", "logFile": "token_usage_log.json", "refreshIntervalSec": 60 }脚本读取配置用Get-Content -Raw | ConvertFrom-Json即可:
$config = Get-Content -Raw -Path "config.json" | ConvertFrom-Json $apiKey = $config.apiKey把 API Key 放到独立配置文件里,好处很多。备份脚本时不用注意脱敏,也能更方便地在多台机器之间复制配置。但请格外注意:config.json不能提交到公开 Git 仓库,否则等于公开自己的密钥。我一般会加一条.gitignore规则,或者干脆不把 config.json 纳入版本管理。
6. 我踩过的坑和排查思路
6.1 PowerShell 5.1 的编码与命令兼容问题
前面提到热搜里有“deepseek配置windows powershell乱码”,这确实是很常见的问题。Windows PowerShell 5.1 的默认编码不是 UTF-8,而是中文系统下的 GBK。当接口返回 UTF-8 编码的中文字符时,控制台直接显示乱码。
真正的撞墙经历是这样的:调用余额接口后,把返回的 JSON 直接打印到控制台,发现中文全是“锟斤拷”,第一反应还以为是 API 数据坏了,后来确认是编码问题。
解决方法是在脚本开头设置控制台编码:
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8这样控制台输出就能正确显示 UTF-8 内容。在 WinForms 界面里,Label 的文本一般不会受控制台编码影响,但如果日志文件写入编码不一致,界面读取时也可能出现乱码,所以读写日志文件时统一使用 UTF-8 编码。
另外再说一下命令链运算符。经常有人喜欢在 PowerShell 里用cmd1 && cmd2,但 PowerShell 5.1 并不支持&&运算符,这是 PowerShell 7 才引入的特性。如果网上教程写的代码用了&&,而你的环境是 5.1,执行时会直接报语法错误。替代写法是用分号;:
Update-Panel; Write-Host "done"或者在需要判断前一个命令是否成功时用if ($LASTEXITCODE -eq 0)。
6.2 请求失败、Token 失效到底要看哪些字段
接口调不通是最让人头疼的。我整理了几个常见错误及排查思路,做成一个速查表:
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| HTTP 401:Invalid API Key | API Key 错误、过期、前缀缺失 | 检查 config.json 中的 key 是否完整,确认以 sk- 开头 |
| HTTP 429:Rate Limit | 请求频率过高或余额不足 | 降低刷新频率,检查账户余额 |
| HTTP 403:Forbidden | 账户被限制或地区问题 | 登录开放平台查看账户状态 |
| 连接被关闭 | TLS 版本过低 | 设置[Net.ServicePointManager]::SecurityProtocol = Tls12 |
| 响应中文乱码 | 控制台编码问题 | 设置[Console]::OutputEncoding = UTF8 |
关于 token 失效,如果你的业务系统里有 JWT 登录,那需要注意 token 类型和续签机制。但对于面板本身,DeepSeek 的 API Key 属于长期密钥,一般不会自动过期,遇到 401 首先检查是否复制错了字符、是否多了空格。真正的重点是不要把 Key 拼到 URL 里,这样既容易泄露,也容易被系统日志记录。
6.3 WinForms 界面卡死与进度提示
我在“事件绑定与 UI 防卡死”里简单提过这个问题,但这里必须再放大一次。
事件回调里如果执行Invoke-RestMethod,整个窗口会变成“未响应”状态,直到请求返回。这个在网络缓慢或接口超时时特别明显,用户疯狂点按钮,体验极差。
我的解决办法是:
- 点击刷新时先把按钮禁用,
$btnRefresh.Enabled = $false。 - 界面上状态栏改成“正在刷新...”。
- 刷新结束后,在
finally中恢复按钮可用。 - 连接超时设置不能省,
Invoke-RestMethod的-TimeoutSec参数设为 30,避免无限等待。
$response = Invoke-RestMethod -Uri $url -Headers $headers -Method Get -TimeoutSec 30如果你希望请求完全不阻塞界面,更正式的做法是用 PowerShell Runspace 或线程,但考虑到面板的代码量,我觉得采用“禁用按钮 + 超时 + 状态提示”这套组合拳已经足够优雅。
6.4 安全习惯:别把 API Key 泄露到日志
最后一条经验,也是我特别想强调的。PowerShell 的-Verbose、-Debug参数,或者脚本中未加处理的$Error,有时会把完整的 URL 或请求头打印出来。如果请求头里带着Authorization: Bearer sk-xxx,而你又把输出重定向到日志文件,那么密钥就等于裸奔了。
我处理安全的几个习惯:
- 日志里绝不记录请求头。
- 错误信息用
$_.Exception.Message,而不是把整个$_对象序列化。 - config.json 不参与 git 版本管理。
- 面板界面上对 API Key 只显示后四位,例如
sk-****1234,方便确认用的是哪把钥匙,又不泄露完整内容。
另外,如果以后要对接其他服务,比如把 token 用量超阈值提醒发送到企业微信或钉钉机器人,也要注意机器人的 Webhook 地址本身相当于一个密钥,别随手贴到博文或公开仓库里。
最后分享一点我自己的使用体会
这个小工具从动手写到稳定用,其实只花了一个下午。但给我带来的改变是实打实的:现在跑批量任务时,我不用再去猜“这次实验花了多少钱”,面板上数字一目了然,续费决策也果断多了。我用得最顺手的是把日志按模型分组这个功能,可以清楚看到不同任务的成本占比,之后调整 prompt 策略就很有针对性。
如果你也想做一个类似的面板,不用照搬我的代码。把 API 请求、日志统计、WinForms 展示这三块拆开,很容易根据自己的需求扩展。比如加上按时间范围筛选,或者把某一天的消费折线图用原生控件画出来,甚至做一个阈值告警——这些都是在现有框架上添砖加瓦的事。
如果你已经在自己项目里实现了更好的 token 统计方式,欢迎告诉我。工具虽小,但这种“今天不把重复劳动干掉,明天就会继续重复”的想法,我觉得是做开发最值得保持的一点劲头。