Metadata-Version: 2.4
Name: token-balance
Version: 0.0.2
Summary: 多厂商 Token 余额查询工具（DeepSeek / 硅基流动 / Kimi / OpenAI / NEW API / 自定义站点等）
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: aiohttp>=3.8
Requires-Dist: PyYAML>=6.0
Requires-Dist: mcp<2.0,>=1.0

# token_balance — 多厂商 Token 余额查询工具

一个独立于 AstrBot 的 Python 命令行工具，用于一键查询各 AI 厂商/中转站的
Token 余额。功能源自 [astrbot_plugin_balance](token/astrbot_plugin_balance-main)
与 [astrbot_plugin_llm_balance](token/astrbot_plugin_llm_balance-master)，
去掉了 AstrBot 依赖，做成可直接运行的 CLI + **MCP Server**（AI 客户端可
在对话中直接调用查余额），并支持 JSON 输出以便脚本化调用。

## 特性

- ✅ 内置 17 个平台预设，只需填 `api_key`（部分需 `base_url`）
- ✅ 任意自定义站点：YAML 配置 `url` + `method` + `headers` + `result_template`
- ✅ 模板公式：`{{data.balance}}` 取值、`{{round({data.quota}/500000*7.1, 2)}}` 计算
- ✅ 并发查询，单行失败不影响其他行
- ✅ 密钥脱敏显示（错误信息中的响应体也会自动脱敏），支持 `env:变量名` 引用密钥
- ✅ 文本表格（成功/失败分组）/ JSON / 可自定义模板 三种输出
- ✅ 临时查询：平台名、API 地址（自动识别 OpenAI Billing / New API）、NewAPI 连接 JSON 三种方式
- ✅ `platform_aliases` 自定义平台别名
- ✅ MCP Server：Codex / Claude Desktop / Cursor 等客户端对话中直接查询
- ✅ 首次启动自动生成配置文件模板（粘贴即用，只需填密钥）

## 安装

```bash
pip install -r requirements.txt
# 或单独安装依赖
pip install aiohttp PyYAML mcp
# 或安装为命令（推荐）
pip install -e .
```

安装后可用两个命令：

```bash
token-balance                 # CLI
token-balance-mcp             # MCP Server（stdio）
```

也可以用 uv 直接运行（无需安装，自动从 PyPI 拉取）：

```bash
uvx token-balance                            # CLI（命令名 = 包名）
uvx --from token-balance token-balance-mcp   # MCP Server（命令名 ≠ 包名，需 --from）
```

> 已发布到 PyPI：<https://pypi.org/project/token-balance/>（v0.0.1 测试版）。
> 本地开发时可用 `uvx --from .` 指向项目目录，效果相同。

### 发布（维护者）

```bash
uv version patch   # 或手动改 pyproject.toml 版本号
uv build           # 生成 dist/
uv publish         # 需要 PyPI API Token（已不支持密码认证）
```

## 快速开始（CLI）

```bash
# 1. 首次运行：自动生成 config.yaml 模板（无需手动复制）
python -m token_balance
# 提示"已自动生成配置文件模板: config.yaml"后，编辑该文件
# 把 "sk-在这里填你的密钥" 换成真实 api_key（推荐用 env:KEY 引用环境变量）

# 2. 再次运行：查询所有配置的服务
python -m token_balance

# 3. 只查某个服务
python -m token_balance check deepseek

# 4. 临时查询（不写配置文件）
python -m token_balance query deepseek sk-xxxx
python -m token_balance query https://api.example.com/v1 sk-xxxx

# 5. NewAPI 面板复制连接信息直接粘贴查询
python -m token_balance query '{"_type":"newapi_channel_conn","key":"sk-xxx","url":"https://new.xinjianya.top"}'

# 6. JSON 输出（适合脚本/监控）
python -m token_balance --json
```

## MCP Server

MCP（Model Context Protocol）让 AI 客户端（Codex、Claude Desktop、Cursor 等）
在对话中直接调用工具查余额，无需把密钥贴给模型——密钥只存在于 server 进程内，
返回结果自动脱敏。

### 启动方式

```bash
token-balance-mcp                       # 安装后直接运行（stdio）
uvx --from . token-balance-mcp          # uv 方式（免安装）
token-balance-mcp --config D:\path\config.yaml   # 指定配置文件
token-balance-mcp --transport sse --host 0.0.0.0 --port 10003   # SSE 远程
token-balance-mcp --transport http --host 0.0.0.0 --port 10003   # Streamable HTTP
```

