dsh-desktop-statusbar 2.0.0 → 2.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.
package/lib/index.js CHANGED
@@ -22,6 +22,7 @@
22
22
  * @module dsh-desktop-statusbar
23
23
  */
24
24
  import { z } from 'zod'
25
+ import { adapterFor } from './balance-api.js'
25
26
  import { holidayHtmlToText, parseHolidayNotice } from './holiday.js'
26
27
  import { readFileSync } from 'node:fs'
27
28
  import { join } from 'node:path'
@@ -340,6 +341,12 @@ const timingViewSchema = z.object({
340
341
  slowestTtft: timingMark.nullable(),
341
342
  fastestTps: timingMark.nullable(),
342
343
  slowestTps: timingMark.nullable(),
344
+ /* 本轮累计:气泡里"本轮平均首字 / 本轮平均速度"要用(官方投影只有会话累计值) */
345
+ turn: z.number().nullable(),
346
+ turnTtftMs: z.number().nonnegative(),
347
+ turnTtftSamples: z.number().int().nonnegative(),
348
+ turnDecodeMs: z.number().nonnegative(),
349
+ turnDecodeTokens: z.number().nonnegative(),
343
350
  }).strict()
344
351
 
345
352
  const sessionTimingSchema = timingViewSchema.extend({ openStep: timingStep.nullable() }).strict()
@@ -417,12 +424,25 @@ const sessionTimingProjection = {
417
424
  slowestTtft: null,
418
425
  fastestTps: null,
419
426
  slowestTps: null,
427
+ turn: null,
428
+ turnTtftMs: 0,
429
+ turnTtftSamples: 0,
430
+ turnDecodeMs: 0,
431
+ turnDecodeTokens: 0,
420
432
  openStep: null,
421
433
  }),
