@linxin666/dsh-usage 0.3.24 → 0.4.1
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.i18n.yaml +2 -2
- package/README.md +4 -4
- package/README.zh.md +4 -4
- package/icon.svg +3 -0
- package/lib/client.js +540 -655
- package/lib/client.js.map +1 -1
- package/lib/index.js +92 -75
- package/lib/types/client/UsageFootCard.d.ts +55 -0
- package/lib/types/client/UsageSectionCard.d.ts +3 -3
- package/lib/types/client/foot-card-mount.d.ts +34 -0
- package/lib/types/client/index.d.ts +21 -8
- package/lib/types/client/locales.d.ts +6 -9
- package/lib/types/host/http.d.ts +18 -1
- package/lib/types/host/usage-service.d.ts +4 -3
- package/lib/types/index.d.ts +14 -0
- package/package.json +19 -17
- package/src/client/UsageFootCard.tsx +300 -0
- package/src/client/UsageSectionCard.tsx +41 -9
- package/src/client/foot-card-mount.tsx +112 -0
- package/src/client/index.ts +53 -33
- package/src/client/locales.ts +12 -18
- package/src/client/usage.module.css +101 -165
- package/src/host/http.ts +23 -1
- package/src/host/usage-service.ts +9 -4
- package/src/index.ts +59 -55
- package/lib/types/client/UsageSidebarPanel.d.ts +0 -34
- package/lib/types/client/sidebar-entry-core.d.ts +0 -87
- package/lib/types/client/sidebar-entry.d.ts +0 -35
- package/lib/types/client/sidebar-panel-mount.d.ts +0 -40
- package/src/client/UsageSidebarPanel.tsx +0 -196
- package/src/client/sidebar-entry-core.ts +0 -321
- package/src/client/sidebar-entry.ts +0 -101
- package/src/client/sidebar-panel-mount.tsx +0 -113
package/src/client/locales.ts
CHANGED
|
@@ -48,6 +48,7 @@ export const zh = {
|
|
|
48
48
|
'usage.config.title': '设置',
|
|
49
49
|
'usage.config.enabled': '启用插件',
|
|
50
50
|
'usage.config.pollIntervalSec': '轮询间隔(秒)',
|
|
51
|
+
'usage.config.saveFailed': '保存失败:设置未被接受。',
|
|
51
52
|
'usage.bank.title': '鲸元券',
|
|
52
53
|
'usage.bank.hint': '官方 API 每消耗 100 万 tokens 铸造 1 鲸元;保存或分享这张票券。',
|
|
53
54
|
'usage.bank.noUsage': '暂无 DeepSeek 官方用量数据(统计自插件启用起)',
|
|
@@ -58,15 +59,11 @@ export const zh = {
|
|
|
58
59
|
'usage.bank.save': '保存图片',
|
|
59
60
|
'usage.bank.share': '分享',
|
|
60
61
|
'usage.bank.drawError': '票券生成失败:{error}',
|
|
61
|
-
'usage.
|
|
62
|
-
'usage.
|
|
63
|
-
'usage.
|
|
64
|
-
'usage.
|
|
65
|
-
'usage.
|
|
66
|
-
'usage.sidebar.error': '加载失败:{error}',
|
|
67
|
-
'usage.sidebar.loading': '正在加载用量数据…',
|
|
68
|
-
'usage.sidebar.balanceLeft': '剩余 {balance}',
|
|
69
|
-
'usage.sidebar.todayUsage': '今日 {tokens} tokens',
|
|
62
|
+
'usage.foot.cost': '今日消费',
|
|
63
|
+
'usage.foot.noData': '今日暂无用量',
|
|
64
|
+
'usage.foot.open': '打开使用统计设置',
|
|
65
|
+
'usage.foot.collapse': '收起用量卡片',
|
|
66
|
+
'usage.foot.expand': '展开用量卡片',
|
|
70
67
|
}
|
|
71
68
|
|
|
72
69
|
/** English mirror; every zh key present. */
|
|
@@ -110,6 +107,7 @@ export const en: Record<UsageKey, string> = {
|
|
|
110
107
|
'usage.config.title': 'Settings',
|
|
111
108
|
'usage.config.enabled': 'Enable plugin',
|
|
112
109
|
'usage.config.pollIntervalSec': 'Poll interval (seconds)',
|
|
110
|
+
'usage.config.saveFailed': 'Save failed: the deployment did not accept the setting.',
|
|
113
111
|
'usage.bank.title': 'Whale-yuan voucher',
|
|
114
112
|
'usage.bank.hint': 'Every 1,000,000 tokens spent on the official API mint one whale yuan; save or share the note.',
|
|
115
113
|
'usage.bank.noUsage': 'No official DeepSeek usage yet (counting starts when the plugin is enabled)',
|
|
@@ -120,15 +118,11 @@ export const en: Record<UsageKey, string> = {
|
|
|
120
118
|
'usage.bank.save': 'Save image',
|
|
121
119
|
'usage.bank.share': 'Share',
|
|
122
120
|
'usage.bank.drawError': 'Failed to render the voucher: {error}',
|
|
123
|
-
'usage.
|
|
124
|
-
'usage.
|
|
125
|
-
'usage.
|
|
126
|
-
'usage.
|
|
127
|
-
'usage.
|
|
128
|
-
'usage.sidebar.error': 'Loading failed: {error}',
|
|
129
|
-
'usage.sidebar.loading': 'Loading usage data…',
|
|
130
|
-
'usage.sidebar.balanceLeft': '{balance} left',
|
|
131
|
-
'usage.sidebar.todayUsage': '{tokens} tokens today',
|
|
121
|
+
'usage.foot.cost': 'Today spend',
|
|
122
|
+
'usage.foot.noData': 'No usage yet today',
|
|
123
|
+
'usage.foot.open': 'Open usage statistics settings',
|
|
124
|
+
'usage.foot.collapse': 'Collapse the usage card',
|
|
125
|
+
'usage.foot.expand': 'Expand the usage card',
|
|
132
126
|
}
|
|
133
127
|
|
|
134
128
|
export type UsageKey = keyof typeof zh
|
|
@@ -384,82 +384,71 @@
|
|
|
384
384
|
|
|
385
385
|
|
|
386
386
|
/* ---------------------------------------------------------------------------
|
|
387
|
-
Sidebar
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
387
|
+
Sidebar foot card: the compact usage glance the mount seats as the shell
|
|
388
|
+
foot area's last child, directly below the Settings row. The chrome is an
|
|
389
|
+
outer div; the content is one transparent main button (the whole card opens
|
|
390
|
+
the usage settings section) with a sibling corner chevron toggling the
|
|
391
|
+
collapsed strip — sibling buttons, since nesting buttons is invalid HTML.
|
|
392
|
+
Quiet card on the same currentColor mixes as the section cards; hidden in
|
|
393
|
+
the 56px rail (the frame carries data-sidebar-collapsed).
|
|
391
394
|
--------------------------------------------------------------------------- */
|
|
392
395
|
|
|
393
|
-
|
|
396
|
+
/* Seated above the Settings row as a native sidebar row: the shell's row
|
|
397
|
+
radius and hover fill, with none of the card chrome that made it read as a
|
|
398
|
+
pill pasted onto the foot. Only the expanded body keeps a faint surface to
|
|
399
|
+
hold its four lines together; the collapsed strip is a plain 36px row. */
|
|
400
|
+
.footCard {
|
|
401
|
+
position: relative;
|
|
394
402
|
box-sizing: border-box;
|
|
395
|
-
display: flex;
|
|
396
|
-
align-items: center;
|
|
397
|
-
gap: 8px;
|
|
398
403
|
width: 100%;
|
|
399
|
-
|
|
400
|
-
padding: 0 10px;
|
|
401
|
-
background: transparent;
|
|
404
|
+
margin: 2px 0 4px;
|
|
402
405
|
border: none;
|
|
403
|
-
border-radius:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
white-space: nowrap;
|
|
406
|
+
border-radius: 12px;
|
|
407
|
+
background: color-mix(in srgb, currentColor 4%, transparent);
|
|
408
|
+
color: inherit;
|
|
409
|
+
transition: background-color 120ms ease;
|
|
408
410
|
}
|
|
409
411
|
|
|
410
|
-
.
|
|
412
|
+
.footCard:hover {
|
|
411
413
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
412
|
-
color: var(--dsw-alias-label-primary);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
.entryIcon {
|
|
416
|
-
display: inline-flex;
|
|
417
|
-
align-items: center;
|
|
418
|
-
justify-content: center;
|
|
419
|
-
width: 24px;
|
|
420
|
-
height: 24px;
|
|
421
|
-
flex: none;
|
|
422
414
|
}
|
|
423
415
|
|
|
424
|
-
.
|
|
425
|
-
|
|
426
|
-
width: 18px;
|
|
427
|
-
height: 18px;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
.entryLabel {
|
|
431
|
-
overflow: hidden;
|
|
432
|
-
text-overflow: ellipsis;
|
|
416
|
+
.footCardCollapsed {
|
|
417
|
+
background: transparent;
|
|
433
418
|
}
|
|
434
419
|
|
|
435
|
-
|
|
436
|
-
keeps the old row's hit area and the action buttons seat at the right
|
|
437
|
-
edge. */
|
|
438
|
-
.entryMain {
|
|
420
|
+
.footMain {
|
|
439
421
|
appearance: none;
|
|
422
|
+
box-sizing: border-box;
|
|
440
423
|
display: flex;
|
|
441
|
-
|
|
442
|
-
gap:
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
height: 100%;
|
|
446
|
-
padding: 0;
|
|
447
|
-
background: transparent;
|
|
424
|
+
flex-direction: column;
|
|
425
|
+
gap: 4px;
|
|
426
|
+
width: 100%;
|
|
427
|
+
padding: 8px;
|
|
448
428
|
border: none;
|
|
429
|
+
border-radius: 12px;
|
|
430
|
+
background: transparent;
|
|
449
431
|
color: inherit;
|
|
450
432
|
font: inherit;
|
|
451
433
|
text-align: left;
|
|
452
434
|
cursor: pointer;
|
|
453
435
|
}
|
|
454
436
|
|
|
455
|
-
.
|
|
437
|
+
.footMain:focus-visible {
|
|
438
|
+
outline: none;
|
|
439
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.footToggle {
|
|
456
443
|
appearance: none;
|
|
444
|
+
position: absolute;
|
|
445
|
+
top: 6px;
|
|
446
|
+
right: 6px;
|
|
457
447
|
display: inline-flex;
|
|
458
448
|
align-items: center;
|
|
459
449
|
justify-content: center;
|
|
460
|
-
width:
|
|
461
|
-
height:
|
|
462
|
-
flex: none;
|
|
450
|
+
width: 20px;
|
|
451
|
+
height: 20px;
|
|
463
452
|
padding: 0;
|
|
464
453
|
border: none;
|
|
465
454
|
border-radius: 6px;
|
|
@@ -470,166 +459,113 @@
|
|
|
470
459
|
transition: opacity 120ms ease, background-color 120ms ease;
|
|
471
460
|
}
|
|
472
461
|
|
|
473
|
-
.
|
|
462
|
+
.footCard:hover .footToggle {
|
|
474
463
|
opacity: 0.8;
|
|
475
464
|
}
|
|
476
465
|
|
|
477
|
-
.
|
|
466
|
+
.footToggle:hover {
|
|
478
467
|
opacity: 1;
|
|
479
468
|
background: color-mix(in srgb, currentColor 10%, transparent);
|
|
480
469
|
}
|
|
481
470
|
|
|
482
|
-
.
|
|
483
|
-
.entryMain:focus-visible {
|
|
471
|
+
.footToggle:focus-visible {
|
|
484
472
|
outline: none;
|
|
485
473
|
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);
|
|
486
474
|
}
|
|
487
475
|
|
|
488
|
-
.
|
|
489
|
-
cursor: default;
|
|
490
|
-
opacity: 0.4;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
.entryAction svg {
|
|
476
|
+
.footToggle svg {
|
|
494
477
|
display: block;
|
|
495
|
-
width: 14px;
|
|
496
|
-
height: 14px;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/* Refresh cooldown: the spinning glyph marks the in-flight probe cycle. */
|
|
500
|
-
.entryAction:disabled svg {
|
|
501
|
-
animation: usageEntrySpin 900ms linear infinite;
|
|
502
478
|
}
|
|
503
479
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
480
|
+
.footHead {
|
|
481
|
+
display: flex;
|
|
482
|
+
align-items: baseline;
|
|
483
|
+
justify-content: space-between;
|
|
484
|
+
gap: 8px;
|
|
508
485
|
}
|
|
509
486
|
|
|
510
|
-
|
|
511
|
-
:
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
487
|
+
.footTitle {
|
|
488
|
+
display: inline-flex;
|
|
489
|
+
align-items: center;
|
|
490
|
+
gap: 6px;
|
|
491
|
+
min-width: 0;
|
|
492
|
+
font-size: 12px;
|
|
493
|
+
opacity: 0.65;
|
|
494
|
+
white-space: nowrap;
|
|
518
495
|
}
|
|
519
496
|
|
|
520
|
-
|
|
521
|
-
:
|
|
497
|
+
.footTitle svg {
|
|
498
|
+
display: block;
|
|
522
499
|
flex: none;
|
|
523
|
-
width: 100%;
|
|
524
|
-
justify-content: center;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
:global([data-dsh-frame][data-sidebar-collapsed]) .entryAction,
|
|
528
|
-
:global([data-sidebar-collapsed]) .entryAction {
|
|
529
|
-
display: none;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
:global([data-dsh-frame][data-sidebar-collapsed]) .sidebarSummary,
|
|
533
|
-
:global([data-sidebar-collapsed]) .sidebarSummary {
|
|
534
|
-
display: none;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
:global([data-dsh-frame][data-sidebar-collapsed]) .entryLabel,
|
|
538
|
-
:global([data-sidebar-collapsed]) .entryLabel {
|
|
539
|
-
display: none;
|
|
540
500
|
}
|
|
541
501
|
|
|
542
|
-
/*
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
margin: 0 4px 12px;
|
|
549
|
-
padding: 0 10px;
|
|
550
|
-
font-size: 11px;
|
|
551
|
-
color: inherit;
|
|
552
|
-
opacity: 0.75;
|
|
502
|
+
/* The headline value leaves room for the corner toggle. */
|
|
503
|
+
.footValue {
|
|
504
|
+
margin-right: 18px;
|
|
505
|
+
font-size: 15px;
|
|
506
|
+
font-weight: 600;
|
|
507
|
+
font-variant-numeric: tabular-nums;
|
|
553
508
|
white-space: nowrap;
|
|
554
|
-
overflow: hidden;
|
|
555
509
|
}
|
|
556
510
|
|
|
557
|
-
.
|
|
558
|
-
|
|
559
|
-
|
|
511
|
+
.footLine {
|
|
512
|
+
font-size: 11px;
|
|
513
|
+
opacity: 0.7;
|
|
514
|
+
font-variant-numeric: tabular-nums;
|
|
560
515
|
overflow: hidden;
|
|
561
516
|
text-overflow: ellipsis;
|
|
562
|
-
|
|
517
|
+
white-space: nowrap;
|
|
563
518
|
}
|
|
564
519
|
|
|
565
|
-
.
|
|
566
|
-
|
|
567
|
-
|
|
520
|
+
.footMeta {
|
|
521
|
+
font-size: 10px;
|
|
522
|
+
opacity: 0.5;
|
|
568
523
|
font-variant-numeric: tabular-nums;
|
|
569
524
|
}
|
|
570
525
|
|
|
571
|
-
/*
|
|
572
|
-
.
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
gap: 8px;
|
|
576
|
-
margin: 0 4px 12px;
|
|
577
|
-
padding: 10px 10px 12px;
|
|
578
|
-
border: 1px solid color-mix(in srgb, currentColor 14%, transparent);
|
|
579
|
-
border-radius: 10px;
|
|
580
|
-
color: inherit;
|
|
581
|
-
background: color-mix(in srgb, currentColor 4%, transparent);
|
|
526
|
+
/* Collapsed strip: tighter chrome, one horizontal line, room for the toggle. */
|
|
527
|
+
.footCardCollapsed .footMain {
|
|
528
|
+
min-height: 36px;
|
|
529
|
+
padding: 7px 8px;
|
|
582
530
|
}
|
|
583
531
|
|
|
584
|
-
.
|
|
532
|
+
.footStrip {
|
|
585
533
|
display: flex;
|
|
586
|
-
|
|
587
|
-
gap:
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
.sidebarProviderHead {
|
|
591
|
-
display: flex;
|
|
592
|
-
align-items: baseline;
|
|
593
|
-
justify-content: space-between;
|
|
594
|
-
gap: 8px;
|
|
534
|
+
align-items: center;
|
|
535
|
+
gap: 6px;
|
|
536
|
+
padding-right: 20px;
|
|
595
537
|
font-size: 12px;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
.sidebarProviderName {
|
|
599
|
-
overflow: hidden;
|
|
600
|
-
text-overflow: ellipsis;
|
|
601
538
|
white-space: nowrap;
|
|
539
|
+
overflow: hidden;
|
|
602
540
|
}
|
|
603
541
|
|
|
604
|
-
.
|
|
542
|
+
.footStrip svg {
|
|
543
|
+
display: block;
|
|
605
544
|
flex: none;
|
|
606
|
-
opacity: 0.
|
|
545
|
+
opacity: 0.65;
|
|
607
546
|
}
|
|
608
547
|
|
|
609
|
-
.
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
548
|
+
.footStripProvider {
|
|
549
|
+
flex: none;
|
|
550
|
+
max-width: 45%;
|
|
551
|
+
overflow: hidden;
|
|
552
|
+
text-overflow: ellipsis;
|
|
553
|
+
font-weight: 500;
|
|
615
554
|
}
|
|
616
555
|
|
|
617
|
-
.
|
|
618
|
-
|
|
619
|
-
height: 4px;
|
|
620
|
-
border-radius: 2px;
|
|
621
|
-
background: color-mix(in srgb, currentColor 14%, transparent);
|
|
556
|
+
.footStripLabel {
|
|
557
|
+
opacity: 0.65;
|
|
622
558
|
overflow: hidden;
|
|
559
|
+
text-overflow: ellipsis;
|
|
623
560
|
}
|
|
624
561
|
|
|
625
|
-
.
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
background: currentColor;
|
|
562
|
+
.footStripValue {
|
|
563
|
+
margin-left: auto;
|
|
564
|
+
font-weight: 600;
|
|
565
|
+
font-variant-numeric: tabular-nums;
|
|
630
566
|
}
|
|
631
567
|
|
|
632
|
-
.
|
|
633
|
-
|
|
634
|
-
|
|
568
|
+
:global([data-dsh-frame][data-sidebar-collapsed]) .footCard,
|
|
569
|
+
:global([data-sidebar-collapsed]) .footCard {
|
|
570
|
+
display: none;
|
|
635
571
|
}
|
package/src/host/http.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Shared JSON body/response helpers for the host route families: one strict
|
|
4
4
|
* bounded body reader, one lenient bounded body reader, one JSON object
|
|
5
|
-
* narrow,
|
|
5
|
+
* narrow, one JSON writer, and the outbound request-init helper that keeps a
|
|
6
|
+
* host fetch body decodable (withIdentityEncoding). Previously the readers
|
|
7
|
+
* were copy-pasted across the
|
|
6
8
|
* package route files (routes.ts, update-routes.ts, mobile-api.ts, and each
|
|
7
9
|
* family's route module) with drifting contracts: body caps ranging 4 KiB to
|
|
8
10
|
* 1 MiB and four distinct overflow behaviors (reject, undefined, null, throw).
|
|
@@ -88,6 +90,26 @@ export function asJsonObject(value: unknown): Record<string, unknown> | undefine
|
|
|
88
90
|
return isJsonObject(value) ? value : undefined
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Request init that asks a remote origin for an uncompressed body.
|
|
95
|
+
*
|
|
96
|
+
* The DSH host boots `@deepseek-ai/dsh-http-proxy`, whose top-level import of
|
|
97
|
+
* the npm `undici` copy replaces the legacy `undici.globalDispatcher.1` slot
|
|
98
|
+
* Node's built-in `fetch()` reads. That cross-major wrapper drops the
|
|
99
|
+
* `content-encoding` header and automatic decompression, so a plain fetch
|
|
100
|
+
* resolves to raw gzip/brotli/zstd bytes — compressed noise no JSON or text
|
|
101
|
+
* consumer can read. Every host fetch that parses a remote body must request
|
|
102
|
+
* identity encoding. Caller headers survive; `accept-encoding` is forced to
|
|
103
|
+
* `identity` because a caller value has no decoder behind it in this host.
|
|
104
|
+
* @param init - the caller's request init (signal, headers, method, ...).
|
|
105
|
+
* @returns a copy with `accept-encoding: identity` merged in.
|
|
106
|
+
*/
|
|
107
|
+
export function withIdentityEncoding(init: RequestInit = {}): RequestInit {
|
|
108
|
+
const headers = new Headers(init.headers)
|
|
109
|
+
headers.set('accept-encoding', 'identity')
|
|
110
|
+
return { ...init, headers }
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
/**
|
|
92
114
|
* Write one JSON response. Default headers are the family defaults
|
|
93
115
|
* (content-type and referrer-policy); caller headers are appended or
|
|
@@ -14,6 +14,7 @@ import { credentialKey, credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
|
14
14
|
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
|
15
15
|
import type { TokenUsage } from '@deepseek-ai/dsh-llm'
|
|
16
16
|
import { dshHome } from '../dsh-home.ts'
|
|
17
|
+
import { withIdentityEncoding } from './http.ts'
|
|
17
18
|
import { adapterFor, isDeepSeekProviderRoute, providerErrorMessage } from '../core/adapters.ts'
|
|
18
19
|
import type { BalanceParse, PlanParse } from '../core/adapters.ts'
|
|
19
20
|
import { foldAliasRoutes } from '../core/provider-routes.ts'
|
|
@@ -164,8 +165,9 @@ export class UsageService {
|
|
|
164
165
|
|
|
165
166
|
/**
|
|
166
167
|
* Stop timers and flush pending ledger writes. The returned promise
|
|
167
|
-
* resolves after the final flush lands, so a successor instance (
|
|
168
|
-
*
|
|
168
|
+
* resolves after the final flush lands, so a successor instance (the Host
|
|
169
|
+
* reloads the profile row on a config change) can serialize its first load
|
|
170
|
+
* behind it.
|
|
169
171
|
*/
|
|
170
172
|
stop(): Promise<void> {
|
|
171
173
|
this.disposed = true
|
|
@@ -175,7 +177,7 @@ export class UsageService {
|
|
|
175
177
|
return this.flushLedger()
|
|
176
178
|
}
|
|
177
179
|
|
|
178
|
-
/** Re-apply options
|
|
180
|
+
/** Re-apply options to this running instance; retention shrink prunes now. */
|
|
179
181
|
applyOptions(options: UsageServiceOptions): void {
|
|
180
182
|
this.options = options
|
|
181
183
|
this.pruneIfNeeded()
|
|
@@ -596,7 +598,10 @@ export class UsageService {
|
|
|
596
598
|
if (half === undefined) return undefined
|
|
597
599
|
try {
|
|
598
600
|
const spec = half.build({ apiKey: credential.key as string, ...(credential.accountId !== undefined ? { accountId: credential.accountId } : {}) })
|
|
599
|
-
const response = await fetch(spec.url, {
|
|
601
|
+
const response = await fetch(spec.url, withIdentityEncoding({
|
|
602
|
+
headers: spec.headers,
|
|
603
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
604
|
+
}))
|
|
600
605
|
const body: unknown = await response.json().catch(() => undefined)
|
|
601
606
|
if (!response.ok) throw new Error(providerErrorMessage(response.status, body))
|
|
602
607
|
const parsed = half.parse(response.status, body)
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,12 @@ import { makeUsageOverviewRoute, makeUsageRefreshRoute } from './host/routes.ts'
|
|
|
8
8
|
|
|
9
9
|
export const name = 'dsh-usage'
|
|
10
10
|
export const inject = ['webServer']
|
|
11
|
+
/**
|
|
12
|
+
* Family settings namespace this row serves. Since 0.1.7 the settings surface
|
|
13
|
+
* addresses one form per profile entry id and the Host generates this row's
|
|
14
|
+
* page from the Config schema below, so the namespace is only the alias the
|
|
15
|
+
* family settings bridge (dsh-web-settings) resolves onto this row's entry id.
|
|
16
|
+
*/
|
|
11
17
|
export const USAGE_SETTINGS_NAMESPACE = 'dsh-usage' as SettingsNamespace
|
|
12
18
|
|
|
13
19
|
export interface Config {
|
|
@@ -36,74 +42,72 @@ export function resolveConfig(config?: Config): ResolvedConfig {
|
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Tail of the process-wide flush chain; undefined while no predecessor's final
|
|
47
|
+
* ledger flush is still in flight. The Host reloads this profile row when its
|
|
48
|
+
* configuration changes, so a successor activation is a dispose + apply pair in
|
|
49
|
+
* the same process: its first ledger load must serialize behind its
|
|
50
|
+
* predecessor's final write (and behind the reloads before that one).
|
|
51
|
+
*/
|
|
52
|
+
let pendingStop: Promise<unknown> | undefined
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Host body. The plugin's own Config schema above is the settings document the
|
|
56
|
+
* 0.1.7 Host serves for this row; the effective config arrives here as the
|
|
57
|
+
* `config` argument the Loader resolved and the Host reloads the row when the
|
|
58
|
+
* user saves a change, so nothing in this body observes or writes settings.
|
|
59
|
+
* @param ctx - host root context.
|
|
60
|
+
* @param config - the row's resolved configuration.
|
|
61
|
+
*/
|
|
39
62
|
export const apply = mountOnce('@linxin666/dsh-usage', (ctx: Context, config?: Config): void => {
|
|
40
|
-
|
|
63
|
+
const value = resolveConfig(config)
|
|
64
|
+
// A disabled row mounts nothing: no probe cycle, no ledger, no routes. The
|
|
65
|
+
// settings page owns the enable switch, and re-enabling reloads this row.
|
|
66
|
+
if (!value.enabled) return
|
|
67
|
+
|
|
68
|
+
let live = true
|
|
41
69
|
let service: UsageService | undefined
|
|
42
|
-
let pendingStop: Promise<void> | undefined
|
|
43
70
|
let disposeRoutes: (() => void) | undefined
|
|
44
71
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (service !== next) return
|
|
61
|
-
next.start()
|
|
62
|
-
const disposers = [makeUsageOverviewRoute(ctx, next), makeUsageRefreshRoute(ctx, next)]
|
|
63
|
-
.map((route) => ctx.webServer.register(route))
|
|
64
|
-
disposeRoutes = () => {
|
|
65
|
-
for (const dispose of disposers) {
|
|
66
|
-
try {
|
|
67
|
-
dispose()
|
|
68
|
-
} catch {
|
|
69
|
-
// Route fiber already gone during shutdown.
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
+
const begin = (): void => {
|
|
73
|
+
// The fiber may have gone down while the predecessor's final flush was
|
|
74
|
+
// still pending; only start while this activation is the live one.
|
|
75
|
+
if (!live) return
|
|
76
|
+
const next = new UsageService(ctx, value)
|
|
77
|
+
service = next
|
|
78
|
+
next.start()
|
|
79
|
+
const disposers = [makeUsageOverviewRoute(ctx, next), makeUsageRefreshRoute(ctx, next)]
|
|
80
|
+
.map((route) => ctx.webServer.register(route))
|
|
81
|
+
disposeRoutes = () => {
|
|
82
|
+
for (const dispose of disposers) {
|
|
83
|
+
try {
|
|
84
|
+
dispose()
|
|
85
|
+
} catch {
|
|
86
|
+
// Route fiber already gone during shutdown.
|
|
72
87
|
}
|
|
73
88
|
}
|
|
74
|
-
// Serialize the start behind a just-stopped predecessor: its final
|
|
75
|
-
// ledger flush must land on disk before this instance loads the file.
|
|
76
|
-
if (pendingStop !== undefined) pendingStop.then(begin, begin)
|
|
77
|
-
else begin()
|
|
78
|
-
} else {
|
|
79
|
-
service.applyOptions(value)
|
|
80
89
|
}
|
|
81
90
|
}
|
|
82
91
|
|
|
83
|
-
ctx.inject(['settings'], (settingsCtx) => {
|
|
84
|
-
try {
|
|
85
|
-
if (typeof settingsCtx.settings?.installSection === 'function') {
|
|
86
|
-
settingsCtx.settings.installSection(ctx, USAGE_SETTINGS_NAMESPACE, Config, config ?? {}, {
|
|
87
|
-
setSource: (next) => { source = next; rearm() },
|
|
88
|
-
onChange: rearm,
|
|
89
|
-
})
|
|
90
|
-
} else if (typeof settingsCtx.settings?.register === 'function') {
|
|
91
|
-
const scope = settingsCtx.settings.register(USAGE_SETTINGS_NAMESPACE, Config, { base: config ?? {} })
|
|
92
|
-
source = () => scope?.get?.() ?? (config ?? {})
|
|
93
|
-
scope?.watch?.(() => { rearm() })
|
|
94
|
-
rearm()
|
|
95
|
-
}
|
|
96
|
-
} catch {
|
|
97
|
-
// Defensive fallback against settings registration differences
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
|
|
101
92
|
ctx.effect(() => {
|
|
102
|
-
|
|
93
|
+
if (pendingStop === undefined) {
|
|
94
|
+
// No predecessor's flush is in flight: start now.
|
|
95
|
+
begin()
|
|
96
|
+
} else {
|
|
97
|
+
// Serialize the start behind every predecessor: their final writes must
|
|
98
|
+
// land on disk before this instance loads the file.
|
|
99
|
+
pendingStop = pendingStop.then(begin, begin).then(() => undefined)
|
|
100
|
+
}
|
|
103
101
|
return () => {
|
|
102
|
+
live = false
|
|
104
103
|
disposeRoutes?.()
|
|
105
|
-
|
|
104
|
+
disposeRoutes = undefined
|
|
105
|
+
const stopping = service?.stop()
|
|
106
106
|
service = undefined
|
|
107
|
+
// A successor waits for this instance's final flush, which itself waited
|
|
108
|
+
// for the predecessor's start: a row disposed before it ever started
|
|
109
|
+
// keeps the earlier link instead of dropping it.
|
|
110
|
+
if (stopping !== undefined) pendingStop = (pendingStop ?? Promise.resolve()).then(() => stopping)
|
|
107
111
|
}
|
|
108
112
|
}, 'dsh-usage: runtime')
|
|
109
113
|
})
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sidebar usage panel (issue #1592): the usage body seated directly under the
|
|
3
|
-
* sidebar entry row. The row carries the panel's controls (refresh, collapse
|
|
4
|
-
* chevron), so this component renders only the content: expanded, plan
|
|
5
|
-
* providers show their quota windows and balance-only providers their
|
|
6
|
-
* remaining amount; collapsed, a one-line strip shows the current session
|
|
7
|
-
* provider's today usage (tokens, calls, and cost when priced) from the same
|
|
8
|
-
* /api/dsh-usage/overview document the settings section renders. The open
|
|
9
|
-
* state lives in the mount (persisted to localStorage) and arrives through
|
|
10
|
-
* the open face; polling runs at 10s expanded, 30s collapsed, and pauses
|
|
11
|
-
* while the page is hidden.
|
|
12
|
-
* @module @linxin666/dsh-usage/client/UsageSidebarPanel
|
|
13
|
-
*/
|
|
14
|
-
import { type ReactNode } from 'react';
|
|
15
|
-
import type { UsageStoreInstance } from './usage-store.ts';
|
|
16
|
-
/** Expanded-state face the owning mount supplies (subscribe + current value). */
|
|
17
|
-
export interface OpenStateSource {
|
|
18
|
-
subscribe(listener: () => void): () => void;
|
|
19
|
-
isOpen(): boolean;
|
|
20
|
-
}
|
|
21
|
-
/** Props of the sidebar panel (the apply body supplies the store and poller). */
|
|
22
|
-
export interface UsageSidebarPanelProps {
|
|
23
|
-
store: UsageStoreInstance;
|
|
24
|
-
/** Fetch one overview now. */
|
|
25
|
-
poll: () => void;
|
|
26
|
-
/** Expanded state owned by the mount; false renders nothing and pauses polling. */
|
|
27
|
-
open: OpenStateSource;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Render the sidebar usage panel body.
|
|
31
|
-
* @param props - store, the poll callback, and the mount's open-state face.
|
|
32
|
-
* @returns the panel content, or null while collapsed.
|
|
33
|
-
*/
|
|
34
|
-
export declare function UsageSidebarPanel(props: UsageSidebarPanelProps): ReactNode;
|