参数说明：

| 参数 | 默认 | 说明 |
|---|---|---|
| `--transport` | `stdio` | `stdio`（本机客户端）/ `sse` / `http`（streamable-http，远程） |
| `--host` | `127.0.0.1` | 监听地址；远程访问需 `0.0.0.0` |
| `--port` | `8000` | 监听端口 |
| `--mount-path` | `/sse` | SSE 挂载路径（传 `mcp` 则端点为 `/mcp/sse`） |

配置文件路径解析顺序：`--config` 参数 > `TOKEN_BALANCE_CONFIG` 环境变量 >
当前目录 `config.yaml`。

### 工具列表

| 工具 | 说明 |
|---|---|
| `list_services` | 列出配置中的服务与支持的内置平台（含别名），无网络请求 |
| `query_balances` | 查询 config.yaml 中全部或指定服务的余额（支持按服务名/平台类型/别名过滤） |
| `query_endpoint` | 临时查询：平台别名、http(s) URL（自动识别 OpenAI Billing → New API）、NewAPI 连接 JSON |

返回结构示例：

```json
{
  "time": "2026-08-03 16:00:00",
  "summary": { "total": 3, "success": 2, "failed": 1 },
  "results": [
    { "name": "DeepSeek", "ok": true, "currency": "CNY", "total": "12.34",
      "remaining": "12.34", "used": "", "raw_info": "赠送: 10.00 元 | 充值: 2.34 元",
      "rendered": "DeepSeek: 12.34 元", "api_key_masked": "sk-12...34", "error": "" }
  ],
  "config_errors": [],
  "cached": false
}
```

查询结果有 30 秒缓存（`cached: true` 表示命中），防止模型在对话中对同一批
服务反复发起真实请求。

### 资源列表（token://）

除工具外还提供 3 个只读资源，客户端可直接读取（与 mcp-1panel 的
`panel://` 资源同一模式）：

| URI | 说明 |
|---|---|
| `token://services` | 配置中的服务与内置平台列表（无网络请求） |
| `token://balances` | 所有配置服务的余额快照（实时查询） |
| `token://balance/{name}` | 按服务名 / 平台类型 / 别名查询单个服务 |

### 远程传输（SSE / Streamable HTTP）

本机 stdio 之外，可用 `--transport sse` 或 `--transport http` 启动远程服务，
支持任意 MCP 客户端通过 URL 连接（Claude Desktop / Cursor / 1Panel 等）：

```bash
token-balance-mcp --transport sse --host 0.0.0.0 --port 10003
# 启动后提示: [token-balance] MCP server listening: http://0.0.0.0:10003/sse
```

客户端配置（SSE）：

```json
{
  "mcpServers": {
    "token-balance": {
      "url": "http://你的服务器IP:10003/sse",
      "transport": "sse",
      "env": { "TOKEN_BALANCE_CONFIG": "/opt/token-balance/config.yaml" }
    }
  }
}
```

Streamable HTTP 同理，URL 用 `http://你的服务器IP:10003/mcp`、
`"transport": "streamable-http"`（部分客户端直接填 url 即可自动识别）。

> 远程部署注意：密钥在服务端 config.yaml 里，不要通过客户端环境变量下发；
> 建议仅在内网或加反向代理鉴权后暴露。

### 客户端注册（粘贴即用）

所有 JSON 格式的客户端（Claude Desktop / Cursor / Windsurf / Hermes /
Cherry Studio 等）共用同一段配置，只是放到各自的配置文件位置。
`env` 里的 `TOKEN_BALANCE_CONFIG` 路径**不需要预先存在**——首次启动会自动
生成模板，你只需填密钥。

> 以下示例项目路径为 `D:\tools\small\Codex\codex\work\mcp`，按实际修改。

#### 方式 A：uvx 免安装（推荐，对标 npx）

包已发布 PyPI，直接 `--from token-balance` 拉取（首次会自动下载依赖）：

```json
{
  "mcpServers": {
    "token-balance": {
      "command": "uvx",
      "args": ["--from", "token-balance", "token-balance-mcp"],
      "env": {
        "TOKEN_BALANCE_CONFIG": "D:\\tools\\small\\Codex\\codex\\work\\mcp\\config.yaml"
      }
    }
  }
}
```

