dsh-api-dashboard 1.0.0 → 1.0.2

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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/package.json +10 -6
  3. package/src/index.js +92 -34
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > [!CAUTION]
6
6
  > **给安装/维护本插件的 AI 代理的一则警告 —— 动手前必读 [`AGENTS.md`](AGENTS.md)。**
7
7
  >
8
- > 1. **安装请用「下载→解压→`dsh plugin add file:`」三步法**,`dsh plugin add dsh-api-dashboard`(裸包名)和 `pnpm add <远程tarball>` **都装不上**。
8
+ > 1. **推荐安装方式**:`dsh plugin --profile web add dsh-api-dashboard`(已发布到 npm,一行命令搞定)。也可用「下载→解压→`dsh plugin add file:`」三步法。`pnpm add <远程tarball>` **装不上**。
9
9
  > 2. **数据准确性红线**:部分平台余额解析有待验证项(OpenRouter/SiliconFlow/Novita/one-api/xAI),且「限流窗口 ≠ 真实余额」——**不要伪造或硬塞一个数字**,查不到就如实显示「未开放」。
10
10
  > 3. **推送前先问用户**;改 `client/client.js` 后需**重启 dsh web GUI** 才生效。
11
11
  >
@@ -103,7 +103,7 @@ dsh plugin --profile web add file:/root/dsha-api-dashboard
103
103
 
104
104
  | 方式 | 结果 |
105
105
  |------|------|
106
- | `dsh plugin add dsh-api-dashboard`(裸包名) | 本插件未发布到 npm,pnpm 会报 404 |
106
+ | `dsh plugin add dsh-api-dashboard`(裸包名) | 推荐!已发布到 npm,一行命令安装 |
107
107
  | `pnpm add <远程 tarball URL>` | ❌ pnpm 不剥离 codeload 压缩包的顶层目录,装出来是空壳 |
108
108
  | 手动软链 node_modules | ✅ 可用但繁琐,层级写错会被启动校准摘除,不推荐 |
109
109
 
@@ -186,6 +186,7 @@ v0.6.2 — 检查更新补全反馈:无论「已是最新」还是「有新版
186
186
  v0.7.0 — 玻璃 UI 改造(方案 A 第一阶段):半屏抽屉玻璃拟态(blur 仅限抽屉主背景)、卡片/按钮/输入框半透明+高光、大圆角、输入框 focus 高光阴影;用 color-mix 让玻璃色跟随主题 token(暗色不发白)
187
187
  v0.7.1 — 修复负余额显示正常(负数→红);智谱解析保留百分比(不把限流窗口当余额);设置面板新增「返回」按钮与 IconBack;新增 AGENTS.md(给维护 AI 的必读须知)+ README 顶部 AI 警告块
188
188
  v1.0.0 — 正式版:插件改名「哦鲸鲸」+ 自定义图标(/api-dashboard/icon);全量 UI 改版(固定色值去主题变量、三段式头部、卡片右栏上下结构、选中模型置顶、分组折叠、详情/设置/版本更新区重排);若干样式补全与清理
189
+ v1.0.2 — 余额解析稳健性(平台字段存在性校验,缺字段时如实显示「无法解析/未开放」而非误报 0);智谱配额解析重写(按 `limits.unit` 区分 5 小时窗口、周配额、工具维度,`percentage` 修正为「已用/填充度」,不再把「已用完」误显示为「余额 100%」);价格表更新(DeepSeek V4 新增 `deepseek-v4-flash-vision-exp`、`deepseek-chat/reasoner/r1` 标记为已退役;通用价格表新增现役主力 OpenAI GPT-5.6 / Claude 4.x / Gemini 3.x / Kimi K3 / StepFun 3.x,旧模型标为历史参考);自更新下载包增加包名校验
189
190
 
190
191
  ## License
