@feiyang666/dsh-usage-plugin 1.12.2 → 1.13.0
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 +268 -229
- package/README.md +311 -308
- package/README.zh.md +4 -1
- package/lib/balance.js +117 -17
- package/lib/client.js +499 -256
- package/lib/index.js +143 -2
- package/package.json +80 -77
package/README.zh.md
CHANGED
|
@@ -36,11 +36,12 @@ dsh-usage-plugin 是 DeepSeek Harness 生态的**用量与消耗统计插件**
|
|
|
36
36
|
- **用量日历**:按月查看每日用量热力图(按消耗或调用数着色),悬停查看详情(含高峰 / 空闲消耗拆分)、点击某天查看当日调用明细与高峰/空闲消耗统计,附本月每日统计表(高峰消耗 / 空闲消耗 / 总消耗分列)与月度汇总。
|
|
37
37
|
- **缓存命中列表**:最新记录排在最前,支持 今天 / 近7天 / 近30天 / 全部 快捷筛选与自定义起止日期区间,汇总行与表尾合计区分高峰消耗 / 空闲消耗 / 总费用合计;列表分页渲染(每页 100 条),记录量大也不卡顿。
|
|
38
38
|
- **价格表**:**DeepSeek 官方 API 价格表**(覆盖官方模型 `deepseek-v4-flash` / `deepseek-v4-flash-vision-exp` / `deepseek-v4-pro`),展示基础价与峰谷价(高峰/空闲)单价表,高峰价与空闲价分列展示,支持在面板内直接编辑价格并持久化(数据目录 `pricing.json`),也可一键恢复默认。
|
|
39
|
-
- **剩余余额查询**:用当前配置的 `DEEPSEEK_API_KEY` 查询 DeepSeek
|
|
39
|
+
- **剩余余额查询**:用当前配置的 `DEEPSEEK_API_KEY` 查询 DeepSeek 账户余额;并支持 **百炼 Token Plan 配额查询**(复用 `bl auth login --console` 的控制台 OAuth token,无需阿里云 AccessKey,展示本周配额已用百分比与重置时间)。
|
|
40
40
|
- **导出**:CSV / JSON / **PNG 长图**(按最新在前展示,最多含最近 2000 条,超出会提示;PNG 报告含高峰 / 空闲消耗分列统计),可导出到任意目录(原生目录选择器),导出后自动打开所在目录。
|
|
41
41
|
- **导入**:选择文件(JSON / CSV)合并导入,按时间去重。
|
|
42
42
|
- **持久化**:记录实时落盘到**固定专用数据目录**(如 `%LOCALAPPDATA%\dsh-usage-plugin\dsh-usage\usage-records.json`,详见下文「数据目录」),与当前工作区无关,重启自动恢复(上限 100000 条,尽量多存)。
|
|
43
43
|
- **界面适配**:面板字号跟随应用「显示大小」设置自动缩放(em 相对字号),面板宽度以视口封顶、宽表格在容器内横向滑动(max-content + overflow-x),任何窗口大小下所有列与合计都完整可见,不会裁掉右侧内容。
|
|
44
|
+
- **英文界面(i18n)**:面板按浏览器语言自动选择界面语言(中文浏览器保持中文,其他默认英文);「用量与消耗」头部新增切换按钮,点击即时生效,选择保存在 `localStorage`(键 `dsh-usage-lang`)。面板内容即时切换;外部「会话 / 设置」标签页名在启动时注册一次,页面刷新后更新为新语言。
|
|
44
45
|
|
|
45
46
|
---
|
|
46
47
|
|
|
@@ -299,6 +300,8 @@ pnpm dsh web
|
|
|
299
300
|
|
|
300
301
|
## 致谢
|
|
301
302
|
|
|
303
|
+
- **[@ayleen](https://github.com/ayleen)**:为 Web 面板实现完整的英文界面(i18n 层)与响应式语言切换,余额查询改为语义化字段([#7](https://github.com/feiyang-dev/dsh-usage-plugin/pull/7))。
|
|
304
|
+
- **[@wuhuqif176](https://github.com/wuhuqif176)**:在「剩余余额查询」中新增百炼(Qwen)Token Plan 配额查询([#8](https://github.com/feiyang-dev/dsh-usage-plugin/pull/8))。
|
|
302
305
|
- **[@Martin-soaring-dev](https://github.com/Martin-soaring-dev)**:筹备了插件开源贡献(打包、插件契约校验、文档与 CI),并提交贡献分支,成为后续开源版本的基础([#6](https://github.com/feiyang-dev/dsh-usage-plugin/pull/6))。
|
|
303
306
|
- **[@mumuer1024](https://github.com/mumuer1024)**:报告并定位了持久化路径随会话工作区漂移导致历史数据"消失/统计为 0"的问题,提出把数据写到固定专用目录的方案([#4](https://github.com/feiyang-dev/dsh-usage-plugin/issues/4))。
|
|
304
307
|
- **[@liu3734](https://github.com/liu3734)**:报告并定位 macOS(POSIX)下路径处理与 spawn 的 Windows 专用问题,提出跨平台修复方案([#1](https://github.com/feiyang-dev/dsh-usage-plugin/issues/1))。
|
package/lib/balance.js
CHANGED
|
@@ -4,7 +4,7 @@ const PROVIDERS = {
|
|
|
4
4
|
name: 'DeepSeek',
|
|
5
5
|
credentialNames: ['DEEPSEEK_API_KEY'],
|
|
6
6
|
endpoint: 'https://api.deepseek.com/user/balance',
|
|
7
|
-
|
|
7
|
+
credentialHintKey: 'hint.deepseek',
|
|
8
8
|
queryMode: 'direct',
|
|
9
9
|
modelProviderAliases: ['deepseek']
|
|
10
10
|
},
|
|
@@ -13,7 +13,7 @@ const PROVIDERS = {
|
|
|
13
13
|
name: 'SiliconFlow',
|
|
14
14
|
credentialNames: [],
|
|
15
15
|
endpoint: 'https://api.siliconflow.cn/v1/user/info',
|
|
16
|
-
|
|
16
|
+
credentialHintKey: 'hint.siliconflow',
|
|
17
17
|
queryMode: 'direct',
|
|
18
18
|
modelProviderAliases: ['siliconflow', 'silicon-flow'],
|
|
19
19
|
credentialHelpUrl: 'https://cloud.siliconflow.cn/account/ak'
|
|
@@ -23,7 +23,7 @@ const PROVIDERS = {
|
|
|
23
23
|
name: 'DigitalOcean',
|
|
24
24
|
credentialNames: ['DIGITALOCEAN_TOKEN', 'DIGITALOCEAN_ACCESS_TOKEN'],
|
|
25
25
|
endpoint: 'https://api.digitalocean.com/v2/customers/my/balance',
|
|
26
|
-
|
|
26
|
+
credentialHintKey: 'hint.digitalocean',
|
|
27
27
|
queryMode: 'account',
|
|
28
28
|
modelProviderAliases: ['digitalocean', 'digital-ocean'],
|
|
29
29
|
credentialHelpUrl: 'https://cloud.digitalocean.com/account/api/tokens'
|
|
@@ -33,10 +33,20 @@ const PROVIDERS = {
|
|
|
33
33
|
name: 'AMD GPU Cloud',
|
|
34
34
|
credentialNames: [],
|
|
35
35
|
endpoint: '',
|
|
36
|
-
|
|
36
|
+
credentialHintKey: 'hint.amd',
|
|
37
37
|
queryMode: 'unsupported',
|
|
38
38
|
modelProviderAliases: ['amd', 'amd-gpu-cloud'],
|
|
39
39
|
credentialHelpUrl: 'https://www.amd.com/en/developer/resources/cloud-access/amd-developer-cloud.html'
|
|
40
|
+
},
|
|
41
|
+
'qwen-token-plan': {
|
|
42
|
+
id: 'qwen-token-plan',
|
|
43
|
+
name: '百炼 Token Plan',
|
|
44
|
+
credentialNames: ['BAILIAN_CONSOLE_TOKEN'],
|
|
45
|
+
endpoint: '',
|
|
46
|
+
credentialHint: '百炼控制台 OAuth 登录 token(~/.bailian/config.json 的 access_token,用 bl auth login --console 生成)',
|
|
47
|
+
queryMode: 'console-token',
|
|
48
|
+
modelProviderAliases: ['aliyun', 'qwen', 'bailian', 'token-plan', '百炼', '阿里云百炼'],
|
|
49
|
+
credentialHelpUrl: 'https://bailian.console.aliyun.com/cli'
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
52
|
|
|
@@ -46,7 +56,7 @@ export function providerList() {
|
|
|
46
56
|
return Object.values(PROVIDERS).map((p) => ({
|
|
47
57
|
id: p.id,
|
|
48
58
|
name: p.name,
|
|
49
|
-
|
|
59
|
+
credentialHintKey: p.credentialHintKey,
|
|
50
60
|
queryMode: p.queryMode,
|
|
51
61
|
credentialHelpUrl: p.credentialHelpUrl || ''
|
|
52
62
|
}))
|
|
@@ -56,6 +66,16 @@ export function getBalanceProvider(id) {
|
|
|
56
66
|
return PROVIDERS[String(id || 'deepseek').toLowerCase()] || null
|
|
57
67
|
}
|
|
58
68
|
|
|
69
|
+
export function missingCredentialError(provider) {
|
|
70
|
+
const names = ((provider && provider.credentialNames) || []).join(' / ')
|
|
71
|
+
const subject = names || ((provider && provider.name) || 'provider')
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
errorCode: 'missing-credential',
|
|
75
|
+
error: '未找到 ' + subject + ',请配置后重试'
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
59
79
|
function normalizedRoute(value) {
|
|
60
80
|
return String(value || '').toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
61
81
|
}
|
|
@@ -130,10 +150,10 @@ function parseDeepSeek(provider, data) {
|
|
|
130
150
|
totalBalance: legacyInfo.totalBalance,
|
|
131
151
|
infos: [legacyInfo],
|
|
132
152
|
details: [
|
|
133
|
-
{
|
|
134
|
-
{
|
|
153
|
+
{ labelKey: 'toppedUp', value: legacyInfo.toppedUpBalance, hintKey: 'actualTopUp' },
|
|
154
|
+
{ labelKey: 'granted', value: legacyInfo.grantedBalance, hintKey: 'platformGift' }
|
|
135
155
|
],
|
|
136
|
-
|
|
156
|
+
sourceNoteKey: 'note.deepseek'
|
|
137
157
|
})
|
|
138
158
|
}
|
|
139
159
|
|
|
@@ -156,15 +176,94 @@ function parseSiliconFlow(provider, data) {
|
|
|
156
176
|
totalBalance: money(computed),
|
|
157
177
|
zeroBalance: numeric.every((entry) => Number.isFinite(entry) && entry === 0),
|
|
158
178
|
details: [
|
|
159
|
-
{
|
|
160
|
-
{
|
|
179
|
+
{ labelKey: 'toppedUp', value: money(charged), hintKey: 'field.chargeBalance' },
|
|
180
|
+
{ labelKey: 'grantedLegacy', value: money(granted), hintKey: 'field.balance' }
|
|
161
181
|
],
|
|
162
182
|
fieldDefinitions: [
|
|
163
|
-
{ name: 'totalBalance',
|
|
164
|
-
{ name: 'chargeBalance',
|
|
165
|
-
{ name: 'balance',
|
|
183
|
+
{ name: 'totalBalance', meaningKey: 'meaning.totalBalance' },
|
|
184
|
+
{ name: 'chargeBalance', meaningKey: 'meaning.chargeBalance' },
|
|
185
|
+
{ name: 'balance', meaningKey: 'meaning.balance' }
|
|
166
186
|
],
|
|
167
|
-
|
|
187
|
+
sourceNoteKey: 'note.siliconflow'
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function fmtPct(value) {
|
|
192
|
+
const n = Number(value)
|
|
193
|
+
if (!Number.isFinite(n)) return null
|
|
194
|
+
const t = Math.round(n * 1000) / 10
|
|
195
|
+
return (Number.isInteger(t) ? String(t) : t.toFixed(1)) + '%'
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function fmtResetEpoch(epochMs) {
|
|
199
|
+
const n = Number(epochMs)
|
|
200
|
+
if (!Number.isFinite(n) || n <= 0) return null
|
|
201
|
+
const d = new Date(n + 8 * 3600 * 1000)
|
|
202
|
+
const pad = (x) => (x < 10 ? '0' : '') + x
|
|
203
|
+
const local = d.getUTCFullYear() + '-' + pad(d.getUTCMonth() + 1) + '-' + pad(d.getUTCDate()) + ' ' + pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ':' + pad(d.getUTCSeconds())
|
|
204
|
+
let diff = Math.max(0, n - Date.now())
|
|
205
|
+
const minutes = Math.floor(diff / 60000)
|
|
206
|
+
const days = Math.floor(minutes / 1440)
|
|
207
|
+
const hours = Math.floor((minutes % 1440) / 60)
|
|
208
|
+
const mins = minutes % 60
|
|
209
|
+
let rel
|
|
210
|
+
if (minutes <= 0) rel = 'now'
|
|
211
|
+
else {
|
|
212
|
+
const parts = []
|
|
213
|
+
if (days) parts.push(days + 'd')
|
|
214
|
+
if (hours) parts.push(hours + 'h')
|
|
215
|
+
if (mins) parts.push(mins + 'm')
|
|
216
|
+
rel = parts.join(' ') || 'now'
|
|
217
|
+
}
|
|
218
|
+
return local + '(' + rel + ')'
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function parseQwenTokenPlan(provider, data) {
|
|
222
|
+
// 百炼 Token Plan 只有 1 周配额(无 5 小时配额):
|
|
223
|
+
// per1WeekPercentage 0~1 的已用比例
|
|
224
|
+
// per1WeekResetTime 毫秒时间戳,即本周配额重置(结束)时间
|
|
225
|
+
const hasPct = data && data.per1WeekPercentage != null
|
|
226
|
+
const hasEnd = data && data.per1WeekResetTime != null
|
|
227
|
+
if (!hasPct && !hasEnd) return fail('百炼 Token Plan 响应中没有可识别的配额字段')
|
|
228
|
+
|
|
229
|
+
// 进度条需要 0~1 的数值
|
|
230
|
+
const ratio = Number(data.per1WeekPercentage)
|
|
231
|
+
const ratioNum = Number.isFinite(ratio) ? Math.min(Math.max(ratio, 0), 1) : null
|
|
232
|
+
const usedPct = ratioNum != null ? fmtPct(ratioNum) : null
|
|
233
|
+
|
|
234
|
+
// 结束(重置)时间 -> 开始时间 = 结束 - 整周
|
|
235
|
+
let weekEnd = null
|
|
236
|
+
let weekStart = null
|
|
237
|
+
if (data.per1WeekResetTime != null) {
|
|
238
|
+
const endNum = Number(data.per1WeekResetTime)
|
|
239
|
+
if (Number.isFinite(endNum) && endNum > 0) {
|
|
240
|
+
weekEnd = endNum
|
|
241
|
+
weekStart = endNum - 7 * 24 * 3600 * 1000
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const formatDate = (ms) => {
|
|
246
|
+
if (!Number.isFinite(ms)) return null
|
|
247
|
+
const d = new Date(ms + 8 * 3600 * 1000)
|
|
248
|
+
const pad = (x) => (x < 10 ? '0' : '') + x
|
|
249
|
+
return d.getUTCFullYear() + '-' + pad(d.getUTCMonth() + 1) + '-' + pad(d.getUTCDate())
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return result(provider, {
|
|
253
|
+
currency: '',
|
|
254
|
+
isAvailable: null,
|
|
255
|
+
totalBalance: usedPct || '暂无信息',
|
|
256
|
+
balanceLabel: '百炼 Token Plan 配额用量',
|
|
257
|
+
kind: 'quota-usage', // 标记为配额进度类,非货币余额
|
|
258
|
+
qwenQuota: {
|
|
259
|
+
ratio: ratioNum, // 0~1 已用比例(进度条用)
|
|
260
|
+
usedPercent: usedPct, // 如 "39.7%"
|
|
261
|
+
weekStart: formatDate(weekStart), // "2026-08-21"
|
|
262
|
+
weekEnd: formatDate(weekEnd), // "2026-08-28"
|
|
263
|
+
ratioLabel: usedPct ? '已用 ' + usedPct : '暂无信息'
|
|
264
|
+
},
|
|
265
|
+
details: [],
|
|
266
|
+
sourceNote: '数据来自阿里云百炼控制台内部门户网关(zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage),展示的是 1 周配额已用进度与周期。'
|
|
168
267
|
})
|
|
169
268
|
}
|
|
170
269
|
|
|
@@ -181,15 +280,15 @@ function parseDigitalOcean(provider, data) {
|
|
|
181
280
|
isAvailable: null,
|
|
182
281
|
totalBalance: money(credit > 0 ? credit : due),
|
|
183
282
|
balanceKind,
|
|
184
|
-
|
|
283
|
+
balanceLabelKey: balanceKind === 'credit' ? 'bal.credit' : (balanceKind === 'due' ? 'bal.due' : 'bal.settled'),
|
|
185
284
|
rawAccountBalance: money(account),
|
|
186
285
|
monthToDateUsage: money(value(data, ['month_to_date_usage'])),
|
|
187
286
|
monthToDateBalance: money(value(data, ['month_to_date_balance'])),
|
|
188
287
|
details: [
|
|
189
|
-
{
|
|
288
|
+
{ labelKey: 'monthToDateUsage', value: money(value(data, ['month_to_date_usage'])), hintKey: 'field.monthToDateUsage' }
|
|
190
289
|
],
|
|
191
290
|
generatedAt: String(value(data, ['generated_at']) || ''),
|
|
192
|
-
|
|
291
|
+
sourceNoteKey: 'note.digitalocean'
|
|
193
292
|
})
|
|
194
293
|
}
|
|
195
294
|
|
|
@@ -201,5 +300,6 @@ export function parseBalanceResponse(providerId, text) {
|
|
|
201
300
|
if (provider.id === 'deepseek') return parseDeepSeek(provider, data)
|
|
202
301
|
if (provider.id === 'siliconflow') return parseSiliconFlow(provider, data)
|
|
203
302
|
if (provider.id === 'digitalocean') return parseDigitalOcean(provider, data)
|
|
303
|
+
if (provider.id === 'qwen-token-plan') return parseQwenTokenPlan(provider, data)
|
|
204
304
|
return fail(provider.name + ' 当前没有可用的公开余额查询接口')
|
|
205
305
|
}
|