查余额这种事,五分钟一次就够了
我习惯把 API 余额挂在状态栏或者 shell 提示符旁边。问题在于,这些东西刷新得太勤了。状态栏每次重绘都去请求一次余额接口,一天下来几百次请求,就为了看一个几乎不变的数字。
而且 DeepSeek 的余额接口不需要那么实时。余额是按用量扣的,我一天也用不了几块钱,五分钟前的数字和现在的数字,差不了多少。
所以我先定下的规则是:别查太勤。
输出就一行
$ uv run --no-project deepseek_usage.py
DS余额:12.34没有表格,没有单位说明,没有更新时间。因为它的用途就是嵌进别的输出里,多一个字都是负担。
脚本本身是单文件,顶部带 PEP-723 元数据,没有任何第三方依赖:
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///uv run --no-project 直接跑,不用建虚拟环境,不用装包。这种东西我连一个目录都不想为它建。
完整代码
整个文件不长,先放完,后面挑几段说。
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""查询并打印 DeepSeek 账户余额。
余额来自 https://api.deepseek.com/user/balance,
API key 从 $SECRETS49/.env 的 deepseek-key 读取。
输出单行:DS余额:12.34
查询结果缓存在系统临时目录,5 分钟内重复运行不再请求接口。
"""
import json
import os
import sys
import tempfile
import time
import urllib.request
BALANCE_URL = "https://api.deepseek.com/user/balance"
REQUEST_TIMEOUT_SECONDS = 10
CACHE_PATH = os.path.join(tempfile.gettempdir(), "deepseek_usage_cache.json")
CACHE_TTL_SECONDS = 300
SECRETS_DIR_ENV = "SECRETS49"
API_KEY_NAME = "deepseek-key"
def read_api_key() -> str:
"""从 $SECRETS49/.env 读取 deepseek-key,缺失时退出并说明原因。"""
secrets_dir = os.environ.get(SECRETS_DIR_ENV)
if not secrets_dir:
sys.exit(f"环境变量 {SECRETS_DIR_ENV} 未设置")
env_file = os.path.join(secrets_dir, ".env")
try:
with open(env_file, encoding="utf-8") as file:
lines = file.read().splitlines()
except OSError:
sys.exit(f"无法读取 {env_file}")
for line in lines:
name, separator, value = line.partition("=")
if separator and name.strip() == API_KEY_NAME:
return value.strip().strip("'\"")
sys.exit(f"{env_file} 里没有 {API_KEY_NAME}")
def fetch_balance(api_key: str) -> dict:
request = urllib.request.Request(
BALANCE_URL,
headers={
"Accept": "application/json",
"Authorization": f"Bearer {api_key}",
},
)
try:
with urllib.request.urlopen(
request, timeout=REQUEST_TIMEOUT_SECONDS
) as response:
return json.load(response)
except (OSError, ValueError):
sys.exit("查询 DeepSeek 余额失败:网络或响应异常")
def load_cached_payload() -> dict | None:
"""返回 5 分钟内的缓存响应,没有或已过期时返回 None。"""
try:
with open(CACHE_PATH, encoding="utf-8") as file:
cached = json.load(file)
except (OSError, ValueError):
return None
if not isinstance(cached, dict):
return None
fetched_at = cached.get("fetched_at")
if not isinstance(fetched_at, (int, float)):
return None
if time.time() - fetched_at > CACHE_TTL_SECONDS:
return None
payload = cached.get("payload")
return payload if isinstance(payload, dict) else None
def save_cached_payload(payload: dict) -> None:
try:
with open(CACHE_PATH, "w", encoding="utf-8") as file:
json.dump({"fetched_at": time.time(), "payload": payload}, file)
except OSError:
print(f"警告:无法写入缓存 {CACHE_PATH}", file=sys.stderr)
def format_balance(payload: dict) -> str:
if payload.get("is_available") is False:
return "DS余额:不可用"
balance_infos = payload.get("balance_infos")
if not isinstance(balance_infos, list):
return ""
infos = [item for item in balance_infos if isinstance(item, dict)]
if not infos:
return ""
info = next((item for item in infos if item.get("currency") == "CNY"), infos[0])
return f"DS余额:{format_amount(info.get('total_balance'))}"
def format_amount(value: object) -> str:
try:
amount = float(value) # type: ignore[arg-type]
except (TypeError, ValueError):
amount = 0.0
return f"{amount:.2f}".rstrip("0").rstrip(".")
def main() -> None:
payload = load_cached_payload()
is_cached = payload is not None
if payload is None:
payload = fetch_balance(read_api_key())
text = format_balance(payload)
if not text:
sys.exit("响应里没有余额数据")
if not is_cached:
save_cached_payload(payload)
print(text)
if __name__ == "__main__":
main()缓存放在临时目录
CACHE_PATH = os.path.join(tempfile.gettempdir(), "deepseek_usage_cache.json")
CACHE_TTL_SECONDS = 300五分钟。放在系统临时目录,系统清理临时文件时顺手带走,不用我管。
读缓存的时候,任何一点不对都当成没缓存:
def load_cached_payload() -> dict | None:
try:
with open(CACHE_PATH, encoding="utf-8") as file:
cached = json.load(file)
except (OSError, ValueError):
return None
if not isinstance(cached, dict):
return None
fetched_at = cached.get("fetched_at")
if not isinstance(fetched_at, (int, float)):
return None
if time.time() - fetched_at > CACHE_TTL_SECONDS:
return None
payload = cached.get("payload")
return payload if isinstance(payload, dict) else None文件坏了、JSON 被截断、fetched_at 是个字符串、缓存里没有 payload——全都返回 None,老老实实去请求接口。缓存坏了就当没有,重新请求一次。
写缓存失败则只警告,不影响结果:
except OSError:
print(f"警告:无法写入缓存 {CACHE_PATH}", file=sys.stderr)注意是 stderr。标准输出要留给那一行余额,嵌到别的脚本里时不能被警告污染。
金额末尾的零全部去掉
format_amount 这段很短,但我挺喜欢:
def format_amount(value: object) -> str:
try:
amount = float(value) # type: ignore[arg-type]
except (TypeError, ValueError):
amount = 0.0
return f"{amount:.2f}".rstrip("0").rstrip(".")先格式化成两位小数,再去掉末尾的零和小数点。
12.34→12.3412.30→12.312.00→12
余额接口给的是字符串,"12.300000" 这种写法我也见过。统一成两位小数再裁一遍,输出就干净了。
rstrip(".") 那一句不能省。12.00 先变成 12.,再去掉点才是 12。少了它就会输出 12.,看着像话没说完。
账户不可用时别显示 0
def format_balance(payload: dict) -> str:
if payload.get("is_available") is False:
return "DS余额:不可用"
...is_available 为 false 时,直接说「不可用」。这时候接口给的余额可能是 0,也可能缺字段,但「不可用」比「0」更接近事实——账户可能被停了,余额数字没有意义。
币种优先选人民币:
info = next((item for item in infos if item.get("currency") == "CNY"), infos[0])没有 CNY 就取第一个。我自己的账户只有人民币,但接口是通用的,多写一行不亏。
密钥只从一个地方读
SECRETS_DIR_ENV = "SECRETS49"
API_KEY_NAME = "deepseek-key"从 $SECRETS49/.env 里读 deepseek-key。没设环境变量、文件读不了、键不存在,三种情况都 sys.exit 带一句人话:
环境变量 SECRETS49 未设置
无法读取 /path/to/.env
/path/to/.env 里没有 deepseek-key不输出 traceback,不打印密钥,不尝试从别的地方找。我所有小脚本的密钥都在这一个文件里,这个约定越简单越好。
挂在状态栏里
状态栏调用它,拿到那一行,塞进底部。五分钟内连续刷新都是缓存,几十毫秒就返回;过了五分钟,下一次刷新才真正请求。