@feiyang666/dsh-usage-plugin 1.9.1 → 1.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +15 -5
- package/README.zh.md +15 -5
- package/cordis.patch.yml +23 -22
- package/lib/balance.js +205 -0
- package/lib/client.js +1489 -1247
- package/lib/index.js +1196 -911
- package/package.json +9 -1
- package/scripts/wire.js +109 -108
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,25 @@
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## v1.9.3 (2026-08-20)
|
|
12
|
+
|
|
13
|
+
- **概览支持日期筛选**:概览 Tab 顶部新增筛选栏,可按「今天 / 近 7 天 / 近 30 天 / 全部」快速切换,或自定义起止日期区间;所有聚合统计(按模型表、服务商 × 模型明细、总计)均按所选范围实时计算,并展示当前范围的记录数与总消耗。
|
|
14
|
+
|
|
15
|
+
## v1.9.2 (2026-08-20)
|
|
16
|
+
|
|
17
|
+
> 修复 issue #4:持久化路径随会话工作区漂移,导致历史用量数据"消失/统计为 0",且 UI 显示路径与实际落盘路径不一致。
|
|
18
|
+
|
|
19
|
+
### 修复
|
|
20
|
+
- **固定专用数据目录**:持久化根不再跟随 `agent.session.cwd` / `sandboxPolicy.workspaceRoot` 漂移。解析顺序为:环境变量 `DSH_USAGE_DATA_DIR` > 系统应用数据目录 `%LOCALAPPDATA%\dsh-usage-plugin`(仅 Windows;macOS/Linux 下该分支不生效,因 `LOCALAPPDATA`/`APPDATA` 未定义)> 用户主目录下专用文件夹 `~/dsh-usage-data`(Windows 为 `%USERPROFILE%\dsh-usage-data`,macOS/Linux 为 `~/dsh-usage-data`)。该目录独立于桌面端安装目录、`~/.dsh` 主目录与任何会话工作区,删除工作区或卸载 APP 都不会丢失数据。
|
|
21
|
+
- **启动时合并去重**:首次初始化与会话激活时,自动把散落在用户主目录、`.dsh`、各工作区旧路径里的 `usage-records.json` 按 `time` 去重合并进固定根(复用现有 `normalizeRecord`),不再只读当前根的那一份。
|
|
22
|
+
- **UI 路径一致**:统计页顶部显示的「数据持久化」路径恒等于真实落盘路径;写入不再绑定工作区沙箱策略,固定目录可正常写入。
|
|
23
|
+
- **数据迁移**:切换根目录时对旧根记录做真正合并(非复制/分裂),重启后自动恢复全部历史。
|
|
24
|
+
|
|
25
|
+
### 影响
|
|
26
|
+
- 旧版本落在 `%USERPROFILE%\dsh-usage`、`.dsh\dsh-usage`、各工作区 `dsh-usage` 下的历史记录,会在首次启动本版本时自动合并到固定目录,无需手动迁移。
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
11
30
|
## v1.9.1 (2026-08-16)
|
|
12
31
|
|
|
13
32
|
- **文档**: README 改为英文优先(`README.md` 英文 + 新增 `README.zh.md` 中文);补充「npm 包名已更换」醒目通知(旧包名 `@feiyang666/deepseekharnessdesktop` → 新包名 `@feiyang666/dsh-usage-plugin`),并移除发布教程等无关内容;修正 tarball 测试命令为新包名文件名。
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ dsh-usage-plugin is a **usage & cost tracker** plugin in the DeepSeek Harness ec
|
|
|
32
32
|
|
|
33
33
|
> Supports **Windows / macOS / Linux**: paths are handled per platform (`node:path`), and the folder picker / "reveal in file manager" use each OS's native mechanism (macOS: `osascript` / `open`; Linux: `zenity` / `xdg-open`). Balance query and export do not depend on Windows-only commands.
|
|
34
34
|
|
|
35
|
-
- **Usage & Cost**: records each model call's token usage and cache hits (input miss / cache hit / cache write / output / reasoning / finish reason), and computes cost using DeepSeek's peak/valley or base pricing (peak hours are automatically priced by Beijing time 09:00–12:00 and 14:00–18:00). Model names come from the actual request parameters, so non-DeepSeek models are shown truthfully instead of "unknown model"; models without an official price are counted as 0. The overview shows a by-model table plus a by-API-provider × model drill-down (each provider grouped with every model's calls and peak/off-peak cost split) and a grand total row.
|
|
35
|
+
- **Usage & Cost**: records each model call's token usage and cache hits (input miss / cache hit / cache write / output / reasoning / finish reason), and computes cost using DeepSeek's peak/valley or base pricing (peak hours are automatically priced by Beijing time 09:00–12:00 and 14:00–18:00). Model names come from the actual request parameters, so non-DeepSeek models are shown truthfully instead of "unknown model"; models without an official price are counted as 0. The overview shows a by-model table plus a by-API-provider × model drill-down (each provider grouped with every model's calls and peak/off-peak cost split) and a grand total row. The overview also supports **date filtering** (Today / Last 7 days / Last 30 days / All, plus a custom start–end range), so the aggregate stats can be scoped to any single day or date range.
|
|
36
36
|
- **Usage Calendar**: a monthly daily-usage heatmap (colored by cost or call count), hover for details including the peak/off-peak cost split, click a day for its call list and peak/off-peak totals, plus a per-day statistics table with peak cost / off-peak cost / total columns and monthly rollups.
|
|
37
37
|
- **Cache Hit List**: newest-first, fully scrollable, with quick filters (Today / 7 days / 30 days / All) and custom date ranges; the summary line and footer total split peak vs off-peak consumption with a grand cost total. The list is paginated (100 rows per page), so it stays smooth even with large data volumes.
|
|
38
38
|
- **Price Table**: the official DeepSeek API price table — base and peak/valley unit prices shown side by side (peak vs off-peak), editable in-panel and persisted to `pricing.json`, with a reset-to-default option.
|
|
@@ -195,11 +195,19 @@ For manual installs (Method B), do it in reverse: remove the `usage-plugin` row
|
|
|
195
195
|
|
|
196
196
|
## Data & locations
|
|
197
197
|
|
|
198
|
-
-
|
|
199
|
-
|
|
200
|
-
-
|
|
198
|
+
> **Since v1.9.2**, records are stored in a **fixed, dedicated data directory** (fixes [#4](https://github.com/feiyang-dev/dsh-usage-plugin/issues/4)). The path no longer follows the session workspace / `~/.dsh` / desktop-app install dir, so your history never "disappears" (counted as 0) when the workspace changes, and the path shown in the UI equals the on-disk path.
|
|
199
|
+
|
|
200
|
+
- **Records**: `<data root>/dsh-usage/usage-records.json`
|
|
201
|
+
- Resolution order for the **data root**:
|
|
202
|
+
1. env var `DSH_USAGE_DATA_DIR` (if set) — overrides everything;
|
|
203
|
+
2. Windows: `%LOCALAPPDATA%\dsh-usage-plugin` (falls back to `%APPDATA%` if `LOCALAPPDATA` is unset);
|
|
204
|
+
3. user home dir: `~/dsh-usage-data` (Windows `%USERPROFILE%\dsh-usage-data`, macOS/Linux `~/dsh-usage-data`).
|
|
205
|
+
- Default on Windows: `%LOCALAPPDATA%\dsh-usage-plugin\dsh-usage\usage-records.json`
|
|
206
|
+
- **Legacy data auto-merge**: on first start, records previously scattered in `%USERPROFILE%\dsh-usage`, `~/.dsh/dsh-usage`, and each workspace's `dsh-usage` (or `.dsh-usage-records.json`) are merged into the fixed root, deduplicated by `time` — no manual migration needed.
|
|
207
|
+
- Price config (edited & saved in the panel): `<data root>/dsh-usage/pricing.json`
|
|
208
|
+
- Default export dir: `<data root>/dsh-usage/{csv,json,images}/`
|
|
201
209
|
- Custom export dir: set in the panel's "Export target directory" or click "Choose directory…"
|
|
202
|
-
- Startup diagnostics (if the plugin fails to activate): `dsh-usage-boot.log`
|
|
210
|
+
- Startup diagnostics (if the plugin fails to activate): `dsh-usage-boot.log` next to the data root
|
|
203
211
|
|
|
204
212
|
---
|
|
205
213
|
|
|
@@ -251,6 +259,8 @@ pnpm dsh web
|
|
|
251
259
|
|
|
252
260
|
## Acknowledgements
|
|
253
261
|
|
|
262
|
+
- **[@Martin-soaring-dev](https://github.com/Martin-soaring-dev)**: prepared the plugin for public contribution (packaging, plugin-contract checks, docs & CI) and submitted the contribution branch that became the basis for the open-source releases ([#6](https://github.com/feiyang-dev/dsh-usage-plugin/pull/6)).
|
|
263
|
+
- **[@mumuer1024](https://github.com/mumuer1024)**: reported and diagnosed the persistence-path drift across workspaces (history "disappearing" / counted as 0) and proposed storing data in a fixed, dedicated directory ([#4](https://github.com/feiyang-dev/dsh-usage-plugin/issues/4)).
|
|
254
264
|
- **[@liu3734](https://github.com/liu3734)**: reported and diagnosed the Windows-only path handling / spawn issues on macOS (POSIX) and proposed the cross-platform fix ([#1](https://github.com/feiyang-dev/dsh-usage-plugin/issues/1)).
|
|
255
265
|
|
|
256
266
|
## License
|
package/README.zh.md
CHANGED
|
@@ -32,7 +32,7 @@ dsh-usage-plugin 是 DeepSeek Harness 生态的**用量与消耗统计插件**
|
|
|
32
32
|
|
|
33
33
|
> 支持 **Windows / macOS / Linux**:路径按当前平台处理(`node:path`),目录选择与「打开所在目录」均调用系统原生方式(macOS 用 `osascript` / `open`,Linux 用 `zenity` / `xdg-open`),余额查询与导出不依赖 Windows 专用命令。
|
|
34
34
|
|
|
35
|
-
- **用量与消耗**:记录每次模型调用的 token 用量与缓存命中(输入·未命中 / 缓存命中 / 缓存写入 / 输出 / 推理 / 结束原因),按 DeepSeek 峰谷/基础价格计算消耗(高峰时段自动按北京时间 9:00–12:00、14:00–18:00 计价)。模型名以请求参数为准如实显示(非 DeepSeek 模型不再显示为「未知模型」,无官方价格的模型消耗按 0 统计)。概览含「按模型」表与「按 API 服务商 ×
|
|
35
|
+
- **用量与消耗**:记录每次模型调用的 token 用量与缓存命中(输入·未命中 / 缓存命中 / 缓存写入 / 输出 / 推理 / 结束原因),按 DeepSeek 峰谷/基础价格计算消耗(高峰时段自动按北京时间 9:00–12:00、14:00–18:00 计价)。模型名以请求参数为准如实显示(非 DeepSeek 模型不再显示为「未知模型」,无官方价格的模型消耗按 0 统计)。概览含「按模型」表与「按 API 服务商 × 模型」明细表(每个服务商一组,组内列出各模型的调用与高峰/空闲分列消耗),底部有总费用合计。概览还支持**日期筛选**(今天 / 近 7 天 / 近 30 天 / 全部,外加自定义起止区间),可将聚合统计限定到任意一天或日期范围。
|
|
36
36
|
- **用量日历**:按月查看每日用量热力图(按消耗或调用数着色),悬停查看详情(含高峰 / 空闲消耗拆分)、点击某天查看当日调用明细与高峰/空闲消耗统计,附本月每日统计表(高峰消耗 / 空闲消耗 / 总消耗分列)与月度汇总。
|
|
37
37
|
- **缓存命中列表**:最新记录排在最前,支持 今天 / 近7天 / 近30天 / 全部 快捷筛选与自定义起止日期区间,汇总行与表尾合计区分高峰消耗 / 空闲消耗 / 总费用合计;列表分页渲染(每页 100 条),记录量大也不卡顿。
|
|
38
38
|
- **价格表**:**DeepSeek 官方 API 价格表**,展示基础价与峰谷价(高峰/空闲)单价表,高峰价与空闲价分列展示,支持在面板内直接编辑价格并持久化(数据目录 `pricing.json`),也可一键恢复默认。
|
|
@@ -195,11 +195,19 @@ dsh plugin --profile web remove @feiyang666/dsh-usage-plugin
|
|
|
195
195
|
|
|
196
196
|
## 数据与位置
|
|
197
197
|
|
|
198
|
-
-
|
|
199
|
-
|
|
200
|
-
-
|
|
198
|
+
> **自 v1.9.2 起**,记录存储在**固定专用数据目录**中(修复 [#4](https://github.com/feiyang-dev/dsh-usage-plugin/issues/4))。路径不再跟随会话工作区 / `~/.dsh` / 桌面端安装目录漂移,因此切换工作区时历史数据不会再"消失"(被统计为 0),且 UI 中显示的路径与真实落盘路径始终一致。
|
|
199
|
+
|
|
200
|
+
- 数据文件:`<数据根>/dsh-usage/usage-records.json`
|
|
201
|
+
- **数据根**的解析顺序:
|
|
202
|
+
1. 环境变量 `DSH_USAGE_DATA_DIR`(若已设置)——优先级最高,覆盖其它;
|
|
203
|
+
2. Windows:`%LOCALAPPDATA%\dsh-usage-plugin`(若 `LOCALAPPDATA` 未设置则回退到 `%APPDATA%`);
|
|
204
|
+
3. 用户主目录:`~/dsh-usage-data`(Windows 为 `%USERPROFILE%\dsh-usage-data`,macOS/Linux 为 `~/dsh-usage-data`)。
|
|
205
|
+
- Windows 下默认位置:`%LOCALAPPDATA%\dsh-usage-plugin\dsh-usage\usage-records.json`
|
|
206
|
+
- 旧数据自动合并:首次启动时,落在 `%USERPROFILE%\dsh-usage`、`~/.dsh/dsh-usage` 以及各工作区 `dsh-usage`(或 `.dsh-usage-records.json`)下的旧记录会按 `time` 去重合并进固定数据根,无需手动迁移。
|
|
207
|
+
- 价格配置(面板内编辑后保存):`<数据根>/dsh-usage/pricing.json`
|
|
208
|
+
- 导出目录(默认):`<数据根>/dsh-usage/{csv,json,images}/`
|
|
201
209
|
- 自定义导出目录:在面板「导出目标目录」里填写或点「选择目录…」
|
|
202
|
-
-
|
|
210
|
+
- 启动诊断日志(若插件激活失败):数据根旁的 `dsh-usage-boot.log`
|
|
203
211
|
|
|
204
212
|
---
|
|
205
213
|
|
|
@@ -251,6 +259,8 @@ pnpm dsh web
|
|
|
251
259
|
|
|
252
260
|
## 致谢
|
|
253
261
|
|
|
262
|
+
- **[@Martin-soaring-dev](https://github.com/Martin-soaring-dev)**:筹备了插件开源贡献(打包、插件契约校验、文档与 CI),并提交贡献分支,成为后续开源版本的基础([#6](https://github.com/feiyang-dev/dsh-usage-plugin/pull/6))。
|
|
263
|
+
- **[@mumuer1024](https://github.com/mumuer1024)**:报告并定位了持久化路径随会话工作区漂移导致历史数据"消失/统计为 0"的问题,提出把数据写到固定专用目录的方案([#4](https://github.com/feiyang-dev/dsh-usage-plugin/issues/4))。
|
|
254
264
|
- **[@liu3734](https://github.com/liu3734)**:报告并定位 macOS(POSIX)下路径处理与 spawn 的 Windows 专用问题,提出跨平台修复方案([#1](https://github.com/feiyang-dev/dsh-usage-plugin/issues/1))。
|
|
255
265
|
|
|
256
266
|
## 许可
|
package/cordis.patch.yml
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
# dsh-usage-plugin — bundle patch layer.
|
|
2
|
-
#
|
|
3
|
-
# This file is the package's `dsh.bundle.patch` layer. When the package is
|
|
4
|
-
# installed into a DSH profile (via `dsh plugin --profile <name> add
|
|
5
|
-
# @feiyang666/dsh-usage-plugin`, or manually), the loader applies this
|
|
6
|
-
# patch over the profile's empty root entry list, mounting the plugin row.
|
|
7
|
-
#
|
|
8
|
-
# The `inject` list is REQUIRED: it tells the Cordis loader to wait until the
|
|
9
|
-
# fs / webServer / subprocess / credentials / sandboxPolicy / agents
|
|
10
|
-
# are ready before activating the plugin. Without it the plugin's `apply`
|
|
11
|
-
# run before those services exist, the `/usage/api` route never registers,
|
|
12
|
-
# the client panel fails with `Unexpected end of JSON input`.
|
|
13
|
-
- insert:
|
|
14
|
-
- id: usage-plugin
|
|
15
|
-
name: '@feiyang666/dsh-usage-plugin'
|
|
16
|
-
inject:
|
|
17
|
-
- fs
|
|
18
|
-
- webServer
|
|
19
|
-
- subprocess
|
|
20
|
-
- credentials
|
|
21
|
-
-
|
|
22
|
-
-
|
|
1
|
+
# dsh-usage-plugin — bundle patch layer.
|
|
2
|
+
#
|
|
3
|
+
# This file is the package's `dsh.bundle.patch` layer. When the package is
|
|
4
|
+
# installed into a DSH profile (via `dsh plugin --profile <name> add
|
|
5
|
+
# @feiyang666/dsh-usage-plugin`, or manually), the loader applies this
|
|
6
|
+
# patch over the profile's empty root entry list, mounting the plugin row.
|
|
7
|
+
#
|
|
8
|
+
# The `inject` list is REQUIRED: it tells the Cordis loader to wait until the
|
|
9
|
+
# fs / webServer / subprocess / credentials / settings / sandboxPolicy / agents
|
|
10
|
+
# services are ready before activating the plugin. Without it the plugin's `apply`
|
|
11
|
+
# may run before those services exist, the `/usage/api` route never registers,
|
|
12
|
+
# and the client panel fails with `Unexpected end of JSON input`.
|
|
13
|
+
- insert:
|
|
14
|
+
- id: usage-plugin
|
|
15
|
+
name: '@feiyang666/dsh-usage-plugin'
|
|
16
|
+
inject:
|
|
17
|
+
- fs
|
|
18
|
+
- webServer
|
|
19
|
+
- subprocess
|
|
20
|
+
- credentials
|
|
21
|
+
- settings
|
|
22
|
+
- sandboxPolicy
|
|
23
|
+
- agents
|
package/lib/balance.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
const PROVIDERS = {
|
|
2
|
+
deepseek: {
|
|
3
|
+
id: 'deepseek',
|
|
4
|
+
name: 'DeepSeek',
|
|
5
|
+
credentialNames: ['DEEPSEEK_API_KEY'],
|
|
6
|
+
endpoint: 'https://api.deepseek.com/user/balance',
|
|
7
|
+
credentialHint: 'DEEPSEEK_API_KEY(推理 Key)',
|
|
8
|
+
queryMode: 'direct',
|
|
9
|
+
modelProviderAliases: ['deepseek']
|
|
10
|
+
},
|
|
11
|
+
siliconflow: {
|
|
12
|
+
id: 'siliconflow',
|
|
13
|
+
name: 'SiliconFlow',
|
|
14
|
+
credentialNames: [],
|
|
15
|
+
endpoint: 'https://api.siliconflow.cn/v1/user/info',
|
|
16
|
+
credentialHint: '模型设置中名为 siliconflow 的提供商所引用的 API Key',
|
|
17
|
+
queryMode: 'direct',
|
|
18
|
+
modelProviderAliases: ['siliconflow', 'silicon-flow'],
|
|
19
|
+
credentialHelpUrl: 'https://cloud.siliconflow.cn/account/ak'
|
|
20
|
+
},
|
|
21
|
+
digitalocean: {
|
|
22
|
+
id: 'digitalocean',
|
|
23
|
+
name: 'DigitalOcean',
|
|
24
|
+
credentialNames: ['DIGITALOCEAN_TOKEN', 'DIGITALOCEAN_ACCESS_TOKEN'],
|
|
25
|
+
endpoint: 'https://api.digitalocean.com/v2/customers/my/balance',
|
|
26
|
+
credentialHint: 'DIGITALOCEAN_TOKEN(账户级 Personal Access Token,不是 DO AI 推理 Key)',
|
|
27
|
+
queryMode: 'account',
|
|
28
|
+
modelProviderAliases: ['digitalocean', 'digital-ocean'],
|
|
29
|
+
credentialHelpUrl: 'https://cloud.digitalocean.com/account/api/tokens'
|
|
30
|
+
},
|
|
31
|
+
'amd-gpu-cloud': {
|
|
32
|
+
id: 'amd-gpu-cloud',
|
|
33
|
+
name: 'AMD GPU Cloud',
|
|
34
|
+
credentialNames: [],
|
|
35
|
+
endpoint: '',
|
|
36
|
+
credentialHint: 'AMD GPU Cloud 当前未公开余额查询端点',
|
|
37
|
+
queryMode: 'unsupported',
|
|
38
|
+
modelProviderAliases: ['amd', 'amd-gpu-cloud'],
|
|
39
|
+
credentialHelpUrl: 'https://www.amd.com/en/developer/resources/cloud-access/amd-developer-cloud.html'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const BALANCE_PROVIDERS = Object.freeze(PROVIDERS)
|
|
44
|
+
|
|
45
|
+
export function providerList() {
|
|
46
|
+
return Object.values(PROVIDERS).map((p) => ({
|
|
47
|
+
id: p.id,
|
|
48
|
+
name: p.name,
|
|
49
|
+
credentialHint: p.credentialHint,
|
|
50
|
+
queryMode: p.queryMode,
|
|
51
|
+
credentialHelpUrl: p.credentialHelpUrl || ''
|
|
52
|
+
}))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getBalanceProvider(id) {
|
|
56
|
+
return PROVIDERS[String(id || 'deepseek').toLowerCase()] || null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizedRoute(value) {
|
|
60
|
+
return String(value || '').toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function matchesModelProvider(providerId, routeId, displayName) {
|
|
64
|
+
const provider = getBalanceProvider(providerId)
|
|
65
|
+
if (!provider) return false
|
|
66
|
+
const candidates = [routeId, displayName].map(normalizedRoute).filter(Boolean)
|
|
67
|
+
const aliases = (provider.modelProviderAliases || []).map(normalizedRoute)
|
|
68
|
+
return candidates.some((candidate) => aliases.includes(candidate))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function resolveBalanceEndpoint(providerId, modelBaseURL) {
|
|
72
|
+
const provider = getBalanceProvider(providerId)
|
|
73
|
+
if (!provider) return ''
|
|
74
|
+
if (provider.id !== 'siliconflow' || !modelBaseURL) return provider.endpoint
|
|
75
|
+
try {
|
|
76
|
+
const url = new URL(String(modelBaseURL))
|
|
77
|
+
const allowed = url.protocol === 'https:'
|
|
78
|
+
&& !url.username
|
|
79
|
+
&& !url.password
|
|
80
|
+
&& !url.port
|
|
81
|
+
&& (url.hostname === 'api.siliconflow.cn' || url.hostname === 'api.siliconflow.com')
|
|
82
|
+
if (allowed) return url.origin + '/v1/user/info'
|
|
83
|
+
} catch (e) {}
|
|
84
|
+
return provider.endpoint
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fail(message) {
|
|
88
|
+
return { ok: false, error: message }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function json(text) {
|
|
92
|
+
try { return JSON.parse(text) } catch (e) { return null }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function value(source, names) {
|
|
96
|
+
if (!source || typeof source !== 'object') return undefined
|
|
97
|
+
for (const name of names) {
|
|
98
|
+
if (source[name] !== undefined && source[name] !== null && source[name] !== '') return source[name]
|
|
99
|
+
}
|
|
100
|
+
return undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function money(v, fallback = '0') {
|
|
104
|
+
return String(v === undefined || v === null || v === '' ? fallback : v)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function result(provider, fields) {
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
provider: provider.id,
|
|
111
|
+
providerName: provider.name,
|
|
112
|
+
queriedAt: Date.now(),
|
|
113
|
+
...fields
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseDeepSeek(provider, data) {
|
|
118
|
+
const infos = data && Array.isArray(data.balance_infos) ? data.balance_infos : []
|
|
119
|
+
if (!infos.length) return fail('DeepSeek 响应中没有余额信息')
|
|
120
|
+
const b = infos[0] || {}
|
|
121
|
+
const legacyInfo = {
|
|
122
|
+
currency: String(b.currency || 'CNY'),
|
|
123
|
+
totalBalance: money(b.total_balance),
|
|
124
|
+
grantedBalance: money(b.granted_balance),
|
|
125
|
+
toppedUpBalance: money(b.topped_up_balance)
|
|
126
|
+
}
|
|
127
|
+
return result(provider, {
|
|
128
|
+
currency: legacyInfo.currency,
|
|
129
|
+
isAvailable: data.is_available == null ? null : data.is_available === true,
|
|
130
|
+
totalBalance: legacyInfo.totalBalance,
|
|
131
|
+
infos: [legacyInfo],
|
|
132
|
+
details: [
|
|
133
|
+
{ label: '充值余额', value: legacyInfo.toppedUpBalance, hint: '实际充值' },
|
|
134
|
+
{ label: '赠送余额', value: legacyInfo.grantedBalance, hint: '平台赠送' }
|
|
135
|
+
],
|
|
136
|
+
sourceNote: '数据来自 DeepSeek 官方 /user/balance 接口。'
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseSiliconFlow(provider, data) {
|
|
141
|
+
if (data && data.code !== undefined && Number(data.code) !== 20000) {
|
|
142
|
+
return fail('SiliconFlow 接口返回错误:' + String(data.message || data.code))
|
|
143
|
+
}
|
|
144
|
+
const d = data && data.data && typeof data.data === 'object' ? data.data : data
|
|
145
|
+
const total = value(d, ['totalBalance', 'total_balance'])
|
|
146
|
+
const granted = value(d, ['balance', 'grantedBalance', 'granted_balance'])
|
|
147
|
+
const charged = value(d, ['chargeBalance', 'charge_balance', 'toppedUpBalance', 'topped_up_balance'])
|
|
148
|
+
if (total === undefined && granted === undefined && charged === undefined) {
|
|
149
|
+
return fail('SiliconFlow 响应中没有可识别的余额字段')
|
|
150
|
+
}
|
|
151
|
+
const computed = total !== undefined ? total : (Number(granted || 0) + Number(charged || 0))
|
|
152
|
+
const numeric = [computed, granted || 0, charged || 0].map((entry) => Number(entry))
|
|
153
|
+
return result(provider, {
|
|
154
|
+
currency: String(value(d, ['currency']) || 'CNY'),
|
|
155
|
+
isAvailable: null,
|
|
156
|
+
totalBalance: money(computed),
|
|
157
|
+
zeroBalance: numeric.every((entry) => Number.isFinite(entry) && entry === 0),
|
|
158
|
+
details: [
|
|
159
|
+
{ label: '充值余额', value: money(charged), hint: 'chargeBalance · 用户充值余额' },
|
|
160
|
+
{ label: '赠送/旧免费余额', value: money(granted), hint: 'balance · 旧版赠送余额字段' }
|
|
161
|
+
],
|
|
162
|
+
fieldDefinitions: [
|
|
163
|
+
{ name: 'totalBalance', meaning: '公开 API 返回的总余额,通常为 balance 与 chargeBalance 的合计' },
|
|
164
|
+
{ name: 'chargeBalance', meaning: '用户充值形成的余额' },
|
|
165
|
+
{ name: 'balance', meaning: '赠送或旧版免费余额字段' }
|
|
166
|
+
],
|
|
167
|
+
sourceNote: '数据来自 SiliconFlow 官方 /v1/user/info 接口;该接口未公开代金券或历史用量字段,数值可能与控制台可用总额不同。'
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseDigitalOcean(provider, data) {
|
|
172
|
+
const account = value(data, ['account_balance'])
|
|
173
|
+
if (account === undefined) return fail('DigitalOcean 响应中没有 account_balance 字段')
|
|
174
|
+
const accountNumber = Number(account)
|
|
175
|
+
if (!Number.isFinite(accountNumber)) return fail('DigitalOcean account_balance 不是有效数字')
|
|
176
|
+
const credit = accountNumber < 0 ? Math.abs(accountNumber) : 0
|
|
177
|
+
const due = accountNumber > 0 ? accountNumber : 0
|
|
178
|
+
const balanceKind = credit > 0 ? 'credit' : (due > 0 ? 'due' : 'settled')
|
|
179
|
+
return result(provider, {
|
|
180
|
+
currency: 'USD',
|
|
181
|
+
isAvailable: null,
|
|
182
|
+
totalBalance: money(credit > 0 ? credit : due),
|
|
183
|
+
balanceKind,
|
|
184
|
+
balanceLabel: balanceKind === 'credit' ? '可用信用余额' : (balanceKind === 'due' ? '待结算账户余额' : '账户余额'),
|
|
185
|
+
rawAccountBalance: money(account),
|
|
186
|
+
monthToDateUsage: money(value(data, ['month_to_date_usage'])),
|
|
187
|
+
monthToDateBalance: money(value(data, ['month_to_date_balance'])),
|
|
188
|
+
details: [
|
|
189
|
+
{ label: '本月至今使用', value: money(value(data, ['month_to_date_usage'])), hint: 'month_to_date_usage · 当前账期使用金额' }
|
|
190
|
+
],
|
|
191
|
+
generatedAt: String(value(data, ['generated_at']) || ''),
|
|
192
|
+
sourceNote: '数据来自 DigitalOcean 账户级 Billing API;负数账户余额按可用信用额取绝对值展示,DO AI 推理 Key 不能用于此查询。'
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function parseBalanceResponse(providerId, text) {
|
|
197
|
+
const provider = getBalanceProvider(providerId)
|
|
198
|
+
if (!provider) return fail('不支持的余额服务商:' + String(providerId || ''))
|
|
199
|
+
const data = json(text)
|
|
200
|
+
if (data === null) return fail('无法解析 ' + provider.name + ' 余额响应')
|
|
201
|
+
if (provider.id === 'deepseek') return parseDeepSeek(provider, data)
|
|
202
|
+
if (provider.id === 'siliconflow') return parseSiliconFlow(provider, data)
|
|
203
|
+
if (provider.id === 'digitalocean') return parseDigitalOcean(provider, data)
|
|
204
|
+
return fail(provider.name + ' 当前没有可用的公开余额查询接口')
|
|
205
|
+
}
|