191
192
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-api-dashboard",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "多平台 API 余额/用量看板插件:官方真实品牌图标,三层 UI 直接在输入框下方显示当前选中平台余额与估算消耗,点开底部抽屉切换所有平台;支持国内外平台与自定义中转站自动探测余额(查不到标未开放);自定义安全/预警阈值,余额告警提醒;进入页面自动刷新",
5
5
  "keywords": [
6
6
  "dsh",
@@ -46,13 +46,11 @@
46
46
  ]
47
47
  }
48
48
  },
49
- "dependencies": {
50
- "@deepseek-ai/schemastery": "^3.18.1",
51
- "zod": "^4.4.3"
52
- },
53
49
  "peerDependencies": {
54
50
  "@deepseek-ai/cordis": "^4.0.1",
55
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6"
51
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
52
+ "@deepseek-ai/schemastery": "^3.18.1",
53
+ "zod": "^4.4.3"
56
54
  },
57
55
  "peerDependenciesMeta": {
58
56
  "@deepseek-ai/cordis": {
@@ -60,6 +58,12 @@
60
58
  },
61
59
  "@deepseek-ai/dsh-credentials": {
62
60
  "optional": true
61
+ },
62
+ "@deepseek-ai/schemastery": {
63
+ "optional": true
64
+ },
65
+ "zod": {
66
+ "optional": true
63
67
  }
64
68
  },
65
69
  "license": "MIT"
