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:
@@ -21,6 +21,7 @@ type ChatEngine struct {
|
||||
tools json.RawMessage
|
||||
onChunk func(map[string]interface{})
|
||||
stream bool
|
||||
TotalTokens int
|
||||
}
|
||||
|
||||
// NewChatEngine creates a new ChatEngine instance.
|
||||
@@ -71,6 +72,10 @@ func (ce *ChatEngine) RunWithTools(ctx context.Context, messages []orchestrator.
|
||||
return finalContent, allToolCalls, allToolResults, err
|
||||
}
|
||||
|
||||
if resp.Usage.TotalTokens > 0 {
|
||||
ce.TotalTokens += resp.Usage.TotalTokens
|
||||
}
|
||||
|
||||
choice := resp.Choices[0]
|
||||
content := cleanThinkingTags(choice.Message.Content)
|
||||
|
||||
@@ -123,6 +128,11 @@ func (ce *ChatEngine) RunWithTools(ctx context.Context, messages []orchestrator.
|
||||
"content": result.Content,
|
||||
"is_error": result.IsError,
|
||||
}
|
||||
if result.Meta != nil {
|
||||
for k, v := range result.Meta {
|
||||
resultData[k] = v
|
||||
}
|
||||
}
|
||||
allToolResults = append(allToolResults, map[string]interface{}{
|
||||
"tool_call_id": tc.ID,
|
||||
"name": tc.Function.Name,
|
||||
@@ -149,6 +159,11 @@ func (ce *ChatEngine) RunWithTools(ctx context.Context, messages []orchestrator.
|
||||
return finalContent, allToolCalls, allToolResults, nil
|
||||
}
|
||||
|
||||
// ProviderName returns the name of the active provider used by the engine.
|
||||
func (ce *ChatEngine) ProviderName() string {
|
||||
return ce.orchestrator.ProviderName()
|
||||
}
|
||||
|
||||
// RunNonStream executes chat without streaming content to client.
|
||||
func (ce *ChatEngine) RunNonStream(ctx context.Context, messages []orchestrator.Message) (string, error) {
|
||||
var finalContent string
|
||||
@@ -159,6 +174,10 @@ func (ce *ChatEngine) RunNonStream(ctx context.Context, messages []orchestrator.
|
||||
return finalContent, err
|
||||
}
|
||||
|
||||
if resp.Usage.TotalTokens > 0 {
|
||||
ce.TotalTokens += resp.Usage.TotalTokens
|
||||
}
|
||||
|
||||
choice := resp.Choices[0]
|
||||
content := cleanThinkingTags(choice.Message.Content)
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -32,9 +32,10 @@ type Conversation struct {
|
||||
}
|
||||
|
||||
type ConversationStore struct {
|
||||
mu sync.RWMutex
|
||||
path string
|
||||
conv *Conversation
|
||||
mu sync.RWMutex
|
||||
path string
|
||||
conv *Conversation
|
||||
realTokens int
|
||||
}
|
||||
|
||||
type TokenCount struct {
|
||||
@@ -133,6 +134,7 @@ func (cs *ConversationStore) Clear() {
|
||||
cs.conv.Summary = ""
|
||||
cs.conv.CreatedAt = time.Now().Format(time.RFC3339)
|
||||
cs.conv.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||
cs.realTokens = 0
|
||||
cs.save()
|
||||
}
|
||||
|
||||
@@ -154,9 +156,22 @@ func (cs *ConversationStore) TrimOld(keepCount int) {
|
||||
}
|
||||
|
||||
func (cs *ConversationStore) ApproxTokenCount() int {
|
||||
if cs.realTokens > 0 {
|
||||
return cs.realTokens
|
||||
}
|
||||
return cs.ApproxTokenCountDetailed().total
|
||||
}
|
||||
|
||||
// AddRealTokens accumulates actual token counts from the API response.
|
||||
func (cs *ConversationStore) AddRealTokens(tokens int) {
|
||||
if tokens <= 0 {
|
||||
return
|
||||
}
|
||||
cs.mu.Lock()
|
||||
cs.realTokens += tokens
|
||||
cs.mu.Unlock()
|
||||
}
|
||||
|
||||
func (cs *ConversationStore) ApproxTokenCountDetailed() TokenCount {
|
||||
cs.mu.RLock()
|
||||
defer cs.mu.RUnlock()
|
||||
|
||||
@@ -3,9 +3,12 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/muyue/muyue/internal/agent"
|
||||
"github.com/muyue/muyue/internal/orchestrator"
|
||||
@@ -42,7 +45,14 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
orb.SetSystemPrompt(agent.StudioSystemPrompt())
|
||||
var studioPrompt strings.Builder
|
||||
studioPrompt.WriteString(agent.StudioSystemPrompt())
|
||||
studioPrompt.WriteString(fmt.Sprintf("\nDate: %s\nHeure: %s\n", time.Now().Format("02/01/2006"), time.Now().Format("15:04:05")))
|
||||
studioPrompt.WriteString(fmt.Sprintf("Root: %t\n", os.Geteuid() == 0))
|
||||
if os.Geteuid() != 0 {
|
||||
studioPrompt.WriteString("⚠️ Session utilisateur standard — les commandes sudo/doas nécessitent une autorisation. N'utilise PAS sudo ou doas sans demander.\n")
|
||||
}
|
||||
orb.SetSystemPrompt(studioPrompt.String())
|
||||
orb.SetTools(s.agentToolsJSON)
|
||||
|
||||
if body.Stream {
|
||||
@@ -91,6 +101,9 @@ func (s *Server) handleStreamChat(w http.ResponseWriter, orb *orchestrator.Orche
|
||||
storeContent = string(storeJSON)
|
||||
}
|
||||
s.convStore.Add("assistant", storeContent)
|
||||
s.convStore.AddRealTokens(engine.TotalTokens)
|
||||
|
||||
s.consumption.Record(engine.ProviderName(), engine.TotalTokens)
|
||||
|
||||
sseWriter.Write(map[string]interface{}{"done": "true"})
|
||||
}
|
||||
@@ -107,6 +120,10 @@ func (s *Server) handleNonStreamChat(w http.ResponseWriter, orb *orchestrator.Or
|
||||
}
|
||||
|
||||
s.convStore.Add("assistant", finalContent)
|
||||
s.convStore.AddRealTokens(engine.TotalTokens)
|
||||
|
||||
s.consumption.Record(engine.ProviderName(), engine.TotalTokens)
|
||||
|
||||
writeJSON(w, map[string]string{"content": finalContent})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,21 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const summarizePrompt = `Résume la conversation suivante de manière concise et structurée. Garde les points clés, les décisions prises, le contexte technique important. Le résumé doit permettre de continuer la conversation sans perte de contexte. Réponds uniquement avec le résumé, sans meta-commentaire.`
|
||||
const summarizePrompt = `Résume cette conversation de manière ultra-concise et structurée.
|
||||
|
||||
CONSERVE :
|
||||
- Les décisions techniques prises et leur rationale
|
||||
- Les configurations modifiées (noms exacts, valeurs)
|
||||
- Les fichiers/chemins manipulés
|
||||
- Les erreurs rencontrées et leurs résolutions
|
||||
- Le contexte nécessaire pour continuer
|
||||
|
||||
ÉLIMINE :
|
||||
- Les échanges de politesse
|
||||
- Les tentatives infructueuses (sauf si la solution n'a pas été trouvée)
|
||||
- Les sorties d'outils brutes (garde seulement les conclusions)
|
||||
|
||||
FORMAT : Markdown structuré avec sections. Max 500 mots. Pas de méta-commentaire.`
|
||||
|
||||
func writeJSON(w http.ResponseWriter, data interface{}) {
|
||||
json.NewEncoder(w).Encode(data)
|
||||
|
||||
@@ -534,6 +534,39 @@ func (s *Server) handleProvidersQuota(w http.ResponseWriter, r *http.Request) {
|
||||
q.Healthy = p.APIKey != ""
|
||||
if p.APIKey == "" {
|
||||
q.Error = "no API key"
|
||||
results = append(results, q)
|
||||
continue
|
||||
}
|
||||
mimoBase := p.BaseURL
|
||||
if mimoBase == "" {
|
||||
mimoBase = "https://token-plan-ams.xiaomimimo.com/v1"
|
||||
}
|
||||
req, _ := http.NewRequest("GET", strings.TrimRight(mimoBase, "/")+"/models", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+p.APIKey)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
q.Error = err.Error()
|
||||
} else {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
var data map[string]interface{}
|
||||
if json.Unmarshal(body, &data) == nil {
|
||||
if modelList, ok := data["data"].([]interface{}); ok {
|
||||
models := make([]map[string]interface{}, 0)
|
||||
for _, m := range modelList {
|
||||
if mm, ok := m.(map[string]interface{}); ok {
|
||||
id, _ := mm["id"].(string)
|
||||
if id != "" {
|
||||
models = append(models, map[string]interface{}{
|
||||
"model": id,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
q.Data = map[string]interface{}{"models": models, "available": len(models)}
|
||||
q.Healthy = true
|
||||
}
|
||||
}
|
||||
}
|
||||
case "claude", "anthropic":
|
||||
// Claude Code n'a pas d'API externe, vérifier l'installation
|
||||
@@ -551,6 +584,15 @@ func (s *Server) handleProvidersQuota(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, map[string]interface{}{"providers": results})
|
||||
}
|
||||
|
||||
func (s *Server) handleProvidersConsumption(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
writeError(w, "GET only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
data := s.consumption.GetAll()
|
||||
writeJSON(w, map[string]interface{}{"providers": data})
|
||||
}
|
||||
|
||||
func (s *Server) handleRecentCommands(w http.ResponseWriter, r *http.Request) {
|
||||
home, _ := os.UserHomeDir()
|
||||
type cmdEntry struct {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/muyue/muyue/internal/agent"
|
||||
"github.com/muyue/muyue/internal/orchestrator"
|
||||
@@ -62,35 +63,36 @@ func (s *Server) handleShellChat(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) buildShellSystemPrompt(_ ShellChatRequest) string {
|
||||
func (s *Server) buildShellSystemPrompt(req ShellChatRequest) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(`Tu es l'Analyste Système de Muyue. Tu es un expert en administration système et développement.
|
||||
Tu aides l'utilisateur à comprendre son système, diagnostiquer des problèmes, et optimiser son environnement.
|
||||
|
||||
OUTILS DISPONIBLES:
|
||||
- terminal: Exécute des commandes shell sur le système local et retourne le résultat
|
||||
|
||||
RÈGLES:
|
||||
- Utilise l'outil terminal pour exécuter des commandes quand c'est nécessaire
|
||||
- Analyse les résultats et explique-les clairement
|
||||
- Formate tes réponses en markdown avec des blocs de code quand approprié
|
||||
- Sois concis et technique
|
||||
- Quand tu proposes des commandes alternatives, utilise des blocs de code markdown
|
||||
|
||||
`)
|
||||
sb.WriteString(shellSystemPromptBase)
|
||||
|
||||
analysis := LoadSystemAnalysis()
|
||||
if analysis != "" {
|
||||
sb.WriteString("=== ANALYSE SYSTÈME ACTUELLE ===\n")
|
||||
sb.WriteString("<system_context>\n")
|
||||
sb.WriteString(analysis)
|
||||
sb.WriteString("\n=== FIN DE L'ANALYSE ===\n\n")
|
||||
sb.WriteString("\n</system_context>\n\n")
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("OS: %s/%s\n", runtime.GOOS, runtime.GOARCH))
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
sb.WriteString("Hostname: " + hostname + "\n")
|
||||
}
|
||||
if user := os.Getenv("USER"); user != "" {
|
||||
sb.WriteString("User: " + user + "\n")
|
||||
}
|
||||
|
||||
isRoot := os.Geteuid() == 0
|
||||
sb.WriteString(fmt.Sprintf("Root: %t\n", isRoot))
|
||||
if isRoot {
|
||||
sb.WriteString("⚠️ Session en root — toutes les commandes ont les privilèges administrateur.\n")
|
||||
} else {
|
||||
sb.WriteString("⚠️ Session utilisateur standard — les commandes sudo/doas nécessitent une autorisation. N'utilise PAS sudo ou doas sans demander.\n")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
sb.WriteString(fmt.Sprintf("Date: %s\nHeure: %s\n", now.Format("02/01/2006"), now.Format("15:04:05")))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
@@ -131,6 +133,9 @@ func (s *Server) handleShellChatStream(w http.ResponseWriter, orb *orchestrator.
|
||||
storeContent = string(storeJSON)
|
||||
}
|
||||
s.shellConvStore.Add("assistant", storeContent)
|
||||
s.shellConvStore.AddRealTokens(engine.TotalTokens)
|
||||
|
||||
s.consumption.Record(engine.ProviderName(), engine.TotalTokens)
|
||||
|
||||
sseWriter.Write(map[string]interface{}{
|
||||
"done": "true",
|
||||
@@ -150,6 +155,10 @@ func (s *Server) handleShellChatNonStream(w http.ResponseWriter, orb *orchestrat
|
||||
}
|
||||
|
||||
s.shellConvStore.Add("assistant", finalContent)
|
||||
s.shellConvStore.AddRealTokens(engine.TotalTokens)
|
||||
|
||||
s.consumption.Record(engine.ProviderName(), engine.TotalTokens)
|
||||
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"content": finalContent,
|
||||
"tokens": s.shellConvStore.ApproxTokens(),
|
||||
@@ -289,15 +298,33 @@ func (s *Server) handleShellAnalyze(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
orb.SetSystemPrompt(agent.StudioSystemPrompt())
|
||||
|
||||
analysisPrompt := `Tu es un expert en administration système. Analyse les informations suivantes sur le système de l'utilisateur.
|
||||
Génère un rapport d'analyse concis et structuré en markdown qui inclut:
|
||||
1. Un résumé de l'état du système
|
||||
2. Les points d'attention (performance, sécurité, configuration)
|
||||
3. Des recommandations spécifiques d'optimisation
|
||||
4. Les outils manquants qui pourraient être utiles
|
||||
5. L'état du réseau et des connexions
|
||||
analysisPrompt := `Tu es un expert en administration système. Analyse les informations suivantes et génère un rapport structuré en markdown.
|
||||
|
||||
Sois concret et technique. Le rapport sera utilisé comme contexte pour un assistant terminal.
|
||||
STRUCTURE REQUISE :
|
||||
|
||||
## État du système
|
||||
- Résumé en 2-3 phrases de l'état général (OK/Attention/Critique)
|
||||
|
||||
## Points d'attention
|
||||
Liste les problèmes détectés par priorité :
|
||||
- **CRITIQUE** : problèmes de sécurité, espace disque < 10%, mémoire < 10%
|
||||
- **ATTENTION** : CPU élevé, services en échec, config non-optimale
|
||||
- **INFO** : améliorations possibles, mises à jour disponibles
|
||||
|
||||
## Recommandations
|
||||
Pour chaque point d'attention, donne UNE commande ou action corrective concrète.
|
||||
|
||||
## Outils manquants
|
||||
Liste les outils utiles non installés avec la commande d'installation.
|
||||
|
||||
## Réseau
|
||||
- Interfaces actives, ports en écoute, connectivité
|
||||
|
||||
RÈGLES :
|
||||
- Pas de blabla générique — sois spécifique à CE système
|
||||
- Inclus les valeurs numériques réelles (%, Go, MHz)
|
||||
- Max 1500 mots
|
||||
- Le rapport sert de contexte persistant pour un assistant terminal
|
||||
|
||||
` + sysInfo.String()
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ type Server struct {
|
||||
mux *http.ServeMux
|
||||
convStore *ConversationStore
|
||||
shellConvStore *ShellConvStore
|
||||
consumption *consumptionStore
|
||||
agentRegistry *agent.Registry
|
||||
agentToolsJSON json.RawMessage
|
||||
shellAgentRegistry *agent.Registry
|
||||
@@ -50,6 +51,7 @@ func NewServer(cfg *config.MuyueConfig) *Server {
|
||||
s.scanResult = scanner.ScanSystem()
|
||||
s.convStore = NewConversationStore()
|
||||
s.shellConvStore = NewShellConvStore()
|
||||
s.consumption = newConsumptionStore()
|
||||
s.agentRegistry = agent.DefaultRegistry()
|
||||
tools := s.agentRegistry.OpenAITools()
|
||||
toolsJSON, _ := json.Marshal(tools)
|
||||
@@ -131,6 +133,7 @@ func (s *Server) routes() {
|
||||
s.mux.HandleFunc("/api/skills/import", s.handleSkillImport)
|
||||
s.mux.HandleFunc("/api/dashboard/status", s.handleDashboardStatus)
|
||||
s.mux.HandleFunc("/api/providers/quota", s.handleProvidersQuota)
|
||||
s.mux.HandleFunc("/api/providers/consumption", s.handleProvidersConsumption)
|
||||
s.mux.HandleFunc("/api/recent-commands", s.handleRecentCommands)
|
||||
s.mux.HandleFunc("/api/running-processes", s.handleRunningProcesses)
|
||||
s.mux.HandleFunc("/api/system/metrics", s.handleSystemMetrics)
|
||||
|
||||
@@ -14,6 +14,63 @@ import (
|
||||
const shellMaxTokens = 100000
|
||||
const shellCharsPerToken = 4
|
||||
|
||||
const shellSystemPromptBase = `Tu es l'**Analyste Système** de Muyue. Tu es un expert en administration système, DevOps et développement.
|
||||
|
||||
<critical_rules>
|
||||
1. **AGIS, ne décris pas** — Utilise l'outil terminal pour exécuter, ne te contente pas de proposer des commandes.
|
||||
2. **SOIS AUTONOME** — Cherche les infos manquantes via des commandes avant de demander à l'utilisateur. Essaie plusieurs approches avant de bloquer.
|
||||
3. **SOIS CONCIS** — Max 4 lignes par défaut. Pas de préambule. Réponse directe et technique.
|
||||
4. **GÈRE LES ERREURS** — Si une commande échoue, lis l'erreur, comprends la cause, essaie une approche alternative. 2-3 tentatives avant de rapporter.
|
||||
5. **SÉCURITÉ** — Ne révèle jamais de credentials. Demande confirmation avant les commandes destructrices (rm -rf, format, etc.).
|
||||
6. **LANGUE** — Réponds dans la même langue que l'utilisateur.
|
||||
</critical_rules>
|
||||
|
||||
<tool_usage>
|
||||
Outil disponible : **terminal** — Exécute des commandes shell sur le système local.
|
||||
|
||||
Stratégies :
|
||||
- **Diagnostique** — Enchaîne les commandes de diagnostic (ps, df, free, top, journalctl, dmesg, netstat, ss, etc.)
|
||||
- **Parallélisme** — Combine les commandes avec && ou ; quand elles sont indépendantes
|
||||
- **Filtrage** — Utilise grep, awk, sort, head pour extraire l'essentiel des sorties volumineuses
|
||||
- **Non-interactif** — Préfère les commandes non-interactives (apt install -y, non pas apt install)
|
||||
- **Troncature** — Si le résultat dépasse 2000 caractères, résume les points clés au lieu de tout afficher
|
||||
</tool_usage>
|
||||
|
||||
<decision_making>
|
||||
- Décide par toi-même : exécute des commandes pour comprendre l'état du système
|
||||
- Ne demande confirmation que pour les actions destructrices
|
||||
- Si tu ne connais pas la commande exacte, exécute la commande avec --help pour la trouver
|
||||
- Si bloqué : documente ce que tu as essayé, pourquoi, et l'action minimale requise
|
||||
- Ne t'arrête jamais pour une tâche complexe — découpe en étapes et exécute-les
|
||||
</decision_making>
|
||||
|
||||
<error_recovery>
|
||||
1. Lis le message d'erreur complet (stderr + stdout)
|
||||
2. Identifie la cause racine (permissions, paquet manquant, config, service)
|
||||
3. Essaie : vérifier le service, vérifier les logs, chercher le paquet, tester la connexion
|
||||
4. Propose une solution concrète, pas générique
|
||||
</error_recovery>
|
||||
|
||||
<response_format>
|
||||
- **Commandes** : blocs markdown avec le langage (bash, sh, etc.)
|
||||
- **Résultats** : résume les métriques clés, pas de dump complet
|
||||
- **Erreurs** : cause + solution en 1-2 lignes
|
||||
- **Succès** : confirmation en 1 ligne
|
||||
- **Analyses** : markdown structuré avec sections si nécessaire
|
||||
</response_format>
|
||||
|
||||
<mermaid>
|
||||
Tu peux utiliser des diagrammes Mermaid pour visualiser :
|
||||
- Architecture système (graph TD/LR)
|
||||
- Flux réseau (sequenceDiagram)
|
||||
- Processus (flowchart)
|
||||
- Timeline (gantt)
|
||||
|
||||
Utilise un bloc de code avec le langage mermaid quand ça clarifie l'explication. Pas pour du texte simple.
|
||||
</mermaid>
|
||||
|
||||
`
|
||||
|
||||
type ShellMessage struct {
|
||||
ID string `json:"id"`
|
||||
Role string `json:"role"`
|
||||
@@ -22,9 +79,10 @@ type ShellMessage struct {
|
||||
}
|
||||
|
||||
type ShellConvStore struct {
|
||||
mu sync.RWMutex
|
||||
path string
|
||||
msgs []ShellMessage
|
||||
mu sync.RWMutex
|
||||
path string
|
||||
msgs []ShellMessage
|
||||
realTokens int
|
||||
}
|
||||
|
||||
func NewShellConvStore() *ShellConvStore {
|
||||
@@ -82,19 +140,37 @@ func (s *ShellConvStore) Clear() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.msgs = []ShellMessage{}
|
||||
s.realTokens = 0
|
||||
s.save()
|
||||
}
|
||||
|
||||
func (s *ShellConvStore) ApproxTokens() int {
|
||||
if s.realTokens > 0 {
|
||||
return s.realTokens
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
total := 0
|
||||
for _, m := range s.msgs {
|
||||
total += utf8.RuneCountInString(m.Content) / shellCharsPerToken
|
||||
}
|
||||
total += utf8.RuneCountInString(shellSystemPromptBase) / shellCharsPerToken
|
||||
if analysis := LoadSystemAnalysis(); analysis != "" {
|
||||
total += utf8.RuneCountInString(analysis) / shellCharsPerToken
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// AddRealTokens accumulates actual token counts from the API response.
|
||||
func (s *ShellConvStore) AddRealTokens(tokens int) {
|
||||
if tokens <= 0 {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.realTokens += tokens
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *ShellConvStore) AtLimit() bool {
|
||||
return s.ApproxTokens() >= shellMaxTokens
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user