> 本地开发（未发布版本）时把 `"token-balance"` 换成项目目录，例如
> `["--from", "D:\\tools\\small\\Codex\\codex\\work\\mcp", "token-balance-mcp"]`。
> 注意：`uvx` 的规则是"命令名 = 包名"时才能直接写命令名；本包命令名
> `token-balance-mcp` 与包名不同，必须用 `--from` 指定包。

- Claude Desktop：写入 `claude_desktop_config.json`
- Cursor：写入项目 `.cursor/mcp.json`
- Windsurf：写入 `~/.codeium/windsurf/mcp_config.json`
- Hermes / Cherry Studio 等：写入各自 MCP 配置入口（格式相同）

#### 方式 B：python -m（本机已装好依赖）

```json
{
  "mcpServers": {
    "token-balance": {
      "command": "python",
      "args": ["-m", "token_balance.mcp_server"],
      "env": {
        "PYTHONPATH": "D:\\tools\\small\\Codex\\codex\\work\\mcp",
        "TOKEN_BALANCE_CONFIG": "D:\\tools\\small\\Codex\\codex\\work\\mcp\\config.yaml"
      }
    }
  }
}
```

#### 方式 C：pip 安装后

```bash
pip install -e D:\tools\small\Codex\codex\work\mcp
```

```json
{
  "mcpServers": {
    "token-balance": {
      "command": "token-balance-mcp",
      "args": [],
      "env": { "TOKEN_BALANCE_CONFIG": "D:\\tools\\small\\Codex\\codex\\work\\mcp\\config.yaml" }
    }
  }
}
```

#### 方式 D：发布 PyPI 后（即方式 A）

包已发布到 PyPI，方式 A 的 `--from token-balance` 就是发布后的形态；
CLI 则是 `uvx token-balance`（命令名与包名相同，无需 `--from`）。

#### Codex（TOML 格式）

`~/.codex/config.toml`：

```toml
[mcp_servers.token-balance]
command = "uvx"
args = ["--from", "token-balance", "token-balance-mcp"]
env = { TOKEN_BALANCE_CONFIG = "D:\\tools\\small\\Codex\\codex\\work\\mcp\\config.yaml" }
```

> 国内网络若直连 PyPI 超时，可在 `env` 加
> `UV_DEFAULT_INDEX = "https://pypi.tuna.tsinghua.edu.cn/simple"`（镜像同步有延迟，
> 新版本发布后可能需等待几分钟到几小时）。

### 配置文件说明

- **首次启动自动生成**：server / CLI 启动时如果目标配置文件不存在，会在
  目标位置**自动生成带占位符的模板**（含 deepseek / siliconflow / kimi 示例，
  密钥位置写 `sk-在这里填你的密钥`），并把提示打到 stderr。你只需要：
  1. 打开生成的文件，把占位密钥换成真实 API Key（或改成 `env:变量名`）
  2. 保存后重启 MCP 实例 / 重新运行命令
  无需手动拉取模板、无需手动放置文件。
- **查找顺序**：`--config` 参数 > `TOKEN_BALANCE_CONFIG` 环境变量 > 当前目录 `config.yaml`。
- ⚠️ **不要依赖默认路径**：MCP server 的「当前目录」由客户端拉起进程时决定，
  Codex、Claude Desktop、1Panel 容器各不相同。部署 MCP 时必须用
  `TOKEN_BALANCE_CONFIG` 显式指定**绝对路径**（这样自动生成也会生成到该路径）。
- **Codex（本机）**：在 `config.toml` 的 `env` 里写
  `TOKEN_BALANCE_CONFIG = "D:\path\config.yaml"`；首次启动自动生成该文件，
  你只需编辑填密钥。密钥可用 `env:KEY` 引用本机环境变量。
- **1Panel（容器）**：见下方「1Panel 部署」小节——宿主机建好挂载目录后，
  首次启动会在挂载进容器的目录里自动生成模板，你直接在宿主机
  1Panel 文件管理里编辑填密钥即可。
- `config.yaml` 已被 `.gitignore` 忽略，避免密钥入库。

### 1Panel 部署（粘贴式步骤）

1Panel 内置 MCP 管理通过 uvx/npx 启动 stdio server，再桥接为 SSE。
部署 token-balance 只需 4 步：

