Files
community-plugins/deepseek_usage/deepseek_usage.luau
T
Priyanshu GuptaandGitHub 8aa75342f3 add(deepseek_usage): DeepSeek API credit & balance monitor (#220)
* add(deepseek_usage): add DeepSeek usage balance monitor plugin

* add(deepseek_usage): add thumbnail.webp

* chore(deepseek_usage): update plugin id to coder/deepseek_usage

* fix(deepseek_usage): update 960x540 thumbnail, README IPC docs, and plugin.toml schema

* fix(deepseek_usage): fix panel ID, normalize ui.graph values 0-1, declare xdg-open dependency

* fix(deepseek_usage): replace thumbnail with official Noctalia generator asset
2026-08-03 16:33:41 -04:00

214 lines
6.0 KiB
Luau

--!nonstrict
-- DeepSeek Usage Bar Widget & Background Service Entry
local DEFAULT_REFRESH_MINUTES = 15
local HISTORY_FILE = "balance_history.json"
local MAX_HISTORY_POINTS = 48
local CURRENCY_SYMBOLS = {
USD = "$",
CNY = "¥",
EUR = "€",
GBP = "£",
JPY = "¥",
}
local function formatBalance(balance: number?, currency: string?): string
if balance == nil then
return "--.--"
end
local curr = currency or "USD"
local symbol = CURRENCY_SYMBOLS[curr]
if symbol then
return string.format("%s%.2f", symbol, balance)
else
return string.format("%.2f %s", balance, curr)
end
end
local state = {
balance = nil :: number?,
currency = "USD",
lastUpdated = nil :: string?,
errorMsg = nil :: string?,
isFetching = false,
}
-- Load accumulated balance history from disk
local function loadHistory(): { { time: number, balance: number } }
local dir = noctalia.pluginDataDir()
if not dir then return {} end
local path = dir .. "/" .. HISTORY_FILE
local content = noctalia.readFile(path)
if not content then return {} end
local decoded = noctalia.json.decode(content)
if type(decoded) == "table" then
return decoded
end
return {}
end
-- Save balance sample to persistent history
local function recordBalanceSample(val: number)
local dir = noctalia.pluginDataDir()
if not dir then return end
local history = loadHistory()
local now = os.time()
table.insert(history, { time = now, balance = val })
-- Keep only the latest N samples
while #history > MAX_HISTORY_POINTS do
table.remove(history, 1)
end
local encoded = noctalia.json.encode(history, false)
if encoded then
noctalia.writeFile(dir .. "/" .. HISTORY_FILE, encoded)
end
-- Share history state with panel
noctalia.state.set("deepseek.history", history)
end
local function renderWidget()
local apiKey = noctalia.getConfig("api_key") or ""
if apiKey == "" then
barWidget.setGlyph("wallet")
barWidget.setText(noctalia.tr("widget.no_key"))
barWidget.setTooltip({
{ key = noctalia.tr("tooltip.status"), value = noctalia.tr("status.unconfigured") },
{ key = noctalia.tr("tooltip.action"), value = noctalia.tr("tooltip.click_to_configure") },
})
return
end
if state.isFetching and state.balance == nil then
barWidget.setGlyph("refresh")
barWidget.setText(noctalia.tr("widget.loading"))
return
end
if state.errorMsg and state.balance == nil then
barWidget.setGlyph("alert-circle")
barWidget.setText(noctalia.tr("widget.error"))
barWidget.setTooltip({
{ key = noctalia.tr("tooltip.status"), value = state.errorMsg },
})
return
end
local threshold = tonumber(noctalia.getConfig("low_balance_threshold")) or 2.0
local balanceStr = formatBalance(state.balance, state.currency)
barWidget.setGlyph("wallet")
barWidget.setText(balanceStr)
if state.balance ~= nil and state.balance < threshold then
barWidget.setColor("error")
else
barWidget.setColor("primary")
end
barWidget.setTooltip({
{ key = noctalia.tr("tooltip.balance"), value = balanceStr .. " " .. state.currency },
{ key = noctalia.tr("tooltip.last_updated"), value = state.lastUpdated or noctalia.tr("status.never") },
{ key = noctalia.tr("tooltip.status"), value = state.errorMsg or noctalia.tr("status.ok") },
})
end
local function fetchSummary()
local apiKey = noctalia.getConfig("api_key") or ""
if apiKey == "" then
state.errorMsg = noctalia.tr("status.unconfigured")
noctalia.state.set("deepseek.state", state)
renderWidget()
return
end
state.isFetching = true
renderWidget()
noctalia.http({
url = "https://api.deepseek.com/user/balance",
method = "GET",
headers = {
"Authorization: Bearer " .. apiKey,
"Accept: application/json",
},
}, function(res)
state.isFetching = false
if not res.ok then
state.errorMsg = string.format("HTTP Transport Error (%d)", res.status)
noctalia.notifyError("DeepSeek Usage", state.errorMsg)
noctalia.state.set("deepseek.state", state)
renderWidget()
return
end
if res.status ~= 200 then
state.errorMsg = string.format("API Error (%d)", res.status)
if res.status == 401 then
state.errorMsg = noctalia.tr("error.invalid_key")
end
noctalia.notifyError("DeepSeek Usage", state.errorMsg)
noctalia.state.set("deepseek.state", state)
renderWidget()
return
end
local parsed, err = noctalia.json.decode(res.body)
if not parsed or type(parsed) ~= "table" then
state.errorMsg = noctalia.tr("error.parse_failed")
noctalia.state.set("deepseek.state", state)
renderWidget()
return
end
local balanceInfos = parsed.balance_infos
if balanceInfos and #balanceInfos > 0 then
local primaryWallet = balanceInfos[1]
state.currency = primaryWallet.currency or "USD"
state.balance = tonumber(primaryWallet.total_balance) or 0.0
state.errorMsg = nil
state.lastUpdated = noctalia.formatTime("%H:%M:%S", os.time())
recordBalanceSample(state.balance)
-- Check low balance notification
local threshold = tonumber(noctalia.getConfig("low_balance_threshold")) or 2.0
if state.balance < threshold then
noctalia.notify("DeepSeek Balance Warning", string.format("Your DeepSeek balance is low: %s", formatBalance(state.balance, state.currency)))
end
else
state.errorMsg = noctalia.tr("error.no_wallet")
end
noctalia.state.set("deepseek.state", state)
renderWidget()
end)
end
function update()
local refreshMinutes = tonumber(noctalia.getConfig("refresh_minutes")) or DEFAULT_REFRESH_MINUTES
noctalia.setUpdateInterval(refreshMinutes * 60 * 1000)
fetchSummary()
end
function onClick()
noctalia.togglePanel("coder/deepseek_usage:panel")
end
noctalia.state.watch("deepseek.refresh_requested", function(req)
if req then
noctalia.state.set("deepseek.refresh_requested", false)
fetchSummary()
end
end)
-- Initial setup
renderWidget()