| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- package main
- import (
- "embed"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "os"
- "sync"
- "time"
- "github.com/getlantern/systray"
- )
- // ---------- 嵌入资源 ----------
- //go:embed icon.ico
- var iconData []byte
- // 确保 embed 包被"使用"(Go 1.26+ 要求)
- var _ = embed.FS{}
- // ---------- 默认配置(可被环境变量覆盖) ----------
- var (
- apiKey = getEnv("DEEPSEEK_API_KEY", "")
- balanceURL = getEnv("DEEPSEEK_BALANCE_URL", "https://api.deepseek.com/user/balance")
- requestTimeout = 15 // 秒
- refreshMinutes = 10 // 自动刷新间隔,0 表示关闭
- lowThreshold = 1.0 // 余额低于此值(元)时告警
- )
- // ---------- 运行时状态 ----------
- var (
- lastBalance string
- lastUpdate time.Time
- mu sync.RWMutex
- )
- // ---------- API 响应结构 ----------
- type balanceResponse struct {
- IsAvailable bool `json:"is_available"`
- BalanceInfos []struct {
- Currency string `json:"currency"`
- TotalBalance string `json:"total_balance"`
- GrantedBalance string `json:"granted_balance"`
- ToppedUpBalance string `json:"topped_up_balance"`
- } `json:"balance_infos"`
- }
- 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)
- }
- systray.Run(onReady, onExit)
- }
- func onReady() {
- systray.SetIcon(iconData)
- systray.SetTitle("DeepSeek")
- systray.SetTooltip("DeepSeek 余额查询 - 正在获取...")
- // 菜单项
- mBalance := systray.AddMenuItem("余额: 查询中...", "")
- mBalance.Disable()
- mManualRefresh := systray.AddMenuItem("刷新余额", "手动刷新")
- mAutoRefresh := systray.AddMenuItem("自动刷新: 开启", "切换自动刷新")
- mThreshold := systray.AddMenuItem("低余额告警: 关闭", "切换低余额告警")
- systray.AddSeparator()
- mAbout := systray.AddMenuItem("关于", "关于本程序")
- mQuit := systray.AddMenuItem("退出", "关闭程序")
- // 首次查询
- go refreshBalance(mBalance)
- // 定时器控制
- var (
- ticker *time.Ticker
- tickerStop chan struct{}
- autoRefresh = true
- alertOn = false
- )
- startTicker := func() {
- if refreshMinutes <= 0 {
- return
- }
- ticker = time.NewTicker(time.Duration(refreshMinutes) * time.Minute)
- tickerStop = make(chan struct{})
- go func() {
- for {
- select {
- case <-ticker.C:
- refreshBalance(mBalance)
- if alertOn {
- checkLowBalance()
- }
- case <-tickerStop:
- ticker.Stop()
- return
- }
- }
- }()
- }
- stopTicker := func() {
- if tickerStop != nil {
- close(tickerStop)
- tickerStop = nil
- }
- }
- startTicker()
- // 事件循环
- for {
- select {
- case <-mManualRefresh.ClickedCh:
- refreshBalance(mBalance)
- case <-mAutoRefresh.ClickedCh:
- if autoRefresh {
- stopTicker()
- mAutoRefresh.SetTitle("自动刷新: 关闭")
- autoRefresh = false
- } else {
- startTicker()
- mAutoRefresh.SetTitle("自动刷新: 开启")
- autoRefresh = true
- }
- case <-mThreshold.ClickedCh:
- alertOn = !alertOn
- if alertOn {
- mThreshold.SetTitle("低余额告警: 开启")
- mThreshold.SetTooltip(fmt.Sprintf("余额低于 ¥%.0f 时提醒", lowThreshold))
- checkLowBalance()
- } else {
- mThreshold.SetTitle("低余额告警: 关闭")
- mThreshold.SetTooltip("切换低余额告警")
- }
- case <-mAbout.ClickedCh:
- systray.SetTooltip(fmt.Sprintf(
- "自动刷新:%s | 间隔:%dmin | 告警:%s",
- boolToStr(autoRefresh), refreshMinutes, boolToStr(alertOn),
- ))
- case <-mQuit.ClickedCh:
- stopTicker()
- systray.Quit()
- return
- }
- }
- }
- func onExit() {
- // 清理
- }
- // refreshBalance 请求 API 并更新托盘状态
- func refreshBalance(menuItem *systray.MenuItem) {
- text, currency, totalVal := fetchBalance()
- mu.Lock()
- lastBalance = text
- lastUpdate = time.Now()
- mu.Unlock()
- tooltip := fmt.Sprintf("DeepSeek 余额: %s %s\n更新时间: %s",
- text, currency, lastUpdate.Format("15:04:05"))
- systray.SetTooltip(tooltip)
- menuItem.SetTitle("余额: " + text + " " + currency)
- menuItem.SetTooltip(tooltip)
- // 更新程序标题
- if totalVal > 0 {
- systray.SetTitle(text)
- } else {
- systray.SetTitle("DeepSeek")
- }
- }
- // fetchBalance 返回 (余额文本, 货币, 数值用于比较)
- func fetchBalance() (string, string, float64) {
- client := &http.Client{Timeout: time.Duration(requestTimeout) * time.Second}
- req, err := http.NewRequest("GET", balanceURL, nil)
- if err != nil {
- return "创建请求失败", "", 0
- }
- req.Header.Set("Authorization", "Bearer "+apiKey)
- req.Header.Set("Accept", "application/json")
- resp, err := client.Do(req)
- if err != nil {
- return "请求失败: " + err.Error(), "", 0
- }
- defer resp.Body.Close()
- if resp.StatusCode == http.StatusUnauthorized {
- return "API Key 无效", "", 0
- }
- if resp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
- return fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(body)), "", 0
- }
- var br balanceResponse
- if err := json.NewDecoder(resp.Body).Decode(&br); err != nil {
- return "解析响应失败", "", 0
- }
- if !br.IsAvailable || len(br.BalanceInfos) == 0 {
- return "余额不可用", "", 0
- }
- // 合并所有币种
- var parts []string
- var total float64
- currency := "CNY"
- for _, info := range br.BalanceInfos {
- var val float64
- fmt.Sscanf(info.TotalBalance, "%f", &val)
- parts = append(parts, fmt.Sprintf("%s %s", info.TotalBalance, info.Currency))
- if info.Currency == "CNY" {
- total += val
- }
- currency = info.Currency
- }
- if len(parts) == 1 {
- return parts[0], currency, total
- }
- return fmt.Sprintf("%v", parts), currency, total
- }
- // checkLowBalance 检查余额是否低于阈值,通过弹窗告警(使用 Windows API)
- func checkLowBalance() {
- _, _, totalVal := fetchBalance()
- if totalVal > 0 && totalVal < lowThreshold {
- msg := fmt.Sprintf("当前余额 ¥%.2f,低于阈值 ¥%.0f,请及时充值。", totalVal, lowThreshold)
- showMessageBox("⚠️ DeepSeek 余额不足", msg)
- }
- }
- // ---------- 工具函数 ----------
- func getEnv(key, defaultVal string) string {
- if val, ok := os.LookupEnv(key); ok && val != "" {
- return val
- }
- return defaultVal
- }
- func boolToStr(b bool) string {
- if b {
- return "是"
- }
- return "否"
- }
|