feat: terminal sudo blocking, token tracking, mermaid & consumption UI
All checks were successful
Beta Release / beta (push) Successful in 1m3s
All checks were successful
Beta Release / beta (push) Successful in 1m3s
- Block sudo/doas commands when not running as root - Add real token counting from API responses - Track and display consumption by provider/day - Add Mermaid diagram rendering in Shell and Studio - Add copy-to-clipboard buttons for code blocks - Support tables in AI message rendering - Update system prompt with context (date, time, root status) 💘 Generated with Crush Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
This commit is contained in:
127
internal/api/consumption.go
Normal file
127
internal/api/consumption.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/muyue/muyue/internal/config"
|
||||
)
|
||||
|
||||
type consumptionEntry struct {
|
||||
Date string `json:"date"`
|
||||
Tokens int `json:"tokens"`
|
||||
Requests int `json:"requests"`
|
||||
}
|
||||
|
||||
type providerConsumption struct {
|
||||
Name string `json:"name"`
|
||||
Daily []consumptionEntry `json:"daily"`
|
||||
Total int `json:"total_tokens"`
|
||||
Requests int `json:"total_requests"`
|
||||
}
|
||||
|
||||
type consumptionStore struct {
|
||||
mu sync.Mutex
|
||||
providers map[string]*providerConsumption
|
||||
}
|
||||
|
||||
func newConsumptionStore() *consumptionStore {
|
||||
cs := &consumptionStore{
|
||||
providers: make(map[string]*providerConsumption),
|
||||
}
|
||||
cs.load()
|
||||
cs.prune()
|
||||
return cs
|
||||
}
|
||||
|
||||
func (cs *consumptionStore) Record(providerName string, tokens int) {
|
||||
if tokens <= 0 || providerName == "" {
|
||||
return
|
||||
}
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
today := time.Now().UTC().Format("2006-01-02")
|
||||
|
||||
p, ok := cs.providers[providerName]
|
||||
if !ok {
|
||||
p = &providerConsumption{Name: providerName}
|
||||
cs.providers[providerName] = p
|
||||
}
|
||||
|
||||
p.Total += tokens
|
||||
p.Requests++
|
||||
|
||||
if len(p.Daily) > 0 && p.Daily[len(p.Daily)-1].Date == today {
|
||||
p.Daily[len(p.Daily)-1].Tokens += tokens
|
||||
p.Daily[len(p.Daily)-1].Requests++
|
||||
} else {
|
||||
p.Daily = append(p.Daily, consumptionEntry{
|
||||
Date: today,
|
||||
Tokens: tokens,
|
||||
Requests: 1,
|
||||
})
|
||||
}
|
||||
|
||||
cs.save()
|
||||
}
|
||||
|
||||
func (cs *consumptionStore) GetAll() map[string]*providerConsumption {
|
||||
cs.mu.Lock()
|
||||
defer cs.mu.Unlock()
|
||||
|
||||
result := make(map[string]*providerConsumption)
|
||||
for k, v := range cs.providers {
|
||||
pc := *v
|
||||
daily := make([]consumptionEntry, len(v.Daily))
|
||||
copy(daily, v.Daily)
|
||||
pc.Daily = daily
|
||||
result[k] = &pc
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (cs *consumptionStore) prune() {
|
||||
cutoff := time.Now().UTC().AddDate(0, 0, -7).Format("2006-01-02")
|
||||
for _, p := range cs.providers {
|
||||
filtered := make([]consumptionEntry, 0)
|
||||
for _, d := range p.Daily {
|
||||
if d.Date >= cutoff {
|
||||
filtered = append(filtered, d)
|
||||
}
|
||||
}
|
||||
p.Daily = filtered
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *consumptionStore) filePath() string {
|
||||
dir, err := config.ConfigDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(dir, "consumption.json")
|
||||
}
|
||||
|
||||
func (cs *consumptionStore) load() {
|
||||
fp := cs.filePath()
|
||||
if fp == "" {
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(fp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
json.Unmarshal(data, &cs.providers)
|
||||
}
|
||||
|
||||
func (cs *consumptionStore) save() {
|
||||
fp := cs.filePath()
|
||||
if fp == "" {
|
||||
return
|
||||
}
|
||||
data, _ := json.Marshal(cs.providers)
|
||||
os.WriteFile(fp, data, 0644)
|
||||
}
|
||||
Reference in New Issue
Block a user