**① 粘贴配置（若 1Panel 支持导入 mcpServers JSON；否则按 ② 表单填）**

容器版（Linux，挂载目录路径）：

```json
{
  "mcpServers": {
    "token-balance": {
      "command": "uvx",
      "args": ["--from", "token-balance", "token-balance-mcp"],
      "env": {
        "TOKEN_BALANCE_CONFIG": "/opt/1panel/mcp/token-balance/config.yaml"
      }
    }
  }
}
```

本机版（Windows / macOS）：

```json
{
  "mcpServers": {
    "token-balance": {
      "command": "uvx",
      "args": ["--from", "token-balance", "token-balance-mcp"],
      "env": {
        "TOKEN_BALANCE_CONFIG": "D:\\tools\\small\\Codex\\codex\\work\\mcp\\config.yaml"
      }
    }
  }
}
```

> ⚠️ 注意：uvx 要求"命令名 = 包名"才能直接写命令名；本包命令名
> `token-balance-mcp` 与包名 `token-balance` 不同，**必须**用
> `--from token-balance` 指定包，否则报 `No solution found`。
> 容器内路径不要写 `localhost`（指向容器自身），配置文件的宿主机路径
> 通过挂载映射后，`env` 里写**容器内**路径。

**② 宿主机建目录（放配置文件用）**
```bash
mkdir -p /opt/1panel/mcp/token-balance
```

**③ 1Panel → AI → MCP → 创建实例，按下面填：**

| 字段 | 值 |
|---|---|
| 名称 | `token-balance` |
| 类型 | `uvx`（或 `npx`，见下方说明） |
| 运行命令 | `uvx --from token-balance token-balance-mcp`（已发布 PyPI；内网环境可改 `--from /opt/1panel/mcp/token-balance` 指向本地源码） |
| 输出类型 | `sse`（或 `streamableHttp`） |
| 环境变量 | `TOKEN_BALANCE_CONFIG=/opt/1panel/mcp/token-balance/config.yaml` |
| 挂载 | 宿主机 `/opt/1panel/mcp/token-balance` → 容器 `/opt/1panel/mcp/token-balance` |
| 端口 | 如 `10003`，按需开启外部访问 |

> 运行命令两种选择：
> - **已发布 PyPI**：`uvx --from token-balance token-balance-mcp`（无需放源码，
>   首次启动需容器能访问 PyPI；国内网络可在环境变量加 `UV_DEFAULT_INDEX` 镜像）
> - **内网/离线**：把项目源码放到宿主机 `/opt/1panel/mcp/token-balance/` 下，
>   运行命令 `uvx --from /opt/1panel/mcp/token-balance token-balance-mcp`

**④ 首次启动自动生成模板**
启动实例后，server 会自动在挂载目录生成
`/opt/1panel/mcp/token-balance/config.yaml`（宿主机同路径可见）。
到 1Panel「文件」里打开它，把 `sk-在这里填你的密钥` 换成真实 API Key
（或 `env:变量名`），保存。

**⑤ 重启实例**，然后用面板给出的 SSE 地址在任意 MCP 客户端使用。

> 容器内访问宿主机服务（如本机部署的 new-api 中转站）不能用 `localhost`，
> 要用 `http://172.17.0.1:<端口>` 或 `http://host.docker.internal:<端口>`。
> 厂商公网 API（DeepSeek 等）无此问题。

## 内置平台

| 类型 | 平台 | 需要 | 余额单位 |
|---|---|---|---|
| `deepseek` | DeepSeek 深度求索 | api_key | CNY 元 |
| `siliconflow` | 硅基流动 | api_key | USD |
| `moonshot` / `kimi` / `kimi-full` | Kimi / Moonshot 月之暗面 | api_key | CNY 元 |
| `openai` | OpenAI | api_key | USD |
| `chatanywhere` | ChatAnywhere | api_key | USD |
| `openrouter` | OpenRouter | api_key | USD |
| `onething` | 网心云 OneThing | api_key | CNY 元 |
| `minimax` | MiniMax 海螺 | api_key | 次数/额度 |
| `aihubmix` | AIHubMix | api_key（Manage Key） | CNY 元 |
| `apimart` 系列 | APIMart | api_key | CNY / 积分 |
| `newapi` | NEW API 中转站 | base_url + api_key | 额度 |
| `oneapi` | One-API 自建 | base_url + api_key | 元 |

