credgauge 实战:用 Electron 把 AI 服务余额钉在桌面(含 Windows 静默启动踩坑全过程)
摘要:本文记录从零开发一个桌面挂件工具 credgauge 的完整过程。该工具用 Electron 实现桌面置顶小窗,实时轮询 DeepSeek 官方 API 和 ApiNebula 中转站的余额信息,每 60 秒自动刷新。文章涵盖需求分析、架构选型、配置与隐私方案、三种启动方式(终端/双击/开机自启)的实现,并重点复盘了 Windows 桌面开发中四个典型坑:VBScript 中文编码、spawn 链路里的 cmd 窗口、Nushell PATH 继承、开机自启方案选型。适合对 Electron 桌面应用、Node.js CLI、Windows 系统编程感兴趣的开发者阅读。
适合阅读人群:Electron 初学者 / Node.js 桌面工具开发者 / 经常使用 AI API 的开发者
涉及技术:Electron、Node.js (ESM)、VBScript、Windows 启动机制、IPC 通信、零依赖 .env 加载
阅读收获:
-
掌握 Electron 无边框置顶挂件的实现方式
-
学会 Windows 下真正的"无终端窗口"静默启动方案
-
理解 VBScript 编码陷阱与 Node.js spawn 的 shell 选项差异
-
获得一个可直接使用的开源小工具
@TOC
一、需求背景:为什么需要余额挂件
1.1 痛点描述
日常使用 AI API 的开发者大概都有过这样的体验:
-
打开 DeepSeek 控制台查余额 → 登录、点菜单、等加载
-
切到中转站 ApiNebula 看额度 → 又一轮登录、点菜单、等加载
-
回到 IDE 继续写代码
-
一小时后重复上述动作
尤其是使用中转站的场景,中转站余额与官方余额是两套独立体系,必须分别查看。一旦中转站额度耗尽,请求开始报 401 Unauthorized,才发现该充值——这种"事后发现"的体验很糟。
1.2 需求拆解
把这个问题抽象成具体需求:
| 需求点 | 解决方案 |
|---|---|
| 多服务余额聚合展示 | 统一 provider 接口,并发查询 |
| 常驻可见 | Electron 桌面置顶小窗 |
| 自动刷新 | 定时轮询(60s) |
| 不打扰 | 半透明、无边框、可拖动 |
| 低门槛启动 | 双击即开、支持开机自启 |
| 配置安全 | .env 本地存储,不进版本库 |
这就是 credgauge 的设计起点。
二、整体设计
2.1 技术选型
| 层 | 选型 | 理由 |
|---|---|---|
| 运行时 | Node.js 18+ (ESM) | 内置 fetch,无需 axios |
| 桌面框架 | Electron | 跨平台、窗口可控性强 |
| 配置 | .env(自实现加载) | 零依赖,不引 dotenv |
| 启动器 | VBScript + Node | Windows 原生,无终端窗口 |
| 开机自启 | 启动文件夹快捷方式 | 比 Electron 官方 API 更可控 |
核心原则:零运行时依赖。整个 package.json 的 dependencies 只有 electron 一个,其余全部用 Node 内置模块。
2.2 项目结构
credgauge/ ├── start.vbs # 双击静默启动入口(无终端窗口) ├── .env.example # 配置模板(不含真实凭证) ├── .env # 真实配置(.gitignore 排除) ├── package.json └── src/ ├── index.js # 库入口,导出各 provider ├── cli.js # CLI(查询/挂件/开机自启) ├── cre.js # 简写入口(交互式配置 + 启动) ├── silent.js # 静默启动入口(供 start.vbs) ├── env.js # .env 加载器(零依赖) ├── setup.js # 交互式配置引导 ├── providers/ │ ├── deepseek.js # DeepSeek API 封装 │ └── apinebula.js # ApiNebula (New API) 封装 └── widget/ ├── main.js # Electron 主进程 ├── preload.js # IPC 桥 └── renderer/ └── index.html # 挂件 UI
2.3 数据流
┌─────────────┐ fetch ┌──────────────┐ │ DeepSeek API │ ──────────► │ │ └─────────────┘ │ │ │ widget/main │ ── IPC ──► renderer ┌─────────────┐ fetch │ (并发查询) │ │ ApiNebula API│ ──────────► │ │ └─────────────┘ └──────────────┘ ▲ │ │ 60s 轮询 ▼ └──────────────────── renderer 渲染
每个 provider 返回统一格式 { name, balance, currency, available },主进程并发查询所有已配置的服务,通过 IPC 推给渲染进程。
三、核心实现
3.1 Provider 统一接口
两个 provider 都返回相同结构,便于主进程统一处理:
// 返回格式
{
name: "DeepSeek", // 服务名
balance: 0.39, // 余额数值
currency: "CNY", // 货币
available: true // 是否可用
}
DeepSeek 调用官方 /user/balance 接口,ApiNebula 调用 New API 架构的 /api/user/self 接口。两者都用 Node 内置 fetch,无需引入 HTTP 库。
3.2 Electron 主进程:无边框置顶小窗
挂件窗口的关键参数:
function createWindow() {
const count = configuredCount();
// 根据已配置服务数量自适应高度
const height = count === 0 ? 56 : count === 1 ? 56 : 80;
win = new BrowserWindow({
width: 170,
height,
frame: false, // 无边框
transparent: true, // 透明背景
resizable: false,
alwaysOnTop: true, // 置顶
skipTaskbar: true, // 不显示在任务栏
show: false,
webPreferences: {
preload: join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
win.loadFile(join(__dirname, "renderer", "index.html"));
win.once("ready-to-show", () => {
win.show();
refresh();
});
ipcMain.on("widget:close", () => app.quit());
ipcMain.on("widget:refresh", () => refresh());
}
定时轮询用 setInterval,60 秒一次:
app.whenReady().then(() => {
createWindow();
timer = setInterval(refresh, 60_000);
});
app.on("window-all-closed", () => {
if (timer) clearInterval(timer);
app.quit();
});
3.3 零依赖 .env 加载器
为了不引入 dotenv,手写了一个 20 行的加载器。支持注释、去引号、不覆盖已存在的环境变量:
// src/env.js
import { readFileSync, existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
export function loadEnv() {
const dir = dirname(fileURLToPath(import.meta.url));
const envPath = join(dir, "..", ".env");
if (!existsSync(envPath)) return;
const content = readFileSync(envPath, "utf-8");
for (const line of content.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue; // 跳过注释和空行
const eq = trimmed.indexOf("=");
if (eq === -1) continue;
const key = trimmed.slice(0, eq).trim();
let val = trimmed.slice(eq + 1).trim();
// 去除首尾引号
if ((val.startsWith('"') && val.endsWith('"')) ||
(val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1);
}
// 不覆盖已存在的环境变量
if (key && !(key in process.env)) process.env[key] = val;
}
}
3.4 隐私保护:.env 不进版本库
这是公开项目必须严守的底线。.gitignore 里排除 .env,仓库只提交 .env.example 模板:
# .gitignore .env .env.local
# .env.example(模板,不含真实凭证) DEEPSEEK_API_KEY=sk-your-deepseek-key APINEBULA_BASE_URL=https://apinebula.ai APINEBULA_TOKEN=your-system-token APINEBULA_USER_ID=your-user-id
每次提交前用 git ls-files .env 确认未被跟踪,是个好习惯。
四、三种启动方式的实现
这是本项目打磨最久的部分。一个好工具应该"打开就用",而不是让用户每次开终端敲命令。
4.1 终端启动(首次配置用)
cre # 简写命令,首次会交互式引导配置 credgauge widget # 等同效果
首次运行会引导填入 API Key / 令牌,写入 .env。配置一次,后续不用再管。交互逻辑用 Node 内置 readline/promises 实现,零依赖。
4.2 双击启动(日常用)
项目根目录的 start.vbs,双击即可静默启动挂件,不弹出任何终端窗口。
' start.vbs —— Silent launcher for credgauge widget
Dim fso, sh, here
Set sh = CreateObject("WScript.Shell")
Set fso = CreateObject("Scripting.FileSystemObject")
here = fso.GetParentFolderName(WScript.ScriptFullName)
sh.CurrentDirectory = here
sh.Run "node src\silent.js", 0, False ' 0 = SW_HIDE 隐藏窗口
Set sh = Nothing
Set fso = Nothing
配合 src/silent.js(跳过交互配置,直接拉起 Electron):
// src/silent.js
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { loadEnv } from "./env.js";
import electronPath from "electron"; // 直接拿到 electron.exe 绝对路径
loadEnv();
const __dirname = dirname(fileURLToPath(import.meta.url));
const mainFile = join(__dirname, "widget", "main.js");
const child = spawn(electronPath, [mainFile], {
stdio: "ignore",
shell: false, // 关键:不经过 shell,避免 cmd 窗口
windowsHide: true, // 隐藏子进程窗口
detached: true, // 脱离父进程
});
child.on("error", () => process.exit(1));
child.unref();
4.3 开机自启
credgauge autostart on # 开启 credgauge autostart status # 查看状态 credgauge autostart off # 关闭
原理:在 Windows 启动文件夹创建指向 start.vbs 的快捷方式。
// cli.js 中的 cmdAutostart 实现(精简版)
function cmdAutostart(sub) {
const lnkPath = join(getStartupDir(), "credgauge.lnk");
const vbsPath = join(__dirname, "..", "start.vbs");
if (sub === "on") {
// 用 PowerShell 的 WScript.Shell COM 对象生成快捷方式
const ps = `$ws=New-Object -ComObject WScript.Shell;
$s=$ws.CreateShortcut('${lnkPath}');
$s.TargetPath='wscript.exe';
$s.Arguments='"${vbsPath}"';
$s.WindowStyle=7;
$s.Save()`;
execSync(`powershell -NoProfile -Command "${ps}"`, { stdio: "ignore" });
} else if (sub === "off") {
execSync(`powershell -NoProfile -Command "Remove-Item '${lnkPath}' -Force"`,
{ stdio: "ignore" });
}
}
快捷方式位于:%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\credgauge.lnk
五、踩坑记录(重点)
开发过程中踩了四个 Windows 桌面开发的经典坑,逐个记录。
5.1 坑一:VBScript 中文注释导致"缺少对象"
现象:双击 start.vbs 报错 缺少对象 'sh'。
第一版代码:
' 双击静默启动 credgauge 挂件(无终端窗口)
Set sh = CreateObject("WScript.Shell")
排查过程:报错指向 sh,但 Set sh = ... 明明写了。折腾半天才意识到——VBScript 默认按 ANSI 编码解析,而文件存成 UTF-8。中文注释是多字节字符,ANSI 解析时字节错位,导致 Set sh = ... 这行没被正确识别,后续用到 sh 就报缺少对象。
修复:注释和字符串全改成 ASCII。
' Silent launcher for credgauge widget (no console window)
Dim fso, sh, here
Set sh = CreateObject("WScript.Shell")
经验:VBScript 里想写中文,要么存成 UTF-16 LE with BOM,要么干脆别写中文。这个坑在英文资料里几乎找不到,中文开发者特别容易踩。
5.2 坑二:终端窗口怎么都去不掉
现象:双击 start.vbs 后挂件起来了,但伴随一个一闪而过的黑色 cmd 窗口。
原因有两层:
-
start.vbs用sh.Run "cmd /c node src\silent.js", 0, False—— 经由cmd起子进程,cmd 窗口闪现 -
silent.js用spawn(electronPath, ..., { shell: true })——shell: true又起了一个 cmd
关键认知:Node.js 的 spawn 在 Windows 上,shell: true 会走 cmd.exe /c,即使 windowsHide: true 也可能闪窗。要彻底无窗口,必须 shell: false 并直接传 .exe 路径。
最终方案:
// silent.js —— 直接 import electron 包,拿到 exe 绝对路径
import electronPath from "electron";
const child = spawn(electronPath, [mainFile], {
stdio: "ignore",
shell: false, // 不经过 shell
windowsHide: true,
detached: true,
});
child.unref();
' start.vbs —— 直接调用 node,不再经由 cmd sh.Run "node src\silent.js", 0, False
启动链路变成 wscript → node(SW_HIDE)→ electron.exe,全程无 cmd 窗口。
经验:electron 这个 npm 包的默认导出就是 electron.exe 的绝对路径,很多人不知道这点,还在用 node_modules/.bin/electron(那个是 shell 脚本,会起 cmd)。
5.3 坑三:Nushell 找不到全局命令
现象:cre 在 cmd / PowerShell 正常,但在 Nushell 报 Command cre not found。
原因:cre 通过 npm link 注册到 npm 全局 bin 目录,但 Nushell 不继承这个目录到 PATH。
修复:在 config.nu 手动追加:
$env.PATH = ($env.PATH | append "C:/Users/<用户名>/AppData/Roaming/.../node")
关键陷阱:路径必须用正斜杠。Windows 反斜杠在 Nushell 字符串里是转义符,写反斜杠会报:
Error: × Invalid literal ╭─[config.nu:899:36] │ $env.PATH = ($env.PATH | append "C:\Users\...") · ─┬─ · ╰── unrecognized escape after '\' in string
经验:跨 shell 兼容是个无底洞。cmd、PowerShell、Nushell、Git Bash 的 PATH、转义、引号规则都不一样。能避开就避开,避不开就老老实实查文档。
5.4 坑四:开机自启方案选型
Electron 官方 API 的局限:
// 官方 API,看似优雅
app.setLoginItemSettings({ openAtLogin: true });
实际用起来有两个问题:
-
Windows 上稳定性一般,有时不生效
-
它启动的是 electron 进程,无法接我这套 VBS 静默启动链路
最终方案:弃用官方 API,改用 Windows 启动文件夹 + 快捷方式 的老办法。
| 方案 | 优点 | 缺点 |
|---|---|---|
| Electron 官方 API | 跨平台、代码少 | Windows 不稳、无法接 VBS 链路 |
| 启动文件夹快捷方式 | 可靠、可控、能接 VBS | 仅 Windows、需写 COM 代码 |
选了后者。用 PowerShell 调 WScript.Shell COM 对象生成 .lnk,几行代码搞定,从此开机自启稳如老狗。
六、安装与使用
6.1 安装三步走
git clone https://github.com/w-zjj/credgauge.git cd credgauge npm install
6.2 配置凭证
Copy-Item .env.example .env # 编辑 .env 填入你的凭证
需要配置的内容:
| 服务 | 变量 | 获取方式 |
|---|---|---|
| DeepSeek | DEEPSEEK_API_KEY | https://platform.deepseek.com |
| ApiNebula | APINEBULA_TOKEN | 控制台个人中心生成系统令牌 |
| ApiNebula | APINEBULA_USER_ID | F12 控制台执行 JSON.parse(localStorage.getItem('user')).id |
| ApiNebula | APINEBULA_BASE_URL | 默认 https://apinebula.ai,一般不改 |
6.3 启动
npm link # 全局注册 cre 命令(可选) cre # 首次启动并配置 credgauge autostart on # 想开机自启就加上这条
配置完成后,日常使用双击 start.vbs 即可,或开机自动启动。
七、命令一览
| 命令 | 说明 |
|---|---|
cre | 启动桌面挂件(简写) |
credgauge widget | 启动桌面挂件 |
credgauge deepseek | 查询 DeepSeek 余额 |
credgauge apinebula | 查询 ApiNebula 余额 |
credgauge all | 查询所有已配置的服务 |
credgauge autostart on | 开启开机自启 |
credgauge autostart off | 关闭开机自启 |
credgauge autostart status | 查看开机自启状态 |
credgauge -v | 显示版本 |
credgauge -h | 显示帮助 |
八、总结与反思
8.1 做对了什么
-
零运行时依赖:除了 electron,不引任何包,安装快、体积小、维护成本低
-
配置安全:.env 严格排除在版本库之外,模板与真实凭证分离
-
启动体验:三种启动方式覆盖不同场景,双击和开机自启真正做到了"无感"
-
provider 抽象:统一接口让新增服务变得容易,未来加 OpenAI、Claude 只需写新 provider
8.2 待改进点
-
跨平台:目前 start.vbs 和启动文件夹方案是 Windows 专属,macOS/Linux 需要另写(可用 plist / .desktop)
-
错误提示:挂件内错误提示较简陋,令牌失效时只是状态点变红,用户不一定知道原因
-
配置 UI:目前首次配置在终端交互,非技术用户不友好,可考虑加图形化配置窗口
8.3 一点感悟
credgauge 不是什么复杂项目,代码量也不大,但它解决了一个真实的、反复出现的小烦扰。做这类小工具的乐趣在于:把一个具体的痛点想清楚,用最轻的方式解决掉,然后在和系统底层(VBS 编码、进程窗口、PATH、快捷方式)打交道的过程中学到一堆细节。
这些东西单独看都不值一提,但攒起来就是对系统的一份理解。
项目地址:https://github.com/w-zjj/credgauge
License:MIT,欢迎白嫖和 PR。
关键词:Electron、Node.js、桌面挂件、Windows 静默启动、VBScript、开机自启、AI API、DeepSeek、ApiNebula、零依赖
版权声明:本文为原创内容,转载请注明出处。项目代码基于 MIT 协议开源。
更多推荐


所有评论(0)