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/README.en.md +4 -4
- package/README.md +4 -4
- package/docs/settings.png +0 -0
- package/lib/balance-api.js +224 -0
- package/lib/client.js +481 -146
- package/lib/index.js +117 -3
- package/package.json +1 -1
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:
|
|
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')
|