各类型的默认别名（`query` 命令可用）：`ds`=deepseek，`sc`/`硅基`=siliconflow，
`kimi`=moonshot，`ca`=chatanywhere，`new`/`中转`=newapi 等。
可在配置文件中用 `platform_aliases` 追加自定义别名：

```yaml
platform_aliases:
  deepseek: "ds,深度求索,我的ds"
```

## 自定义站点

任何提供余额查询 API 的服务商都能接入，例如：

```yaml
services:
  my_site:
    type: custom
    url: "https://api.example.com/v1/user/balance"
    method: GET
    headers:
      Authorization: "Bearer sk-xxxx"
    result_template: "我的站: {{data.balance}} 元"
```

### 模板语法

| 写法 | 说明 | 示例 |
|---|---|---|
| `{{path.to.field}}` | 取值，支持数组下标 | `{{balance_infos.0.total_balance}}` |
| `{{expr({path})}}` | 取值后参与公式计算 | `{{round({data.quota}/500000*7.1, 2)}}` |
| 函数 | abs/round/min/max/pow/sqrt/floor/ceil/log/log10/exp/sin/cos/tan/pi/e | `{{round({data.usage}/{data.limit}*100, 1)}}%` |
| 运算符 | `+ - * / %`（`50%` = 0.5） | `{{abs({data.balance})/100}}` |

取值失败会报「未找到字段: 具体模板路径」，不会泄露密钥。

### 结构化字段（可选）

自定义站点除 `result_template`（整行展示）外，还可以提供以下字段，
让表格/JSON 输出有独立的余额、已用、备注列：

```yaml
    currency: "CNY"                # 固定币种
    total_template: "{{data.total}}"
    remaining_template: "{{data.balance}}"
    used_template: "{{data.used}}"
    raw_info_template: "到期: {{data.expires}}"
```

## 临时查询

```
token-balance query <平台名或别名> <key1> [key2 ...]
token-balance query <http(s)://地址> <key1> [key2 ...]   # 多 key 并发
token-balance query '{"_type":"newapi_channel_conn","key":"sk-xxx","url":"https://..."}'
```

API 地址模式会自动识别端点格式：
1. 优先 OpenAI Billing API（`/v1/dashboard/billing/subscription` + `/usage`）
2. 降级 New API 格式（`/api/usage/token`，地址以 `/v1` 结尾也会正确处理）

适用于 one-api、new-api、AIProxy 等各类中转站。批量 key 查询为并发执行；
两种格式都识别失败时，错误信息会给出各自的失败原因。

## 自定义输出模板

在配置文件中设置以下任一字段即启用模板输出模式（默认是内置表格样式）：

```yaml
success_template: "🟢 **{{source_name}}**\n  🔑 密钥: {{api_key}}\n  💵 {{balance}} {{currency}}\n{{smart_balance}}"
error_template: "🔴 **{{source_name}}**\n  ❌ {{error}}"
header_template: "💰 **{{title}}**"
separator_template: "════════════════════════════════════════"
section_separator_template: "════════════════════════════════════════"
```

模板变量：`{{title}}`、`{{source_name}}`、`{{api_key}}`（脱敏）、`{{currency}}`、
`{{balance}}`（智能余额）、`{{total_balance}}`、`{{remaining_balance}}`、
`{{used_balance}}`、`{{raw_info}}`、`{{smart_balance}}`；
`{{?变量}}` 为条件行，值为空或 `0` 时隐藏整行；`\n` 为换行。
输出按成功/失败分组展示。

## 安全提醒

- 配置文件和命令行不要明文贴到群里/截图
- 建议用 `env:环境变量名` 引用密钥，例如 `api_key: "env:DEEPSEEK_API_KEY"`
- 输出中的密钥一律脱敏；错误信息回显的服务端响应也会先做脱敏处理
- 内置类型缺少 api_key 会在配置加载阶段直接报错，不会带空 key 发请求
- MCP server 查询只读（不修改任何厂商数据）；仅首次启动时会生成配置文件
  模板（已存在则绝不覆盖）；仅注册到可信客户端，密钥只存在于 server 进程

## 测试

```bash
python -m unittest discover -s tests -v
```

测试使用本地 Mock 服务模拟各厂商接口，不需要真实密钥，也不访问外网。
