openclacky 1.2.7 → 1.2.9
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/lib/clacky/agent.rb +3 -0
- data/lib/clacky/agent_config.rb +91 -7
- data/lib/clacky/billing/billing_store.rb +107 -3
- data/lib/clacky/cli.rb +105 -0
- data/lib/clacky/client.rb +38 -5
- data/lib/clacky/default_skills/channel-manager/SKILL.md +33 -110
- data/lib/clacky/default_skills/deploy/SKILL.md +2 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +39 -0
- data/lib/clacky/default_skills/mcp-manager/SKILL.md +0 -7
- data/lib/clacky/default_skills/media-gen/SKILL.md +128 -0
- data/lib/clacky/media/base.rb +68 -0
- data/lib/clacky/media/gemini.rb +36 -0
- data/lib/clacky/media/generator.rb +78 -0
- data/lib/clacky/media/openai_compat.rb +168 -0
- data/lib/clacky/patch_loader.rb +282 -0
- data/lib/clacky/providers.rb +82 -0
- data/lib/clacky/server/channel/adapters/base.rb +4 -0
- data/lib/clacky/server/channel/channel_manager.rb +1 -1
- data/lib/clacky/server/channel/user_adapter_loader.rb +177 -0
- data/lib/clacky/server/channel.rb +5 -0
- data/lib/clacky/server/http_server.rb +236 -25
- data/lib/clacky/server/scheduler.rb +1 -4
- data/lib/clacky/shell_hook_loader.rb +181 -0
- data/lib/clacky/telemetry.rb +11 -5
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +326 -24
- data/lib/clacky/web/billing.js +117 -22
- data/lib/clacky/web/i18n.js +84 -6
- data/lib/clacky/web/index.html +14 -2
- data/lib/clacky/web/model-tester.js +58 -0
- data/lib/clacky/web/onboard.js +17 -30
- data/lib/clacky/web/settings.js +322 -97
- data/lib/clacky.rb +9 -0
- data/scripts/build/lib/network.sh +61 -30
- data/scripts/install.sh +61 -30
- data/scripts/install_browser.sh +61 -30
- data/scripts/install_full.sh +61 -30
- data/scripts/install_rails_deps.sh +61 -30
- data/scripts/install_system_deps.sh +61 -30
- metadata +12 -3
- data/lib/clacky/default_skills/channel-manager/feishu_setup.rb +0 -574
data/lib/clacky/web/app.css
CHANGED
|
@@ -3858,6 +3858,7 @@ body {
|
|
|
3858
3858
|
.model-card-grid-actions {
|
|
3859
3859
|
display: flex;
|
|
3860
3860
|
flex-direction: row;
|
|
3861
|
+
align-items: center;
|
|
3861
3862
|
flex-wrap: wrap;
|
|
3862
3863
|
gap: 0.5rem;
|
|
3863
3864
|
padding-top: 0.625rem;
|
|
@@ -3867,7 +3868,7 @@ body {
|
|
|
3867
3868
|
display: inline-flex;
|
|
3868
3869
|
align-items: center;
|
|
3869
3870
|
justify-content: center;
|
|
3870
|
-
gap: 0.
|
|
3871
|
+
gap: 0.3125rem;
|
|
3871
3872
|
padding: 0.375rem 0.5rem;
|
|
3872
3873
|
border: 1px solid var(--color-border-primary);
|
|
3873
3874
|
border-radius: 6px;
|
|
@@ -3877,15 +3878,9 @@ body {
|
|
|
3877
3878
|
cursor: pointer;
|
|
3878
3879
|
transition: all 0.15s;
|
|
3879
3880
|
white-space: nowrap;
|
|
3881
|
+
min-width: 0;
|
|
3880
3882
|
}
|
|
3881
|
-
.btn-card-grid-action svg {
|
|
3882
|
-
flex-shrink: 0;
|
|
3883
|
-
}
|
|
3884
|
-
.btn-card-grid-action span {
|
|
3885
|
-
overflow: hidden;
|
|
3886
|
-
text-overflow: ellipsis;
|
|
3887
|
-
white-space: nowrap;
|
|
3888
|
-
}
|
|
3883
|
+
.btn-card-grid-action svg { flex-shrink: 0; }
|
|
3889
3884
|
.btn-card-grid-action:hover:not(:disabled) {
|
|
3890
3885
|
border-color: var(--color-accent-primary);
|
|
3891
3886
|
color: var(--color-accent-primary);
|
|
@@ -3895,13 +3890,22 @@ body {
|
|
|
3895
3890
|
opacity: 0.5;
|
|
3896
3891
|
cursor: not-allowed;
|
|
3897
3892
|
}
|
|
3893
|
+
.btn-card-grid-action-primary {
|
|
3894
|
+
background: var(--color-bg-secondary);
|
|
3895
|
+
color: var(--color-text-primary);
|
|
3896
|
+
font-weight: 500;
|
|
3897
|
+
}
|
|
3898
3898
|
.btn-card-grid-action-danger:hover:not(:disabled) {
|
|
3899
3899
|
border-color: var(--color-error);
|
|
3900
3900
|
color: var(--color-error);
|
|
3901
3901
|
background: color-mix(in srgb, var(--color-error) 8%, transparent);
|
|
3902
3902
|
}
|
|
3903
|
-
|
|
3904
|
-
|
|
3903
|
+
.model-card-grid-toolbar {
|
|
3904
|
+
display: flex;
|
|
3905
|
+
flex-wrap: wrap;
|
|
3906
|
+
gap: 0.375rem;
|
|
3907
|
+
margin-left: auto;
|
|
3908
|
+
justify-content: flex-end;
|
|
3905
3909
|
}
|
|
3906
3910
|
.model-card-grid-footer {
|
|
3907
3911
|
display: flex;
|
|
@@ -9018,11 +9022,10 @@ body.setup-mode[data-theme="dark"] {
|
|
|
9018
9022
|
transition: height 0.2s ease;
|
|
9019
9023
|
}
|
|
9020
9024
|
.billing-cache-miss {
|
|
9021
|
-
background: #
|
|
9025
|
+
background: #f59e0b;
|
|
9022
9026
|
width: 100%;
|
|
9023
9027
|
transition: height 0.2s ease;
|
|
9024
|
-
}
|
|
9025
|
-
.billing-output-bar {
|
|
9028
|
+
}.billing-output-bar {
|
|
9026
9029
|
width: 12px;
|
|
9027
9030
|
background: #10b981;
|
|
9028
9031
|
border-radius: 2px 2px 0 0;
|
|
@@ -9038,10 +9041,10 @@ body.setup-mode[data-theme="dark"] {
|
|
|
9038
9041
|
}
|
|
9039
9042
|
|
|
9040
9043
|
/* Legend colors */
|
|
9044
|
+
.billing-legend-total { background: #6366f1; }
|
|
9041
9045
|
.billing-legend-cache-hit { background: #93c5fd; }
|
|
9042
|
-
.billing-legend-cache-miss { background: #
|
|
9046
|
+
.billing-legend-cache-miss { background: #f59e0b; }
|
|
9043
9047
|
.billing-legend-output { background: #10b981; }
|
|
9044
|
-
|
|
9045
9048
|
/* ── Chart Tooltip ───────────────────────────────────────────────────── */
|
|
9046
9049
|
.billing-chart-tooltip {
|
|
9047
9050
|
display: none;
|
|
@@ -9085,10 +9088,10 @@ body.setup-mode[data-theme="dark"] {
|
|
|
9085
9088
|
border-radius: 50%;
|
|
9086
9089
|
flex-shrink: 0;
|
|
9087
9090
|
}
|
|
9091
|
+
.tooltip-total { background: #6366f1; }
|
|
9088
9092
|
.tooltip-cache-hit { background: #93c5fd; }
|
|
9089
|
-
.tooltip-cache-miss { background: #
|
|
9090
|
-
.tooltip-output { background: #10b981; }
|
|
9091
|
-
.tooltip-label {
|
|
9093
|
+
.tooltip-cache-miss { background: #f59e0b; }
|
|
9094
|
+
.tooltip-output { background: #10b981; }.tooltip-label {
|
|
9092
9095
|
flex: 1;
|
|
9093
9096
|
color: var(--color-text-secondary);
|
|
9094
9097
|
}
|
|
@@ -9155,8 +9158,8 @@ body.setup-mode[data-theme="dark"] {
|
|
|
9155
9158
|
border-radius: 3px;
|
|
9156
9159
|
transition: width 0.3s ease;
|
|
9157
9160
|
}
|
|
9158
|
-
.billing-bar-
|
|
9159
|
-
background: linear-gradient(90deg, #
|
|
9161
|
+
.billing-bar-total {
|
|
9162
|
+
background: linear-gradient(90deg, #6366f1 0%, #818cf8 100%);
|
|
9160
9163
|
}
|
|
9161
9164
|
.billing-bar-completion {
|
|
9162
9165
|
background: linear-gradient(90deg, #10b981 0%, #34d399 100%);
|
|
@@ -9164,10 +9167,9 @@ body.setup-mode[data-theme="dark"] {
|
|
|
9164
9167
|
.billing-bar-cache-read {
|
|
9165
9168
|
background: linear-gradient(90deg, #93c5fd 0%, #bfdbfe 100%);
|
|
9166
9169
|
}
|
|
9167
|
-
.billing-bar-cache-
|
|
9168
|
-
background: linear-gradient(90deg, #
|
|
9170
|
+
.billing-bar-cache-miss {
|
|
9171
|
+
background: linear-gradient(90deg, #f59e0b 0%, #fbbf24 100%);
|
|
9169
9172
|
}
|
|
9170
|
-
|
|
9171
9173
|
/* ── Model List ──────────────────────────────────────────────────────── */
|
|
9172
9174
|
.billing-model-list {
|
|
9173
9175
|
display: flex;
|
|
@@ -9569,3 +9571,303 @@ body.setup-mode[data-theme="dark"] {
|
|
|
9569
9571
|
color: var(--color-text-secondary);
|
|
9570
9572
|
font-weight: 500;
|
|
9571
9573
|
}
|
|
9574
|
+
|
|
9575
|
+
/* ── Sessions List ───────────────────────────────────────────────────── */
|
|
9576
|
+
.billing-sessions-row {
|
|
9577
|
+
margin-top: 1rem;
|
|
9578
|
+
}
|
|
9579
|
+
.billing-sessions-section {
|
|
9580
|
+
background: var(--color-bg-secondary);
|
|
9581
|
+
border: 1px solid var(--color-border-primary);
|
|
9582
|
+
border-radius: 12px;
|
|
9583
|
+
padding: 1.25rem;
|
|
9584
|
+
}
|
|
9585
|
+
.billing-sessions-section h3 {
|
|
9586
|
+
font-size: 0.9375rem;
|
|
9587
|
+
font-weight: 600;
|
|
9588
|
+
color: var(--color-text-primary);
|
|
9589
|
+
margin: 0 0 1rem 0;
|
|
9590
|
+
}
|
|
9591
|
+
.billing-sessions-empty {
|
|
9592
|
+
text-align: center;
|
|
9593
|
+
padding: 2rem;
|
|
9594
|
+
color: var(--color-text-secondary);
|
|
9595
|
+
font-size: 0.875rem;
|
|
9596
|
+
}
|
|
9597
|
+
.billing-sessions-header {
|
|
9598
|
+
display: grid;
|
|
9599
|
+
grid-template-columns: 36px 1.5fr 0.8fr 0.8fr 0.8fr 0.8fr 0.8fr 1fr;
|
|
9600
|
+
gap: 0.5rem;
|
|
9601
|
+
padding: 0.75rem 1rem;
|
|
9602
|
+
background: var(--color-bg-tertiary);
|
|
9603
|
+
border-radius: 8px;
|
|
9604
|
+
font-size: 0.75rem;
|
|
9605
|
+
font-weight: 600;
|
|
9606
|
+
color: var(--color-text-secondary);
|
|
9607
|
+
text-transform: uppercase;
|
|
9608
|
+
letter-spacing: 0.05em;
|
|
9609
|
+
margin-bottom: 0.5rem;
|
|
9610
|
+
}
|
|
9611
|
+
.billing-sessions-list {
|
|
9612
|
+
display: flex;
|
|
9613
|
+
flex-direction: column;
|
|
9614
|
+
gap: 0.25rem;
|
|
9615
|
+
}
|
|
9616
|
+
.billing-session-row {
|
|
9617
|
+
display: grid;
|
|
9618
|
+
grid-template-columns: 36px 1.5fr 0.8fr 0.8fr 0.8fr 0.8fr 0.8fr 1fr;
|
|
9619
|
+
gap: 0.5rem;
|
|
9620
|
+
padding: 0.75rem 1rem;
|
|
9621
|
+
border-radius: 8px;
|
|
9622
|
+
transition: background 0.15s;
|
|
9623
|
+
align-items: center;
|
|
9624
|
+
}
|
|
9625
|
+
.billing-session-row:hover {
|
|
9626
|
+
background: var(--color-bg-tertiary);
|
|
9627
|
+
}
|
|
9628
|
+
.billing-session-deleted {
|
|
9629
|
+
opacity: 0.7;
|
|
9630
|
+
border-left: 3px solid var(--color-warning);
|
|
9631
|
+
}
|
|
9632
|
+
.billing-session-deleted .billing-cell-main {
|
|
9633
|
+
color: var(--color-text-secondary);
|
|
9634
|
+
font-style: italic;
|
|
9635
|
+
}
|
|
9636
|
+
.billing-cell {
|
|
9637
|
+
font-size: 0.875rem;
|
|
9638
|
+
white-space: nowrap;
|
|
9639
|
+
overflow: hidden;
|
|
9640
|
+
text-overflow: ellipsis;
|
|
9641
|
+
}
|
|
9642
|
+
.billing-cell-index {
|
|
9643
|
+
font-size: 0.75rem;
|
|
9644
|
+
color: var(--color-text-secondary);
|
|
9645
|
+
text-align: center;
|
|
9646
|
+
}
|
|
9647
|
+
.billing-cell-session {
|
|
9648
|
+
display: flex;
|
|
9649
|
+
flex-direction: column;
|
|
9650
|
+
gap: 0.125rem;
|
|
9651
|
+
min-width: 0;
|
|
9652
|
+
}
|
|
9653
|
+
.billing-cell-main {
|
|
9654
|
+
font-weight: 500;
|
|
9655
|
+
color: var(--color-text-primary);
|
|
9656
|
+
font-family: var(--font-mono);
|
|
9657
|
+
white-space: nowrap;
|
|
9658
|
+
overflow: hidden;
|
|
9659
|
+
text-overflow: ellipsis;
|
|
9660
|
+
}
|
|
9661
|
+
.billing-cell-sub {
|
|
9662
|
+
font-size: 0.7rem;
|
|
9663
|
+
color: var(--color-text-secondary);
|
|
9664
|
+
white-space: nowrap;
|
|
9665
|
+
overflow: hidden;
|
|
9666
|
+
text-overflow: ellipsis;
|
|
9667
|
+
}
|
|
9668
|
+
.billing-cell-number {
|
|
9669
|
+
font-family: var(--font-mono);
|
|
9670
|
+
text-align: right;
|
|
9671
|
+
color: var(--color-text-primary);
|
|
9672
|
+
}
|
|
9673
|
+
.billing-cell-hit {
|
|
9674
|
+
color: #3b82f6;
|
|
9675
|
+
}
|
|
9676
|
+
.billing-cell-miss {
|
|
9677
|
+
color: #f59e0b;
|
|
9678
|
+
}
|
|
9679
|
+
.billing-cell-cost {
|
|
9680
|
+
font-family: var(--font-mono);
|
|
9681
|
+
font-weight: 600;
|
|
9682
|
+
color: var(--color-accent);
|
|
9683
|
+
text-align: right;
|
|
9684
|
+
}
|
|
9685
|
+
.billing-cell-time {
|
|
9686
|
+
font-size: 0.75rem;
|
|
9687
|
+
color: var(--color-text-secondary);
|
|
9688
|
+
}
|
|
9689
|
+
@media (max-width: 768px) {
|
|
9690
|
+
.billing-sessions-header {
|
|
9691
|
+
display: none;
|
|
9692
|
+
}
|
|
9693
|
+
.billing-session-row {
|
|
9694
|
+
grid-template-columns: 1fr 1fr;
|
|
9695
|
+
gap: 0.5rem;
|
|
9696
|
+
padding: 1rem;
|
|
9697
|
+
}
|
|
9698
|
+
.billing-cell-index {
|
|
9699
|
+
display: none;
|
|
9700
|
+
}
|
|
9701
|
+
.billing-cell-session {
|
|
9702
|
+
grid-column: 1 / -1;
|
|
9703
|
+
}
|
|
9704
|
+
.billing-cell-number {
|
|
9705
|
+
text-align: left;
|
|
9706
|
+
}
|
|
9707
|
+
.billing-cell-number::before {
|
|
9708
|
+
font-size: 0.625rem;
|
|
9709
|
+
color: var(--color-text-secondary);
|
|
9710
|
+
text-transform: uppercase;
|
|
9711
|
+
display: block;
|
|
9712
|
+
}
|
|
9713
|
+
.billing-cell-time {
|
|
9714
|
+
grid-column: 1 / -1;
|
|
9715
|
+
text-align: right;
|
|
9716
|
+
}
|
|
9717
|
+
}
|
|
9718
|
+
|
|
9719
|
+
/* ════ Media generation (Settings → Models tab) ════ */
|
|
9720
|
+
.settings-section-desc {
|
|
9721
|
+
font-size: 0.8125rem;
|
|
9722
|
+
color: var(--color-text-secondary);
|
|
9723
|
+
line-height: 1.5;
|
|
9724
|
+
margin: -0.25rem 0 0.5rem;
|
|
9725
|
+
}
|
|
9726
|
+
.media-row {
|
|
9727
|
+
border: 1px solid var(--color-border-primary);
|
|
9728
|
+
border-radius: 8px;
|
|
9729
|
+
background: var(--color-bg-secondary);
|
|
9730
|
+
margin-bottom: 0.5rem;
|
|
9731
|
+
overflow: hidden;
|
|
9732
|
+
}
|
|
9733
|
+
.media-row.is-expanded {
|
|
9734
|
+
background: var(--color-bg-secondary);
|
|
9735
|
+
}
|
|
9736
|
+
.media-row-head {
|
|
9737
|
+
display: flex;
|
|
9738
|
+
align-items: center;
|
|
9739
|
+
gap: 0.75rem;
|
|
9740
|
+
padding: 0.5rem 0.75rem;
|
|
9741
|
+
min-height: 2.25rem;
|
|
9742
|
+
}
|
|
9743
|
+
.media-row-title {
|
|
9744
|
+
font-size: 0.875rem;
|
|
9745
|
+
font-weight: 600;
|
|
9746
|
+
color: var(--color-text-primary);
|
|
9747
|
+
min-width: 3rem;
|
|
9748
|
+
}
|
|
9749
|
+
.media-row-segmented {
|
|
9750
|
+
display: inline-flex;
|
|
9751
|
+
background: var(--color-bg-primary);
|
|
9752
|
+
border: 1px solid var(--color-border-primary);
|
|
9753
|
+
border-radius: 6px;
|
|
9754
|
+
padding: 1px;
|
|
9755
|
+
}
|
|
9756
|
+
.media-row-segmented button {
|
|
9757
|
+
background: transparent;
|
|
9758
|
+
border: none;
|
|
9759
|
+
padding: 0.25rem 0.625rem;
|
|
9760
|
+
font-size: 0.75rem;
|
|
9761
|
+
color: var(--color-text-secondary);
|
|
9762
|
+
border-radius: 5px;
|
|
9763
|
+
cursor: pointer;
|
|
9764
|
+
transition: background-color 0.15s ease, color 0.15s ease;
|
|
9765
|
+
font-family: inherit;
|
|
9766
|
+
}
|
|
9767
|
+
.media-row-segmented button:hover:not(.is-active):not(:disabled) {
|
|
9768
|
+
color: var(--color-text-primary);
|
|
9769
|
+
}
|
|
9770
|
+
.media-row-segmented button.is-active {
|
|
9771
|
+
background: var(--color-button-primary);
|
|
9772
|
+
color: #fff;
|
|
9773
|
+
}
|
|
9774
|
+
.media-row-segmented button:disabled {
|
|
9775
|
+
opacity: 0.4;
|
|
9776
|
+
cursor: not-allowed;
|
|
9777
|
+
}
|
|
9778
|
+
.media-row-status {
|
|
9779
|
+
margin-left: auto;
|
|
9780
|
+
font-size: 0.75rem;
|
|
9781
|
+
color: var(--color-text-secondary);
|
|
9782
|
+
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
9783
|
+
white-space: nowrap;
|
|
9784
|
+
overflow: hidden;
|
|
9785
|
+
text-overflow: ellipsis;
|
|
9786
|
+
max-width: 16rem;
|
|
9787
|
+
}
|
|
9788
|
+
.media-row-detail {
|
|
9789
|
+
border-top: 1px solid var(--color-border-primary);
|
|
9790
|
+
padding: 0.625rem 0.75rem;
|
|
9791
|
+
font-size: 0.8125rem;
|
|
9792
|
+
color: var(--color-text-secondary);
|
|
9793
|
+
line-height: 1.5;
|
|
9794
|
+
background: var(--color-bg-primary);
|
|
9795
|
+
}
|
|
9796
|
+
.media-row-detail.is-warning {
|
|
9797
|
+
color: var(--color-warning, #d97706);
|
|
9798
|
+
}
|
|
9799
|
+
.media-row-hint {
|
|
9800
|
+
margin-top: 0.375rem;
|
|
9801
|
+
font-size: 0.75rem;
|
|
9802
|
+
color: var(--color-text-tertiary, var(--color-text-secondary));
|
|
9803
|
+
}
|
|
9804
|
+
.media-row-detail .media-kv {
|
|
9805
|
+
display: grid;
|
|
9806
|
+
grid-template-columns: max-content 1fr;
|
|
9807
|
+
gap: 0.25rem 0.75rem;
|
|
9808
|
+
font-size: 0.8125rem;
|
|
9809
|
+
}
|
|
9810
|
+
.media-row-detail .media-kv-key {
|
|
9811
|
+
color: var(--color-text-secondary);
|
|
9812
|
+
}
|
|
9813
|
+
.media-row-detail .media-kv-val {
|
|
9814
|
+
color: var(--color-text-primary);
|
|
9815
|
+
font-weight: 500;
|
|
9816
|
+
word-break: break-all;
|
|
9817
|
+
}
|
|
9818
|
+
.media-row-actions {
|
|
9819
|
+
display: flex;
|
|
9820
|
+
gap: 0.5rem;
|
|
9821
|
+
margin-top: 0.5rem;
|
|
9822
|
+
justify-content: flex-end;
|
|
9823
|
+
}
|
|
9824
|
+
.media-row-btn {
|
|
9825
|
+
background: transparent;
|
|
9826
|
+
border: 1px solid var(--color-border-primary);
|
|
9827
|
+
color: var(--color-text-primary);
|
|
9828
|
+
border-radius: 6px;
|
|
9829
|
+
padding: 0.3125rem 0.75rem;
|
|
9830
|
+
font-size: 0.75rem;
|
|
9831
|
+
cursor: pointer;
|
|
9832
|
+
transition: background-color 0.15s ease;
|
|
9833
|
+
font-family: inherit;
|
|
9834
|
+
}
|
|
9835
|
+
.media-row-btn:hover {
|
|
9836
|
+
background: var(--color-bg-secondary);
|
|
9837
|
+
}
|
|
9838
|
+
.media-row-btn.is-primary {
|
|
9839
|
+
background: var(--color-button-primary);
|
|
9840
|
+
color: #fff;
|
|
9841
|
+
border-color: transparent;
|
|
9842
|
+
}
|
|
9843
|
+
.media-row-btn.is-primary:hover {
|
|
9844
|
+
background: var(--color-button-primary-hover);
|
|
9845
|
+
}
|
|
9846
|
+
.media-custom-form {
|
|
9847
|
+
display: grid;
|
|
9848
|
+
grid-template-columns: max-content 1fr;
|
|
9849
|
+
gap: 0.4375rem 0.625rem;
|
|
9850
|
+
align-items: center;
|
|
9851
|
+
}
|
|
9852
|
+
.media-custom-form label {
|
|
9853
|
+
font-size: 0.75rem;
|
|
9854
|
+
color: var(--color-text-secondary);
|
|
9855
|
+
}
|
|
9856
|
+
.media-custom-form input {
|
|
9857
|
+
width: 100%;
|
|
9858
|
+
background: var(--color-bg-secondary);
|
|
9859
|
+
border: 1px solid var(--color-border-primary);
|
|
9860
|
+
color: var(--color-text-primary);
|
|
9861
|
+
border-radius: 6px;
|
|
9862
|
+
padding: 0.375rem 0.5rem;
|
|
9863
|
+
font-size: 0.8125rem;
|
|
9864
|
+
font-family: inherit;
|
|
9865
|
+
box-sizing: border-box;
|
|
9866
|
+
}
|
|
9867
|
+
.media-custom-form .media-form-actions {
|
|
9868
|
+
grid-column: 1 / -1;
|
|
9869
|
+
display: flex;
|
|
9870
|
+
gap: 0.5rem;
|
|
9871
|
+
justify-content: flex-end;
|
|
9872
|
+
margin-top: 0.25rem;
|
|
9873
|
+
}
|
data/lib/clacky/web/billing.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const Billing = (() => {
|
|
5
5
|
let _summary = null;
|
|
6
6
|
let _daily = [];
|
|
7
|
+
let _sessions = []; // 会话列表
|
|
7
8
|
let _allModels = []; // 保存完整的模型列表
|
|
8
9
|
let _currentPeriod = "day";
|
|
9
10
|
let _currentModel = "all";
|
|
@@ -73,15 +74,19 @@ const Billing = (() => {
|
|
|
73
74
|
|
|
74
75
|
try {
|
|
75
76
|
const modelParam = (_currentModel && _currentModel !== "all") ? `&model=${encodeURIComponent(_currentModel)}` : "";
|
|
76
|
-
const [summaryRes, dailyRes] = await Promise.all([
|
|
77
|
+
const [summaryRes, dailyRes, sessionsRes] = await Promise.all([
|
|
77
78
|
fetch(`/api/billing/summary?period=${_currentPeriod}${modelParam}`),
|
|
78
|
-
fetch(`/api/billing/daily?days=30${modelParam}`)
|
|
79
|
+
fetch(`/api/billing/daily?days=30${modelParam}`),
|
|
80
|
+
fetch(`/api/billing/sessions?period=${_currentPeriod}${modelParam}&limit=100`)
|
|
79
81
|
]);
|
|
80
82
|
|
|
81
83
|
_summary = await summaryRes.json();
|
|
82
84
|
const dailyData = await dailyRes.json();
|
|
83
85
|
_daily = dailyData.days || [];
|
|
84
86
|
|
|
87
|
+
const sessionsData = await sessionsRes.json();
|
|
88
|
+
_sessions = sessionsData.sessions || [];
|
|
89
|
+
|
|
85
90
|
// 保存完整模型列表(仅在未筛选时更新)
|
|
86
91
|
if (!_currentModel || _currentModel === "all") {
|
|
87
92
|
_allModels = _summary.by_model ? Object.keys(_summary.by_model) : [];
|
|
@@ -172,6 +177,10 @@ const Billing = (() => {
|
|
|
172
177
|
<div class="billing-chart-row">
|
|
173
178
|
${_renderCombinedChart()}
|
|
174
179
|
</div>
|
|
180
|
+
|
|
181
|
+
<div class="billing-sessions-row">
|
|
182
|
+
${_renderSessionList()}
|
|
183
|
+
</div>
|
|
175
184
|
</div>
|
|
176
185
|
`;
|
|
177
186
|
|
|
@@ -219,14 +228,19 @@ const Billing = (() => {
|
|
|
219
228
|
<span class="tooltip-date">${date}</span>
|
|
220
229
|
<span class="tooltip-total">${total} tokens</span>
|
|
221
230
|
</div>
|
|
231
|
+
<div class="tooltip-row">
|
|
232
|
+
<span class="tooltip-dot tooltip-total"></span>
|
|
233
|
+
<span class="tooltip-label">${I18n.t("billing.totalTokens") || "Total Tokens"}</span>
|
|
234
|
+
<span class="tooltip-value">${total}</span>
|
|
235
|
+
</div>
|
|
222
236
|
<div class="tooltip-row">
|
|
223
237
|
<span class="tooltip-dot tooltip-cache-hit"></span>
|
|
224
|
-
<span class="tooltip-label">${I18n.t("billing.inputCacheHit") || "Input (
|
|
238
|
+
<span class="tooltip-label">${I18n.t("billing.inputCacheHit") || "Input (Hit)"}</span>
|
|
225
239
|
<span class="tooltip-value">${cacheHit}</span>
|
|
226
240
|
</div>
|
|
227
241
|
<div class="tooltip-row">
|
|
228
242
|
<span class="tooltip-dot tooltip-cache-miss"></span>
|
|
229
|
-
<span class="tooltip-label">${I18n.t("billing.inputCacheMiss") || "Input (
|
|
243
|
+
<span class="tooltip-label">${I18n.t("billing.inputCacheMiss") || "Input (Miss)"}</span>
|
|
230
244
|
<span class="tooltip-value">${cacheMiss}</span>
|
|
231
245
|
</div>
|
|
232
246
|
<div class="tooltip-row">
|
|
@@ -333,35 +347,50 @@ const Billing = (() => {
|
|
|
333
347
|
}
|
|
334
348
|
|
|
335
349
|
function _renderTokenBreakdown() {
|
|
350
|
+
const totalTokens = _summary.total_tokens || 0;
|
|
351
|
+
const promptTokens = _summary.prompt_tokens || 0;
|
|
352
|
+
const completionTokens = _summary.completion_tokens || 0;
|
|
353
|
+
const cacheReadTokens = _summary.cache_read_tokens || 0;
|
|
354
|
+
const cacheMissTokens = promptTokens - cacheReadTokens;
|
|
355
|
+
|
|
336
356
|
return `
|
|
337
357
|
<div class="billing-section billing-token-section">
|
|
338
358
|
<h3>${I18n.t("billing.tokenBreakdown") || "Token Breakdown"}</h3>
|
|
339
359
|
<div class="billing-token-bars">
|
|
340
360
|
<div class="billing-token-bar-item">
|
|
341
361
|
<div class="billing-token-bar-header">
|
|
342
|
-
<span class="billing-token-bar-label">${I18n.t("billing.
|
|
343
|
-
<span class="billing-token-bar-value">${_formatCompact(
|
|
362
|
+
<span class="billing-token-bar-label">${I18n.t("billing.totalTokens") || "Total Tokens"}</span>
|
|
363
|
+
<span class="billing-token-bar-value">${_formatCompact(totalTokens)}</span>
|
|
344
364
|
</div>
|
|
345
365
|
<div class="billing-token-bar-track">
|
|
346
|
-
<div class="billing-token-bar-fill billing-bar-
|
|
366
|
+
<div class="billing-token-bar-fill billing-bar-total" style="width: 100%"></div>
|
|
347
367
|
</div>
|
|
348
368
|
</div>
|
|
349
369
|
<div class="billing-token-bar-item">
|
|
350
370
|
<div class="billing-token-bar-header">
|
|
351
|
-
<span class="billing-token-bar-label">${I18n.t("billing.
|
|
352
|
-
<span class="billing-token-bar-value">${_formatCompact(
|
|
371
|
+
<span class="billing-token-bar-label">${I18n.t("billing.inputCacheHit") || "Input (Hit)"}</span>
|
|
372
|
+
<span class="billing-token-bar-value">${_formatCompact(cacheReadTokens)}</span>
|
|
353
373
|
</div>
|
|
354
374
|
<div class="billing-token-bar-track">
|
|
355
|
-
<div class="billing-token-bar-fill billing-bar-
|
|
375
|
+
<div class="billing-token-bar-fill billing-bar-cache-read" style="width: ${_getTokenPercent(cacheReadTokens, totalTokens)}%"></div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
<div class="billing-token-bar-item">
|
|
379
|
+
<div class="billing-token-bar-header">
|
|
380
|
+
<span class="billing-token-bar-label">${I18n.t("billing.inputCacheMiss") || "Input (Miss)"}</span>
|
|
381
|
+
<span class="billing-token-bar-value">${_formatCompact(cacheMissTokens)}</span>
|
|
382
|
+
</div>
|
|
383
|
+
<div class="billing-token-bar-track">
|
|
384
|
+
<div class="billing-token-bar-fill billing-bar-cache-miss" style="width: ${_getTokenPercent(cacheMissTokens, totalTokens)}%"></div>
|
|
356
385
|
</div>
|
|
357
386
|
</div>
|
|
358
387
|
<div class="billing-token-bar-item">
|
|
359
388
|
<div class="billing-token-bar-header">
|
|
360
389
|
<span class="billing-token-bar-label">${I18n.t("billing.output") || "Output"}</span>
|
|
361
|
-
<span class="billing-token-bar-value">${_formatCompact(
|
|
390
|
+
<span class="billing-token-bar-value">${_formatCompact(completionTokens)}</span>
|
|
362
391
|
</div>
|
|
363
392
|
<div class="billing-token-bar-track">
|
|
364
|
-
<div class="billing-token-bar-fill billing-bar-completion" style="width: ${_getTokenPercent(
|
|
393
|
+
<div class="billing-token-bar-fill billing-bar-completion" style="width: ${_getTokenPercent(completionTokens, totalTokens)}%"></div>
|
|
365
394
|
</div>
|
|
366
395
|
</div>
|
|
367
396
|
</div>
|
|
@@ -369,9 +398,8 @@ const Billing = (() => {
|
|
|
369
398
|
`;
|
|
370
399
|
}
|
|
371
400
|
|
|
372
|
-
function _getTokenPercent(
|
|
373
|
-
|
|
374
|
-
const value = _summary[type + '_tokens'] || 0;
|
|
401
|
+
function _getTokenPercent(value, total) {
|
|
402
|
+
if (!total || total === 0) return 0;
|
|
375
403
|
return Math.min((value / total) * 100, 100).toFixed(1);
|
|
376
404
|
}
|
|
377
405
|
|
|
@@ -427,9 +455,8 @@ const Billing = (() => {
|
|
|
427
455
|
|
|
428
456
|
const recentDays = _daily.slice(-14);
|
|
429
457
|
// Max values for scaling
|
|
430
|
-
const maxInput = Math.max(...recentDays.map(d =>
|
|
431
|
-
const maxOutput = Math.max(...recentDays.map(d => d.completion_tokens || 0), 1);
|
|
432
|
-
const maxVal = Math.max(maxInput, maxOutput);
|
|
458
|
+
const maxInput = Math.max(...recentDays.map(d => d.prompt_tokens || 0), 1);
|
|
459
|
+
const maxOutput = Math.max(...recentDays.map(d => d.completion_tokens || 0), 1); const maxVal = Math.max(maxInput, maxOutput);
|
|
433
460
|
|
|
434
461
|
// Chart height in pixels
|
|
435
462
|
const chartHeight = 120;
|
|
@@ -437,9 +464,10 @@ const Billing = (() => {
|
|
|
437
464
|
// Generate bars: each date has Input (stacked: cache hit + cache miss) and Output
|
|
438
465
|
const chartBars = recentDays.map((d, i) => {
|
|
439
466
|
const cacheHit = d.cache_read_tokens || 0; // 命中缓存
|
|
440
|
-
const
|
|
467
|
+
const totalPrompt = d.prompt_tokens || 0; // 全部输入token
|
|
468
|
+
const cacheMiss = totalPrompt - cacheHit; // 未命中缓存 = 全部输入 - 命中
|
|
441
469
|
const output = d.completion_tokens || 0;
|
|
442
|
-
const totalInput =
|
|
470
|
+
const totalInput = totalPrompt;
|
|
443
471
|
const totalTokens = totalInput + output;
|
|
444
472
|
|
|
445
473
|
// Calculate heights in pixels
|
|
@@ -471,13 +499,17 @@ const Billing = (() => {
|
|
|
471
499
|
<div class="billing-chart-header">
|
|
472
500
|
<h4>${I18n.t("billing.dailyUsage") || "Usage Details"}</h4>
|
|
473
501
|
<div class="billing-chart-legends">
|
|
502
|
+
<span class="billing-chart-legend">
|
|
503
|
+
<span class="billing-legend-dot billing-legend-total"></span>
|
|
504
|
+
${I18n.t("billing.totalTokens") || "Total Tokens"}
|
|
505
|
+
</span>
|
|
474
506
|
<span class="billing-chart-legend">
|
|
475
507
|
<span class="billing-legend-dot billing-legend-cache-hit"></span>
|
|
476
|
-
${I18n.t("billing.inputCacheHit") || "Input (
|
|
508
|
+
${I18n.t("billing.inputCacheHit") || "Input (Hit)"}
|
|
477
509
|
</span>
|
|
478
510
|
<span class="billing-chart-legend">
|
|
479
511
|
<span class="billing-legend-dot billing-legend-cache-miss"></span>
|
|
480
|
-
${I18n.t("billing.inputCacheMiss") || "Input (
|
|
512
|
+
${I18n.t("billing.inputCacheMiss") || "Input (Miss)"}
|
|
481
513
|
</span>
|
|
482
514
|
<span class="billing-chart-legend">
|
|
483
515
|
<span class="billing-legend-dot billing-legend-output"></span>
|
|
@@ -493,6 +525,69 @@ const Billing = (() => {
|
|
|
493
525
|
`;
|
|
494
526
|
}
|
|
495
527
|
|
|
528
|
+
function _renderSessionList() {
|
|
529
|
+
if (!_sessions || _sessions.length === 0) {
|
|
530
|
+
return `
|
|
531
|
+
<div class="billing-section billing-sessions-section">
|
|
532
|
+
<h3>${I18n.t("billing.sessions") || "Sessions"}</h3>
|
|
533
|
+
<div class="billing-sessions-empty">${I18n.t("billing.noSessions") || "No session data"}</div>
|
|
534
|
+
</div>
|
|
535
|
+
`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const rows = _sessions.map((s, index) => {
|
|
539
|
+
const sessionId = s.session_id || "unknown";
|
|
540
|
+
const isDeleted = s.is_deleted;
|
|
541
|
+
const sessionName = s.session_name || sessionId;
|
|
542
|
+
const displayName = isDeleted ? (I18n.t("billing.deletedSessions") || "已删除会话") : (sessionName.length > 25 ? sessionName.slice(0, 25) + "..." : sessionName);
|
|
543
|
+
const totalCost = _convertCost(s.total_cost || 0);
|
|
544
|
+
const totalTokens = s.total_tokens || 0;
|
|
545
|
+
const promptTokens = s.prompt_tokens || 0;
|
|
546
|
+
const cacheHit = s.cache_read_tokens || 0;
|
|
547
|
+
const cacheMiss = promptTokens - cacheHit;
|
|
548
|
+
const completionTokens = s.completion_tokens || 0;
|
|
549
|
+
const requests = s.requests || 0;
|
|
550
|
+
const models = (s.models || []).join(", ");
|
|
551
|
+
const lastRequest = s.last_request ? new Date(s.last_request).toLocaleString() : "-";
|
|
552
|
+
const rowClass = isDeleted ? "billing-session-row billing-session-deleted" : "billing-session-row";
|
|
553
|
+
|
|
554
|
+
return `
|
|
555
|
+
<div class="${rowClass}" data-session-id="${_esc(sessionId)}">
|
|
556
|
+
<div class="billing-cell billing-cell-index">${index + 1}</div>
|
|
557
|
+
<div class="billing-cell billing-cell-session" title="${_esc(sessionName)}">
|
|
558
|
+
<span class="billing-cell-main">${_esc(displayName)}</span>
|
|
559
|
+
<span class="billing-cell-sub">${requests} ${I18n.t("billing.requests") || "req"} · ${_esc(models)}</span>
|
|
560
|
+
</div>
|
|
561
|
+
<div class="billing-cell billing-cell-number">${_formatCompact(totalTokens)}</div>
|
|
562
|
+
<div class="billing-cell billing-cell-number billing-cell-hit">${_formatCompact(cacheHit)}</div>
|
|
563
|
+
<div class="billing-cell billing-cell-number billing-cell-miss">${_formatCompact(cacheMiss)}</div>
|
|
564
|
+
<div class="billing-cell billing-cell-number">${_formatCompact(completionTokens)}</div>
|
|
565
|
+
<div class="billing-cell billing-cell-cost">${_getCurrencySymbol()}${_formatCost(totalCost)}</div>
|
|
566
|
+
<div class="billing-cell billing-cell-time">${lastRequest}</div>
|
|
567
|
+
</div>
|
|
568
|
+
`;
|
|
569
|
+
}).join("");
|
|
570
|
+
|
|
571
|
+
return `
|
|
572
|
+
<div class="billing-section billing-sessions-section">
|
|
573
|
+
<h3>${I18n.t("billing.sessions") || "Sessions"}</h3>
|
|
574
|
+
<div class="billing-sessions-header">
|
|
575
|
+
<span class="billing-cell billing-cell-index">#</span>
|
|
576
|
+
<span class="billing-cell billing-cell-session">${I18n.t("billing.sessionId") || "Session"}</span>
|
|
577
|
+
<span class="billing-cell billing-cell-number">${I18n.t("billing.headerTotal") || "总消耗"}</span>
|
|
578
|
+
<span class="billing-cell billing-cell-number">${I18n.t("billing.headerHit") || "命中"}</span>
|
|
579
|
+
<span class="billing-cell billing-cell-number">${I18n.t("billing.headerMiss") || "未命中"}</span>
|
|
580
|
+
<span class="billing-cell billing-cell-number">${I18n.t("billing.headerOutput") || "输出"}</span>
|
|
581
|
+
<span class="billing-cell billing-cell-cost">${I18n.t("billing.cost") || "Cost"}</span>
|
|
582
|
+
<span class="billing-cell billing-cell-time">${I18n.t("billing.lastRequest") || "Time"}</span>
|
|
583
|
+
</div>
|
|
584
|
+
<div class="billing-sessions-list">
|
|
585
|
+
${rows}
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
`;
|
|
589
|
+
}
|
|
590
|
+
|
|
496
591
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
497
592
|
|
|
498
593
|
function _formatCost(cost) {
|