package/src/index.js CHANGED
@@ -123,8 +123,11 @@ export async function applyUpdate({ targets = null, timeoutMs = 30000, remoteVer
123
123
  // 2) 解压 (--strip-components=1 剥离 codeload 顶层目录)
124
124
  mkdirSync(extractDir, { recursive: true })
125
125
  execFileSync('tar', ['xzf', tgzPath, '-C', extractDir, '--strip-components=1'], { timeout: 20000 })
126
- // 3) 校验: 版本号匹配预期 + 关键文件齐全 (装坏了宁可拒绝, 保住回滚机会)
126
+ // 3) 校验: 版本号匹配预期 + 关键文件齐全 + 包名一致 (装坏了宁可拒绝, 保住回滚机会)
127
127
  if (readVersionAt(extractDir) !== wantVersion) throw new Error('extracted version mismatch')
128
+ let extractedName = null
129
+ try { extractedName = JSON.parse(readFileSync(join(extractDir, 'package.json'), 'utf8')).name } catch { /* 忽略 */ }
130
+ if (extractedName !== 'dsh-api-dashboard') throw new Error('extracted package name mismatch')
128
131
  for (const rel of ['src/index.js', 'client/client.js', 'cordis.patch.yml']) {
129
132
  if (!existsSync(join(extractDir, rel))) throw new Error(`missing file after extract: ${rel}`)
130
133
  }
@@ -261,12 +264,37 @@ export const isWeekend = (timestamp = Date.now()) => {
261
264
 
262
265
 
263
266
  // ============================================================
264
- // 通用模型价格表 (CNY, 每百万token)
265
- // 用于 balanceCost 投影中非 DeepSeek 模型的计费估算.
266
- // 来源: 各平台官方定价 (2025-08), 仅做参考, 实际以平台为准.
267
+ // 通用模型价格表 — 每百万token, 币种按各平台官方数字直接填入(多为 USD, 本表不做 CNY 换算,
268
+ // 仅用于 balanceCost 投影中非 DeepSeek 模型的"估算", 不保证精确).
269
+ // ⚠️ DeepSeek 计费请走上面 V4_RATES 峰谷 CNY 表(官方价, 准确); 本通用表覆盖 OpenAI/Claude/Gemini/国产等。
270
+ // 来源: ① 现役主力(2026-08) 来自 NousResearch hermes-agent usage_pricing.py(追踪各官方文档, 逐条注明 source_url);
271
+ // ② 旧模型(2025-08) 为历史参考价. 仅做参考, 实际以平台为准.
267
272
  // ============================================================
268
273
  export const MODEL_PRICES = {
269
- // OpenAI
274
+ // —— 现役主力 (2026-08, USD/百万tokens, 来源: hermes-agent 追踪官方文档) ——
275
+ // OpenAI GPT-5.6 系列
276
+ 'gpt-5.6-sol': { cacheHit: 0.50, cacheMiss: 5.00, output: 30.00 },
277
+ 'gpt-5.6-terra': { cacheHit: 0.25, cacheMiss: 2.50, output: 15.00 },
278
+ 'gpt-5.6-luna': { cacheHit: 0.10, cacheMiss: 1.00, output: 6.00 },
279
+ // Anthropic Claude 4.x / 5
280
+ 'claude-opus-4-8': { cacheHit: 0.50, cacheMiss: 5.00, output: 25.00 },
281
+ 'claude-sonnet-5': { cacheHit: 0.20, cacheMiss: 2.00, output: 10.00 },
282
+ 'claude-sonnet-4-6': { cacheHit: 0.30, cacheMiss: 3.00, output: 15.00 },
283
+ 'claude-haiku-4-5': { cacheHit: 0.10, cacheMiss: 1.00, output: 5.00 },
284
+ // Google Gemini 3.x
285
+ 'gemini-3.6-flash': { cacheHit: 0.15, cacheMiss: 1.50, output: 7.50 },
286
+ 'gemini-3.5-flash': { cacheHit: 0.15, cacheMiss: 1.50, output: 9.00 },
287
+ 'gemini-3.1-pro': { cacheHit: 0.20, cacheMiss: 2.00, output: 12.00 },
288
+ 'gemini-2.5-pro': { cacheHit: 0.125, cacheMiss: 1.25, output: 10.00 },
289
+ // —— 国产主力 (2026-08) ——
290
+ // Kimi K3 (来源: benchlm.ai / morphllm.com 2026-08, 输入/输出 $3/$15; 缓存折扣未取到 → 暂按无折扣)
291
+ 'kimi-k3': { cacheHit: 3.00, cacheMiss: 3.00, output: 15.00 },
292
+ // 阶跃星辰 (来源: platform.stepfun.com 官方定价 2026; 列序=输入/缓存命中/输出, CNY1M)
293
+ 'step-3.7-flash': { cacheHit: 0.27, cacheMiss: 1.35, output: 8.10 },
294
+ 'step-3.5-flash': { cacheHit: 0.14, cacheMiss: 0.70, output: 2.10 },
295
+ // ⚠️ 通义 Qwen3 / 智谱 GLM-5 的现役官方一手价未取到可信数字, 不虚构; 遇到这些模型会落 defaultPrices(未定价),
296
+ // 旧 qwen-*/glm-4 条目仍在下方作历史参考.
297
+ // —— 以下为历史/参考模型 (2025-08, 实际以平台为准) ——
270
298
  'gpt-4o': { cacheHit: 1.25, cacheMiss: 2.5, output: 10 },
271
299
  'gpt-4o-mini': { cacheHit: 0.075, cacheMiss: 0.15, output: 0.6 },
272
300
  'gpt-4-turbo': { cacheHit: 5, cacheMiss: 10, output: 30 },
@@ -282,7 +310,9 @@ export const MODEL_PRICES = {
282
310
  'gemini-2.0-flash': { cacheHit: 0.05, cacheMiss: 0.1, output: 0.4 },
283
311
  'gemini-2.0-pro': { cacheHit: 1.25, cacheMiss: 2.5, output: 10 },
284
312
  'gemini-1.5-pro': { cacheHit: 1.75, cacheMiss: 3.5, output: 10.5 },
285
- // DeepSeek (标准价兜底)
313
+ // DeepSeek (标准价兜底) — ⚠️ 2026-07-24 起 deepseek-chat / deepseek-reasoner / deepseek-r1 已 RETIRED,
314
+ // 官方 API 调用会直接报错(不再重定向到 V4)。现役仅 deepseek-v4-flash / deepseek-v4-pro / deepseek-v4-flash-vision-exp。
315
+ // 保留这三条仅作为「若仍在用的旧配置」的估算占位, 真实计费请走上面 V4 峰谷表。
286
316
  'deepseek-chat': { cacheHit: 0.1, cacheMiss: 1, output: 2 },
287
317
  'deepseek-reasoner': { cacheHit: 0.2, cacheMiss: 2, output: 8 },
288
318
  'deepseek-r1': { cacheHit: 0.2, cacheMiss: 2, output: 8 },
@@ -462,7 +492,8 @@ function checkAlerts(balances, config, ctx) {
462
492
  // 余额解析适配器
463
493
  // ============================================================
464
494
 
465
- function parseResponse(queryType, json) {
495
+ // 纯函数, 导出便于单测 (不影响对外行为)
496
+ export function parseResponse(queryType, json) {
466
497
  switch (queryType) {
467
498
  case 'deepseek': {
468
499
  const infos = Array.isArray(json?.balance_infos) ? json.balance_infos : []
@@ -488,12 +519,17 @@ function parseResponse(queryType, json) {
488
519
  case 'siliconflow': {
489
520
  const d = json?.data
490
521
  if (!d) return null
491
- return { total: toAmount(d.totalBalance), currency: 'CNY', available: null, used: null, note: '硅基流动总余额' }
522
+ // 数据红线: totalBalance 字段名/单位未实测, 缺字段即放弃, 不造假 0。
523
+ if (d.totalBalance == null) return null
524
+ return { total: toAmount(d.totalBalance), currency: 'CNY', available: null, used: null, note: '硅基流动总余额(字段待实测)' }
492
525
  }
493
526
  case 'openrouter': {
494
527
  const d = json?.data
495
528
  if (!d) return null
496
- return { total: toAmount(d.total_credits) - toAmount(d.total_usage), currency: 'USD', available: toAmount(d.total_credits), used: toAmount(d.total_usage), note: 'OpenRouter 余额' }
529
+ // 数据红线: total_credits / total_usage 字段名未用真实 key 实测, 可能不叫这个名。
530
+ // 若两个预期字段都缺失 → 视为解析失败(返回 null, 前端标"无法解析/未开放"), 绝不显示假"余额0"。
531
+ if (d.total_credits == null && d.total_usage == null) return null
532
+ return { total: toAmount(d.total_credits) - toAmount(d.total_usage), currency: 'USD', available: toAmount(d.total_credits), used: toAmount(d.total_usage), note: 'OpenRouter 余额(字段待实测)' }
497
533
  }
498
534
  case 'novita': {
499
535
  if (!json || !('availableBalance' in json)) return null
@@ -501,14 +537,15 @@ function parseResponse(queryType, json) {
501
537
  return { total: raw / 10000, currency: 'USD', available: null, used: null, note: 'Novita (0.0001单位)' }
502
538
  }
503
539
  case 'stepfun': {
504
- if (!json || !('balance' in json)) return null
505
- const b = toAmount(json?.balance)
506
- return { total: b, currency: 'CNY', available: null, used: null, note: '阶梯星辰余额' }
540
+ if (!json || json.balance == null) return null
541
+ const b = toAmount(json.balance)
542
+ return { total: b, currency: 'CNY', available: null, used: null, note: '阶跃星辰余额' }
507
543
  }
508
544
  case 'quota': {
509
- const quota = toAmount(json?.data?.quota)
510
- if (quota === 0 && !json?.data?.username) return null
511
- return { total: quota / 500000, currency: 'USD', available: null, used: null, note: 'one-api quota (÷500000)' }
545
+ const q = json?.data
546
+ if (!q || (q.quota == null && q.username == null)) return null
547
+ const quota = toAmount(q.quota)
548
+ return { total: quota / 500000, currency: 'USD', available: null, used: null, note: 'one-api quota (÷500000, 系数待实测)' }
512
549
  }
513
550
  case 'openai-billing': {
514
551
  const limit = toAmount(json?.hard_limit_usd)
@@ -516,27 +553,45 @@ function parseResponse(queryType, json) {
516
553
  return { total: limit, currency: 'USD', available: limit, used: null, note: 'OpenAI 订阅硬上限' }
517
554
  }
518
555
  case 'glm': {
519
- // 智谱 monitor 返回 data.limits[], 同时存在「套餐%」与「限流配额(五小时×2000 token)」。
520
- // 判决口径(用户确认): 保留「百分比」显示——积分/套餐的 percentage 较准确。
521
- // 仅当完全无 percentage 时才回退到 remaining(带"剩余 token"标注, 不冒充真实余额),
522
- // 避免把「五小时限流窗口」误当账户余额误导用户。
556
+ // 智谱 Coding Plan 配额(实测 2026-08-30; unit 含义转自 z.ai 前端源码 / cc-switch)
557
+ // unit=3 → TOKENS_LIMIT 5小时滚动窗口(主显示) [Lite:2000]
558
+ // unit=6 TOKENS_LIMIT 周配额(部分套餐/国际站) [Lite:10000]
559
+ // unit=5 → TIME_LIMIT 工具/搜索类(月度)
560
+ // limit.remaining = 该维度当前剩余(积分);
561
+ // limit.percentage = 该维度【填充度/已用】(100=用完, 0=未用)——绝不是"剩余%";
562
+ // limit.nextResetTime = 该维度下次重置时间(仅滚动/周期维度有)。
563
+ // ⚠️ 严禁把 percentage 当"剩余%"显示: 会把"周配额已用完(remaining:0)"误显示成"余额 100%, 剩 100"。
564
+ // 真实返回: [{unit:3,remaining:2000,pct:0},{unit:6,remaining:0,pct:100,nextResetTime:...}]
523
565
  const limits = Array.isArray(json?.data?.limits) ? json.data.limits : []
524
566
  const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : null }
567
+ const rem = (l) => num(l?.remaining ?? l?.remaining_quota)
525
568
  const pctOf = (l) => { const n = Number(l?.percentage ?? l?.remaining_percentage); return Number.isFinite(n) && n >= 0 && n <= 100 ? n : null }
526
- const near = (a, b) => (Number(a?.nextResetTime) || 0) - (Number(b?.nextResetTime) || 0)
527
- // 1) 优先: 百分比套餐
528
- const pctCands = limits.map((l) => ({ l, p: pctOf(l) })).filter((x) => x.p !== null)
529
- let pk = pctCands.find((x) => x.l.type === 'TOKENS_LIMIT') || (pctCands.sort((a, b) => near(b.l, a.l))[0] ?? null)
530
- if (pk) {
531
- const p = pk.p
532
- return { total: p, currency: '%', available: p, used: 100 - p, note: '智谱剩余%', percent: true, resetAt: pk.l.nextResetTime ?? null }
569
+ const UNIT_LABEL = { 3: '5小时窗口', 6: '周配额', 5: '工具(月度)' }
570
+ // 主显示优先: unit=3(5小时滚动窗口), 其次 unit=6(周配额), 再按剩余降序
571
+ const score = (l) => (l.unit === 3 ? 0 : (l.unit === 6 ? 1 : 2))
572
+ const sorted = limits.slice().sort((a, b) => score(a) - score(b) || (rem(b) ?? -1) - (rem(a) ?? -1))
573
+ const main = sorted.find((l) => rem(l) !== null && rem(l) > 0)
574
+ const usedUp = limits.filter((l) => rem(l) === 0 && pctOf(l) === 100)
575
+ if (main) {
576
+ const r = rem(main)
577
+ const extra = usedUp.length
578
+ ? '; ' + usedUp.map((l) => `${UNIT_LABEL[l.unit] ?? '其他'}已用完(${l.nextResetTime ? '待重置' : '已限流'})`).join('; ')
579
+ : ''
580
+ return {
581
+ total: r, currency: 'tokens', available: r, used: null, percent: null,
582
+ note: `智谱${UNIT_LABEL[main.unit] ?? ''}剩 ${r} 积分${extra}`,
583
+ resetAt: usedUp[0]?.nextResetTime ?? main?.nextResetTime ?? null,
584
+ }
533
585
  }
534
- // 2) 兜底: 无任何 percentage 只剩配额/remaining 值; 明确标注是"剩余 token"而非金额余额
535
- const rem = (l) => num(l?.remaining ?? l?.remaining_quota)
536
- const balCands = limits.map((l) => ({ l, r: rem(l) })).filter((x) => x.r !== null && x.r > 0)
537
- if (balCands.length > 0) {
538
- const best = balCands.sort((a, b) => b.r - a.r)[0]
539
- return { total: best.r, currency: 'tokens', available: best.r, used: null, note: '智谱剩余 token(限流配额)', percent: null, resetAt: best.l.nextResetTime ?? null }
586
+ // 无可用>0全部用尽
587
+ if (usedUp.length) {
588
+ const l = usedUp[0]
589
+ return { total: 0, currency: 'tokens', available: 0, used: null, percent: null, note: '智谱配额已用完(0)' + (l.nextResetTime ? ', 待重置' : ''), resetAt: l?.nextResetTime ?? null }
590
+ }
591
+ // 兜底: 只有 percentage remaining 限流填充度(非余额)
592
+ if (limits.length > 0) {
593
+ const p = pctOf(limits[0])
594
+ if (p !== null) return { total: p, currency: '%', available: null, used: null, percent: true, note: '智谱限流填充度%(非余额)', resetAt: limits[0]?.nextResetTime ?? null }
540
595
  }
541
596
  return null
542
597
  }
@@ -544,6 +599,7 @@ function parseResponse(queryType, json) {
544
599
  case 'kimi': {
545
600
  const u = json?.usage
546
601
  if (!u) return null
602
+ if (u.limit == null && u.remaining == null) return null
547
603
  const limit = toAmount(u.limit), remaining = toAmount(u.remaining)
548
604
  return { total: limit, currency: 'tokens', available: remaining, used: limit - remaining, note: 'Kimi 套餐剩余 tokens', percent: limit > 0 ? (remaining / limit) * 100 : null }
549
605
  }
@@ -1046,8 +1102,10 @@ export function apply(ctx, config) {
1046
1102
  const mk = (p) => p ? { cacheHit: p.cacheHit, cacheMiss: p.cacheMiss, output: p.output } : null
1047
1103
  // v4 峰谷系列
1048
1104
  const models = []
1049
- for (const key of ['deepseek-v4-flash', 'deepseek-v4-pro']) {
1050
- models.push({ model: key, peak: mk(table.peak[key]), offPeak: mk(table.offPeak[key]), peakValley: true })
1105
+ for (const key of ['deepseek-v4-flash', 'deepseek-v4-pro', 'deepseek-v4-flash-vision-exp']) {
1106
+ // v4-flash-vision-exp flash 同价 (vision 不加价), 复用 flash 费率
1107
+ const src = key === 'deepseek-v4-flash-vision-exp' ? 'deepseek-v4-flash' : key
1108
+ models.push({ model: key, peak: mk(table.peak[src]), offPeak: mk(table.offPeak[src]), peakValley: true })
1051
1109
  }
1052
1110
  sendJson(res, 200, { ok: true, currency: cfg.currency ?? 'CNY', peakNow: isPeakTime(), weekend: isWeekend(), models })
1053
1111
  },