422
434
  apply: (state, event) => {
423
435
  if (event.type === 'step/start') {
436
+ /* 换轮就把本轮累计清零:气泡里的"本轮平均…"只算当前这一轮 */
437
+ const turn = event.data.turn
438
+ const sameTurn = state.turn === turn
424
439
  return {
425
440
  ...state,
441
+ turn: turn,
442
+ turnTtftMs: sameTurn ? state.turnTtftMs : 0,
443
+ turnTtftSamples: sameTurn ? state.turnTtftSamples : 0,
444
+ turnDecodeMs: sameTurn ? state.turnDecodeMs : 0,
445
+ turnDecodeTokens: sameTurn ? state.turnDecodeTokens : 0,
426
446
  openStep: {
427
447
  turn: event.data.turn, step: event.data.step, startTime: event.time,
428
448
  firstTokenTime: null, firstTokenTool: false,
@@ -451,6 +471,8 @@ const sessionTimingProjection = {
451
471
  next.samples = state.samples + 1
452
472
  next.fastestTtft = pickSmaller(state.fastestTtft, { value: ttft, at: event.time, model })
453
473
  next.slowestTtft = pickLarger(state.slowestTtft, { value: ttft, at: event.time, model })
474
+ next.turnTtftMs = state.turnTtftMs + ttft
475
+ next.turnTtftSamples = state.turnTtftSamples + 1
454
476
  const output = stepOutputTokens(event.data.usage)
455
477
  const decode = Math.max(0, event.time - first.time)
456
478
  /* 纯工具调用的步骤不进速度极值:分片时间戳挤在同一刻,算出来是假值(首字照旧统计) */
@@ -458,6 +480,8 @@ const sessionTimingProjection = {
458
480
  const speed = { value: output / (decode / 1000), at: event.time, model }
459
481
  next.fastestTps = pickLarger(state.fastestTps, speed)
460
482
  next.slowestTps = pickSmaller(state.slowestTps, speed)
483
+ next.turnDecodeMs = state.turnDecodeMs + decode
484
+ next.turnDecodeTokens = state.turnDecodeTokens + output
461
485
  }
462
486
  return next
463
487
  }
@@ -475,9 +499,14 @@ const sessionTimingProjection = {
475
499
  slowestTtft: state.slowestTtft,
476
500
  fastestTps: state.fastestTps,
477
501
  slowestTps: state.slowestTps,
502
+ turn: state.turn,
503
+ turnTtftMs: state.turnTtftMs,
504
+ turnTtftSamples: state.turnTtftSamples,
505
+ turnDecodeMs: state.turnDecodeMs,
506
+ turnDecodeTokens: state.turnDecodeTokens,
478
507
  }),
479
508
  },
480
- stateVersion: 2,
509
+ stateVersion: 3,
481
510
  }
482
511
 
483
512
  /* ---------------------------------------------------------------- 余额查询 */
@@ -612,6 +641,82 @@ function keyFromCredentialStore(ctx) {
612
641
  return null
613
642
  }
614
643
 
644
+ /**
645
+ * 按适配器的凭据引用名逐个解析 key。用 credentials 服务的 resolve()(子代理核实:
646
+ * describe() 只回答"配了没",resolve() 才给出真实值 { value, source })。
647
+ * ref 在运行时就是普通字符串 —— credentialRef() 只做编译期品牌标记,所以不必 import 它。
648
+ */
649
+ async function resolveAdapterKey(ctx, adapter) {
650
+ const credentials = ctx !== null && ctx !== undefined && typeof ctx.get === 'function' ? ctx.get('credentials') : null
651
+ if (credentials !== null && credentials !== undefined && typeof credentials.resolve === 'function') {
652
+ for (let i = 0; i < adapter.refs.length; i += 1) {
653
+ try {
654
+ const hit = await credentials.resolve(adapter.refs[i])
655
+ if (hit !== undefined && hit !== null && typeof hit.value === 'string' && hit.value.length > 0) {
656
+ return { key: hit.value, source: adapter.refs[i] }
657
+ }
658
+ } catch (error) {
659
+ /* 换下一个引用名 */
660
+ }
661
+ }
662
+ }
663
+ /* 没挂凭据服务时的旧路径:环境变量 / .credentials.yaml */
664
+ const fallback = findApiKey(ctx)
665
+ return { key: fallback.key, source: fallback.source }
666
+ }
667
+
668
+ /** 用适配器查第三方平台的余额,返回与 DeepSeek 那条路一致的形状。 */
669
+ /** 从适配器返回的 parts 里按 label 取金额;没有这个 label 就返回 undefined(气泡少一行)。 */
670
+ function partAmount(parts, label) {
671
+ const list = Array.isArray(parts) ? parts : []
672
+ for (let i = 0; i < list.length; i += 1) {
673
+ if (list[i] !== null && list[i] !== undefined && list[i].label === label) return list[i].amount
674
+ }
675
+ return undefined
676
+ }
677
+
678
+ async function queryWithAdapter(ctx, adapter, now) {
679
+ const found = await resolveAdapterKey(ctx, adapter)
680
+ if (found.key === null) {
681
+ logHost(ctx, 'balance(' + adapter.id + '): 没取到 key,试过 ' + adapter.refs.join(' / '))
682
+ return { ok: false, at: now, reason: 'no-credential', provider: adapter.id }
683
+ }
684
+ const spec = adapter.request(found.key)
685
+ try {
686
+ const response = await fetch(spec.url, {
687
+ method: 'GET',
688
+ headers: spec.headers,
689
+ signal: AbortSignal.timeout(10000),
690
+ })
691
+ if (!response.ok) {
692
+ logHost(ctx, 'balance(' + adapter.id + '): HTTP ' + String(response.status))
693
+ return { ok: false, at: now, reason: 'http-' + String(response.status), source: found.source, provider: adapter.id }
694
+ }
695
+ const parsed = adapter.parse(await response.json())
696
+ if (parsed === null) {
697
+ logHost(ctx, 'balance(' + adapter.id + '): 响应里解析不出余额')
698
+ return { ok: false, at: now, reason: 'no-balance-info', source: found.source, provider: adapter.id }
699
+ }
700
+ return {
701
+ ok: true,
702
+ at: now,
703
+ source: found.source,
704
+ provider: adapter.id,
705
+ currency: parsed.currency,
706
+ total: parsed.total,
707
+ isPercent: parsed.isPercent === true,
708
+ parts: parsed.parts,
709
+ /* 余额气泡那几行读的是顶层字段(与 DeepSeek 那条路保持同一形状),这里按 label 摊平 */
710
+ toppedUp: partAmount(parsed.parts, 'toppedUp'),
711
+ granted: partAmount(parsed.parts, 'granted'),
712
+ spent: partAmount(parsed.parts, 'spent'),
713
+ }
714
+ } catch (error) {
715
+ logHost(ctx, 'balance(' + adapter.id + '): ' + String(error === null || error === undefined ? 'unknown' : error.message))
716
+ return { ok: false, at: now, reason: 'network', source: found.source, provider: adapter.id }
717
+ }
718
+ }
719
+
615
720
  function findApiKey(ctx) {
616
721
  const fromEnv = keyFromEnv()
617
722
  if (fromEnv !== null) return { key: fromEnv, source: 'env' }
@@ -702,12 +807,21 @@ async function queryAccountBalance(ctx) {
702
807
  }
703
808
  }
704
809
 
705
- async function queryBalance(ctx, force) {
810
+ async function queryBalance(ctx, force, provider, model) {
706
811
  const now = Date.now()
707
812
  if (force !== true && balanceCache.value !== null && now - balanceCache.at < BALANCE_TTL_MS) {
708
813
  return balanceCache.value
709
814
  }
710
815
 
816
+ /* 当前会话用的是第三方平台就交给适配器:每个平台有自己的端点、鉴权与字段。
817
+ DeepSeek(含识别不出来的情况)继续走下面的账号服务 + API key 老路。 */
818
+ const adapter = adapterFor(provider, model)
819
+ if (adapter !== null && adapter.id !== 'deepseek') {
820
+ const third = await queryWithAdapter(ctx, adapter, now)
821
+ balanceCache = { at: now, value: third, provider: adapter.id }
822
+ return third
823
+ }
824
+
711
825
  /* 先问官方账号服务:与设置页同一份数据(充值 + 赠金),拿不到才走下面的 API key 查询 */
712
826
  const fromAccount = await queryAccountBalance(ctx)
713
827
  if (fromAccount !== null) {
@@ -908,7 +1022,7 @@ export function apply(ctx) {
908
1022
  return
909
1023
  }
910
1024
  const force = url.searchParams.get('force') === '1'
911
- const value = await queryBalance(ctx, force)
1025
+ const value = await queryBalance(ctx, force, url.searchParams.get('provider'), url.searchParams.get('model'))
912
1026
  sendJson(res, 200, Object.assign({}, value, { probe: accountProbeNote }))
913
1027
  },
914
1028
  }), 'dsh-desktop-statusbar: balance api')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-desktop-statusbar",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "type": "module",
5
5
  "description": "把 DSH 桌面端对话区底部的统计行换成可配置状态栏:9 个字段可选可排序,按官方峰谷口径估算费用,支持自定义模型单价与账户余额。",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",