|
|
@@ -7,6 +7,8 @@ import (
|
|
|
"io"
|
|
|
"net/http"
|
|
|
"os"
|
|
|
+ "path/filepath"
|
|
|
+ "strings"
|
|
|
"sync"
|
|
|
"time"
|
|
|
|
|
|
@@ -20,10 +22,10 @@ var iconData []byte
|
|
|
// 确保 embed 包被"使用"(Go 1.26+ 要求)
|
|
|
var _ = embed.FS{}
|
|
|
|
|
|
-// ---------- 默认配置(可被环境变量覆盖) ----------
|
|
|
+// ---------- 配置(优先级:环境变量 > .env 文件 > 默认值) ----------
|
|
|
var (
|
|
|
- apiKey = getEnv("DEEPSEEK_API_KEY", "")
|
|
|
- balanceURL = getEnv("DEEPSEEK_BALANCE_URL", "https://api.deepseek.com/user/balance")
|
|
|
+ apiKey string
|
|
|
+ balanceURL string
|
|
|
requestTimeout = 15 // 秒
|
|
|
refreshMinutes = 10 // 自动刷新间隔,0 表示关闭
|
|
|
lowThreshold = 1.0 // 余额低于此值(元)时告警
|
|
|
@@ -48,18 +50,26 @@ type balanceResponse struct {
|
|
|
}
|
|
|
|
|
|
func main() {
|
|
|
- if apiKey == "" {
|
|
|
- fmt.Fprintln(os.Stderr, "错误: 环境变量 DEEPSEEK_API_KEY 未设置")
|
|
|
- fmt.Fprintln(os.Stderr, "请设置后运行: set DEEPSEEK_API_KEY=sk-your-key")
|
|
|
- os.Exit(1)
|
|
|
- }
|
|
|
+ // 加载 .env 文件(同级目录),不覆盖已有环境变量
|
|
|
+ loadEnvFile()
|
|
|
+
|
|
|
+ // 读取配置
|
|
|
+ apiKey = getEnv("DEEPSEEK_API_KEY", "")
|
|
|
+ balanceURL = getEnv("DEEPSEEK_BALANCE_URL", "https://api.deepseek.com/user/balance")
|
|
|
+
|
|
|
+ // 始终启动托盘(即使无 Key,也显示图标+提示)
|
|
|
systray.Run(onReady, onExit)
|
|
|
}
|
|
|
|
|
|
func onReady() {
|
|
|
systray.SetIcon(iconData)
|
|
|
systray.SetTitle("DeepSeek")
|
|
|
- systray.SetTooltip("DeepSeek 余额查询 - 正在获取...")
|
|
|
+
|
|
|
+ if apiKey == "" {
|
|
|
+ systray.SetTooltip("DeepSeek - 未配置 API Key\n请在 .env 文件中设置 DEEPSEEK_API_KEY")
|
|
|
+ } else {
|
|
|
+ systray.SetTooltip("DeepSeek 余额查询 - 正在获取...")
|
|
|
+ }
|
|
|
|
|
|
// 菜单项
|
|
|
mBalance := systray.AddMenuItem("余额: 查询中...", "")
|
|
|
@@ -69,11 +79,19 @@ func onReady() {
|
|
|
mAutoRefresh := systray.AddMenuItem("自动刷新: 开启", "切换自动刷新")
|
|
|
mThreshold := systray.AddMenuItem("低余额告警: 关闭", "切换低余额告警")
|
|
|
systray.AddSeparator()
|
|
|
+ mConfigPath := systray.AddMenuItem("打开 .env 配置文件", "用记事本编辑 .env")
|
|
|
mAbout := systray.AddMenuItem("关于", "关于本程序")
|
|
|
mQuit := systray.AddMenuItem("退出", "关闭程序")
|
|
|
|
|
|
// 首次查询
|
|
|
- go refreshBalance(mBalance)
|
|
|
+ go func() {
|
|
|
+ if apiKey == "" {
|
|
|
+ mBalance.SetTitle("余额: 未配置 Key")
|
|
|
+ mBalance.SetTooltip("点击「打开 .env 配置文件」设置 API Key")
|
|
|
+ } else {
|
|
|
+ refreshBalance(mBalance)
|
|
|
+ }
|
|
|
+ }()
|
|
|
|
|
|
// 定时器控制
|
|
|
var (
|
|
|
@@ -93,9 +111,11 @@ func onReady() {
|
|
|
for {
|
|
|
select {
|
|
|
case <-ticker.C:
|
|
|
- refreshBalance(mBalance)
|
|
|
- if alertOn {
|
|
|
- checkLowBalance()
|
|
|
+ if apiKey != "" {
|
|
|
+ refreshBalance(mBalance)
|
|
|
+ if alertOn {
|
|
|
+ checkLowBalance()
|
|
|
+ }
|
|
|
}
|
|
|
case <-tickerStop:
|
|
|
ticker.Stop()
|
|
|
@@ -118,7 +138,11 @@ func onReady() {
|
|
|
for {
|
|
|
select {
|
|
|
case <-mManualRefresh.ClickedCh:
|
|
|
- refreshBalance(mBalance)
|
|
|
+ if apiKey == "" {
|
|
|
+ showMessageBox("DeepSeek Tray", "未配置 API Key。\n请点击右键菜单「打开 .env 配置文件」设置 DEEPSEEK_API_KEY。")
|
|
|
+ } else {
|
|
|
+ refreshBalance(mBalance)
|
|
|
+ }
|
|
|
|
|
|
case <-mAutoRefresh.ClickedCh:
|
|
|
if autoRefresh {
|
|
|
@@ -136,17 +160,21 @@ func onReady() {
|
|
|
if alertOn {
|
|
|
mThreshold.SetTitle("低余额告警: 开启")
|
|
|
mThreshold.SetTooltip(fmt.Sprintf("余额低于 ¥%.0f 时提醒", lowThreshold))
|
|
|
- checkLowBalance()
|
|
|
+ if apiKey != "" {
|
|
|
+ checkLowBalance()
|
|
|
+ }
|
|
|
} else {
|
|
|
mThreshold.SetTitle("低余额告警: 关闭")
|
|
|
mThreshold.SetTooltip("切换低余额告警")
|
|
|
}
|
|
|
|
|
|
+ case <-mConfigPath.ClickedCh:
|
|
|
+ openEnvFile()
|
|
|
+
|
|
|
case <-mAbout.ClickedCh:
|
|
|
- systray.SetTooltip(fmt.Sprintf(
|
|
|
- "自动刷新:%s | 间隔:%dmin | 告警:%s",
|
|
|
- boolToStr(autoRefresh), refreshMinutes, boolToStr(alertOn),
|
|
|
- ))
|
|
|
+ info := fmt.Sprintf("DeepSeek 余额查询系统托盘\n\n自动刷新: %s\n刷新间隔: %d 分钟\n低余额告警: %s\n\n配置文件: deepseek-tray.exe 同目录下的 .env",
|
|
|
+ boolToStr(autoRefresh), refreshMinutes, boolToStr(alertOn))
|
|
|
+ showMessageBox("DeepSeek Balance Tray", info)
|
|
|
|
|
|
case <-mQuit.ClickedCh:
|
|
|
stopTicker()
|
|
|
@@ -174,7 +202,7 @@ func refreshBalance(menuItem *systray.MenuItem) {
|
|
|
menuItem.SetTitle("余额: " + text + " " + currency)
|
|
|
menuItem.SetTooltip(tooltip)
|
|
|
|
|
|
- // 更新程序标题
|
|
|
+ // 更新程序标题(显示在托盘旁)
|
|
|
if totalVal > 0 {
|
|
|
systray.SetTitle(text)
|
|
|
} else {
|
|
|
@@ -194,7 +222,7 @@ func fetchBalance() (string, string, float64) {
|
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
if err != nil {
|
|
|
- return "请求失败: " + err.Error(), "", 0
|
|
|
+ return "网络错误: " + err.Error(), "", 0
|
|
|
}
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
@@ -235,7 +263,7 @@ func fetchBalance() (string, string, float64) {
|
|
|
return fmt.Sprintf("%v", parts), currency, total
|
|
|
}
|
|
|
|
|
|
-// checkLowBalance 检查余额是否低于阈值,通过弹窗告警(使用 Windows API)
|
|
|
+// checkLowBalance 检查余额是否低于阈值,通过弹窗告警
|
|
|
func checkLowBalance() {
|
|
|
_, _, totalVal := fetchBalance()
|
|
|
if totalVal > 0 && totalVal < lowThreshold {
|
|
|
@@ -244,9 +272,68 @@ func checkLowBalance() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// ---------- .env 文件支持 ----------
|
|
|
+
|
|
|
+// loadEnvFile 从可执行文件同级目录读取 .env 文件(不覆盖已有环境变量)
|
|
|
+func loadEnvFile() {
|
|
|
+ exePath, err := os.Executable()
|
|
|
+ if err != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ envPath := filepath.Join(filepath.Dir(exePath), ".env")
|
|
|
+ data, err := os.ReadFile(envPath)
|
|
|
+ if err != nil {
|
|
|
+ return // .env 不存在,静默跳过
|
|
|
+ }
|
|
|
+
|
|
|
+ lines := strings.Split(string(data), "\n")
|
|
|
+ for _, line := range lines {
|
|
|
+ line = strings.TrimSpace(line)
|
|
|
+ if line == "" || strings.HasPrefix(line, "#") {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ // 支持 KEY=VALUE 或 KEY="VALUE" 格式
|
|
|
+ idx := strings.IndexByte(line, '=')
|
|
|
+ if idx < 0 {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ key := strings.TrimSpace(line[:idx])
|
|
|
+ val := strings.TrimSpace(line[idx+1:])
|
|
|
+ val = strings.Trim(val, `"`) // 移除双引号
|
|
|
+
|
|
|
+ // 不覆盖已有环境变量
|
|
|
+ if os.Getenv(key) == "" {
|
|
|
+ os.Setenv(key, val)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// openEnvFile 用记事本打开 .env 文件,若不存在则创建模板
|
|
|
+func openEnvFile() {
|
|
|
+ exePath, err := os.Executable()
|
|
|
+ if err != nil {
|
|
|
+ showMessageBox("错误", "无法获取程序路径")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ envPath := filepath.Join(filepath.Dir(exePath), ".env")
|
|
|
+
|
|
|
+ // 如果 .env 不存在,创建模板
|
|
|
+ if _, err := os.Stat(envPath); os.IsNotExist(err) {
|
|
|
+ tmpl := "# DeepSeek API Key(必填)\nDEEPSEEK_API_KEY=sk-your-key-here\n\n# DeepSeek 余额查询接口(可选)\n# DEEPSEEK_BALANCE_URL=https://api.deepseek.com/user/balance\n"
|
|
|
+ if err := os.WriteFile(envPath, []byte(tmpl), 0644); err != nil {
|
|
|
+ showMessageBox("错误", "无法创建 .env 文件: "+err.Error())
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用记事本打开
|
|
|
+ runNotepad(envPath)
|
|
|
+}
|
|
|
+
|
|
|
// ---------- 工具函数 ----------
|
|
|
|
|
|
func getEnv(key, defaultVal string) string {
|
|
|
+ // 先查环境变量,再查 .env(已在 loadEnvFile 中注入到 os.Environ)
|
|
|
if val, ok := os.LookupEnv(key); ok && val != "" {
|
|
|
return val
|
|
|
}
|