ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

credgauge 实战:用 Electron 把 AI 服务余额钉在桌面(含 Windows 静默启动踩坑全过程)

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 NodeWindows 原生无终端窗口开机自启启动文件夹快捷方式比 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 # 挂件 UI2.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()); }定时轮询用setInterval60 秒一次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_KEYsk-your-deepseek-key APINEBULA_BASE_URLhttps://apinebula.ai APINEBULA_TOKENyour-system-token APINEBULA_USER_IDyour-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 $wsNew-Object -ComObject WScript.Shell; $s$ws.CreateShortcut(${lnkPath}); $s.TargetPathwscript.exe; $s.Arguments${vbsPath}; $s.WindowStyle7; $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 → nodeSW_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.ShellCOM 对象生成.lnk几行代码搞定从此开机自启稳如老狗。六、安装与使用6.1 安装三步走git clone https://github.com/w-zjj/credgauge.git cd credgauge npm install6.2 配置凭证Copy-Item .env.example .env # 编辑 .env 填入你的凭证需要配置的内容服务变量获取方式DeepSeekDEEPSEEK_API_KEYhttps://platform.deepseek.comApiNebulaAPINEBULA_TOKEN控制台个人中心生成系统令牌ApiNebulaAPINEBULA_USER_IDF12 控制台执行JSON.parse(localStorage.getItem(user)).idApiNebulaAPINEBULA_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 只需写新 provider8.2 待改进点跨平台目前 start.vbs 和启动文件夹方案是 Windows 专属macOS/Linux 需要另写可用 plist / .desktop错误提示挂件内错误提示较简陋令牌失效时只是状态点变红用户不一定知道原因配置 UI目前首次配置在终端交互非技术用户不友好可考虑加图形化配置窗口8.3 一点感悟credgauge 不是什么复杂项目代码量也不大但它解决了一个真实的、反复出现的小烦扰。做这类小工具的乐趣在于把一个具体的痛点想清楚用最轻的方式解决掉然后在和系统底层VBS 编码、进程窗口、PATH、快捷方式打交道的过程中学到一堆细节。这些东西单独看都不值一提但攒起来就是对系统的一份理解。项目地址https://github.com/w-zjj/credgaugeLicenseMIT欢迎白嫖和 PR。关键词Electron、Node.js、桌面挂件、Windows 静默启动、VBScript、开机自启、AI API、DeepSeek、ApiNebula、零依赖版权声明本文为原创内容转载请注明出处。项目代码基于 MIT 协议开源。
返回列表